@vgai/editor-sdk 0.5.41 → 0.5.44
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.
- package/package.json +2 -2
- package/src/client.ts +339 -49
- package/src/contributions.ts +208 -1
- package/src/document-probe.ts +55 -2
- package/src/editor-view.ts +1 -0
- package/src/index.ts +15 -0
- package/src/types.ts +241 -12
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/editor-sdk",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.44",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@types/three": "^0.180.0",
|
|
27
|
-
"@vgai/sdk": "0.5.
|
|
27
|
+
"@vgai/sdk": "0.5.44"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@vgai/engine": "*",
|
package/src/client.ts
CHANGED
|
@@ -10,12 +10,18 @@ import type {
|
|
|
10
10
|
AssetPreviewOptions,
|
|
11
11
|
AssetPreviewShotSetDefinition,
|
|
12
12
|
AssetPreviewSource,
|
|
13
|
+
CaptureDimensions,
|
|
14
|
+
DocumentCameraPose,
|
|
15
|
+
DocumentLookOutcome,
|
|
16
|
+
EditorChromeCapture,
|
|
13
17
|
EditorState,
|
|
14
18
|
EditorView,
|
|
19
|
+
EditorWorkspaceName,
|
|
15
20
|
GameCapture,
|
|
16
21
|
GameplayRecordingCapture,
|
|
17
22
|
GameplayRecordingOptions,
|
|
18
23
|
GameplayRecordingStarted,
|
|
24
|
+
GameplayRecordingTimeline,
|
|
19
25
|
HelperVisibility,
|
|
20
26
|
InspectedFieldWrite,
|
|
21
27
|
InspectedHierarchy,
|
|
@@ -32,6 +38,9 @@ import type {
|
|
|
32
38
|
ShadingMode,
|
|
33
39
|
StoryCaptureOptions,
|
|
34
40
|
StoryVariantCapture,
|
|
41
|
+
StructureOp,
|
|
42
|
+
StructureOpOptions,
|
|
43
|
+
StructureOpResult,
|
|
35
44
|
TransformMode,
|
|
36
45
|
TransformSpace,
|
|
37
46
|
Vec3Value,
|
|
@@ -114,6 +123,59 @@ const COMMAND_DEADLINE_MS = 150_000;
|
|
|
114
123
|
*/
|
|
115
124
|
const CONSOLE_DRAIN_TIMEOUT_MS = 1_500;
|
|
116
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Node's `fetch` collapses EVERY network-layer failure into one two-word
|
|
128
|
+
* `TypeError: fetch failed`. The real reason — `ECONNREFUSED`, `ECONNRESET`,
|
|
129
|
+
* `EPIPE`, a DNS miss — lives only on `error.cause` (sometimes two links down,
|
|
130
|
+
* or inside an `AggregateError`), and nothing prints it unless something walks
|
|
131
|
+
* the chain. That is the whole reason `project.bake.preview` was observed
|
|
132
|
+
* failing with a bare "fetch failed" and no way to tell a dead editor from a
|
|
133
|
+
* momentary one (WORK.md, cold barrel 2026-08-29): the code below used to
|
|
134
|
+
* rethrow the `TypeError` untouched, on the belief — stated in a comment right
|
|
135
|
+
* where it happened — that "a connection refused / DNS failure still surfaces
|
|
136
|
+
* as itself". It does not. This walks the chain so the message can say which.
|
|
137
|
+
*/
|
|
138
|
+
function describeFetchFailure(error: unknown): { code: string; detail: string } {
|
|
139
|
+
const messages: string[] = [];
|
|
140
|
+
let current: unknown = error;
|
|
141
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
142
|
+
if (!(current instanceof Error)) break;
|
|
143
|
+
if (current.message) messages.push(current.message);
|
|
144
|
+
const code = (current as { code?: unknown }).code;
|
|
145
|
+
if (typeof code === 'string' && code !== '') {
|
|
146
|
+
return { code, detail: messages.join(' <- ') };
|
|
147
|
+
}
|
|
148
|
+
const aggregate = (current as { errors?: unknown }).errors;
|
|
149
|
+
if (Array.isArray(aggregate) && aggregate.length > 0) {
|
|
150
|
+
const inner = describeFetchFailure(aggregate[0]);
|
|
151
|
+
return { code: inner.code, detail: [...messages, inner.detail].join(' <- ') };
|
|
152
|
+
}
|
|
153
|
+
current = (current as { cause?: unknown }).cause;
|
|
154
|
+
}
|
|
155
|
+
return { code: 'UNKNOWN', detail: messages.join(' <- ') || String(error) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Transport failures where a second attempt is worth making: the connection
|
|
160
|
+
* itself failed or died, rather than the editor answering something unwelcome.
|
|
161
|
+
* A dev server that is restarting (any watched source edit restarts it) is
|
|
162
|
+
* unreachable for a fraction of a second and reachable again after — which is
|
|
163
|
+
* exactly the "fails, then succeeds unchanged" shape that was reported.
|
|
164
|
+
*/
|
|
165
|
+
const RETRYABLE_TRANSPORT_CODES = new Set([
|
|
166
|
+
'ECONNREFUSED',
|
|
167
|
+
'ECONNRESET',
|
|
168
|
+
'EPIPE',
|
|
169
|
+
'ETIMEDOUT',
|
|
170
|
+
'EHOSTUNREACH',
|
|
171
|
+
'UND_ERR_SOCKET',
|
|
172
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
/** Gap before the one automatic retry — long enough for a dev-server restart's
|
|
176
|
+
* listen socket to come back, short enough to stay invisible. */
|
|
177
|
+
const TRANSPORT_RETRY_DELAY_MS = 400;
|
|
178
|
+
|
|
117
179
|
/** Context accompanying one {@link EditorClient} response observation. */
|
|
118
180
|
export interface EditorEnvelopeObservation {
|
|
119
181
|
/** True only when this body came from the console-ledger endpoint and its
|
|
@@ -205,6 +267,9 @@ export class EditorClient {
|
|
|
205
267
|
* The observer must not throw; anything it raises is swallowed, because a
|
|
206
268
|
* reporting hook may never break the command it is reporting on.
|
|
207
269
|
*/
|
|
270
|
+
private readonly transport:
|
|
271
|
+
| ((body: Record<string, unknown>) => Promise<Record<string, unknown>>)
|
|
272
|
+
| null;
|
|
208
273
|
private readonly onEnvelope:
|
|
209
274
|
| ((body: unknown, observation: EditorEnvelopeObservation) => void)
|
|
210
275
|
| null;
|
|
@@ -212,6 +277,14 @@ export class EditorClient {
|
|
|
212
277
|
constructor(opts?: {
|
|
213
278
|
url?: string;
|
|
214
279
|
onEnvelope?: (body: unknown, observation: EditorEnvelopeObservation) => void;
|
|
280
|
+
/**
|
|
281
|
+
* An IN-PAGE command channel, for a client that lives inside the editor
|
|
282
|
+
* page itself on a tier with no editor server (the hosted build's
|
|
283
|
+
* contribution client). Given, every command goes through it instead of
|
|
284
|
+
* `POST /__editor/command`, and answers in the route's own body shape
|
|
285
|
+
* (`{ ok: true, ...data }` / `{ ok: false, error, code? }`).
|
|
286
|
+
*/
|
|
287
|
+
transport?: (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
215
288
|
}) {
|
|
216
289
|
if (opts !== undefined && (typeof opts !== 'object' || opts === null || Array.isArray(opts))) {
|
|
217
290
|
throw new TypeError(
|
|
@@ -219,7 +292,7 @@ export class EditorClient {
|
|
|
219
292
|
);
|
|
220
293
|
}
|
|
221
294
|
const unknownOptions = Object.keys(opts ?? {}).filter(
|
|
222
|
-
(key) => key !== 'url' && key !== 'onEnvelope',
|
|
295
|
+
(key) => key !== 'url' && key !== 'onEnvelope' && key !== 'transport',
|
|
223
296
|
);
|
|
224
297
|
if (unknownOptions.length > 0) {
|
|
225
298
|
throw new Error(
|
|
@@ -228,6 +301,7 @@ export class EditorClient {
|
|
|
228
301
|
}
|
|
229
302
|
this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, '');
|
|
230
303
|
this.onEnvelope = opts?.onEnvelope ?? null;
|
|
304
|
+
this.transport = opts?.transport ?? null;
|
|
231
305
|
this.game = {
|
|
232
306
|
state: async (name: string): Promise<unknown> => {
|
|
233
307
|
// `keys` narrows the relay to the one provider asked for, so a panel
|
|
@@ -245,28 +319,82 @@ export class EditorClient {
|
|
|
245
319
|
};
|
|
246
320
|
}
|
|
247
321
|
|
|
322
|
+
/**
|
|
323
|
+
* `retryTransport` opts a command into ONE automatic retry after a transport
|
|
324
|
+
* failure (see {@link RETRYABLE_TRANSPORT_CODES}). It is deliberately
|
|
325
|
+
* OPT-IN and off by default: a socket that died after the request was written
|
|
326
|
+
* cannot prove the editor did not already run the command, so a blanket retry
|
|
327
|
+
* would risk playing/stopping/writing twice. Read-only relays — the captures —
|
|
328
|
+
* have no such hazard and turn it on.
|
|
329
|
+
*/
|
|
248
330
|
private async command<T extends object = Record<string, never>>(
|
|
249
331
|
body: Record<string, unknown>,
|
|
332
|
+
options?: { retryTransport?: boolean },
|
|
250
333
|
): Promise<T> {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
334
|
+
const type = String(body['type'] ?? 'command');
|
|
335
|
+
if (this.transport) {
|
|
336
|
+
const answered = (await this.transport(body)) as {
|
|
337
|
+
ok: boolean;
|
|
338
|
+
error?: string;
|
|
339
|
+
code?: string;
|
|
340
|
+
} & T;
|
|
341
|
+
if (!answered.ok) {
|
|
342
|
+
throw new EditorCommandError(
|
|
343
|
+
answered.error ?? `Editor command "${type}" failed`,
|
|
344
|
+
answered.code,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return answered;
|
|
348
|
+
}
|
|
349
|
+
const url = `${this.baseUrl}/__editor/command`;
|
|
350
|
+
const request = JSON.stringify(body);
|
|
351
|
+
let res: Response | undefined;
|
|
352
|
+
let retried = false;
|
|
353
|
+
for (;;) {
|
|
354
|
+
try {
|
|
355
|
+
res = await fetch(url, {
|
|
356
|
+
method: 'POST',
|
|
357
|
+
headers: { 'Content-Type': 'application/json' },
|
|
358
|
+
body: request,
|
|
359
|
+
signal: AbortSignal.timeout(COMMAND_DEADLINE_MS),
|
|
360
|
+
});
|
|
361
|
+
break;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
// Only the deadline is reshaped into a "no answer" verdict; everything
|
|
364
|
+
// else is a TRANSPORT failure, and Node hides its reason behind a bare
|
|
365
|
+
// `fetch failed` (see `describeFetchFailure`).
|
|
366
|
+
if ((error as { name?: string } | null)?.name === 'TimeoutError') {
|
|
367
|
+
throw new EditorCommandError(
|
|
368
|
+
`The editor at ${this.baseUrl} never answered "${type}" ` +
|
|
369
|
+
`within ${Math.round(COMMAND_DEADLINE_MS / 1000)}s — past every server-side budget, so ` +
|
|
370
|
+
'the server itself is not answering. Check the terminal running `vgai edit`.',
|
|
371
|
+
undefined,
|
|
372
|
+
true,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const { code, detail } = describeFetchFailure(error);
|
|
376
|
+
if (options?.retryTransport === true && !retried && RETRYABLE_TRANSPORT_CODES.has(code)) {
|
|
377
|
+
retried = true;
|
|
378
|
+
await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS));
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
throw new EditorCommandError(
|
|
382
|
+
`POST ${url} ("${type}") never reached the editor: ${code}${detail ? ` (${detail})` : ''}.` +
|
|
383
|
+
(retried
|
|
384
|
+
? ` Retried once after ${TRANSPORT_RETRY_DELAY_MS}ms; it failed the same way.`
|
|
385
|
+
: '') +
|
|
386
|
+
(options?.retryTransport === true
|
|
387
|
+
? ''
|
|
388
|
+
: ' Not retried automatically: this command can change editor state, and a socket that' +
|
|
389
|
+
' died after the request was written cannot prove the editor did not already run it.') +
|
|
390
|
+
' A transport failure means the port stopped answering, not that the editor refused —' +
|
|
391
|
+
' the dev server restarts on any watched source edit, and it shuts itself down after an' +
|
|
392
|
+
' idle window. Check the terminal running `vgai edit`, and `vgai sessions` for the port' +
|
|
393
|
+
' this client resolved.',
|
|
394
|
+
code,
|
|
395
|
+
false,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
270
398
|
}
|
|
271
399
|
const data = (await this.readJson(res)) as { ok: boolean; error?: string; code?: string } & T;
|
|
272
400
|
if (!data.ok) {
|
|
@@ -375,6 +503,40 @@ export class EditorClient {
|
|
|
375
503
|
await this.command({ type: 'view-preset', preset });
|
|
376
504
|
}
|
|
377
505
|
|
|
506
|
+
/**
|
|
507
|
+
* LOOK AROUND THE OPEN MODEL, visibly. Swings the active Object3D
|
|
508
|
+
* document's camera — the one on the human's screen — by `azimuth`/
|
|
509
|
+
* `elevation` radians, animated over `duration` seconds, and resolves when
|
|
510
|
+
* the move ends. A human drag during the move cancels it where it stands
|
|
511
|
+
* (`cancelledBy: 'human'`); the promise still resolves.
|
|
512
|
+
*/
|
|
513
|
+
async orbitDocument(options: {
|
|
514
|
+
azimuth?: number;
|
|
515
|
+
elevation?: number;
|
|
516
|
+
duration?: number;
|
|
517
|
+
}): Promise<DocumentLookOutcome> {
|
|
518
|
+
return this.command<DocumentLookOutcome>({ type: 'document-orbit', ...options });
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** A slow full revolution around the open document's subject, at a constant rate. */
|
|
522
|
+
async turntableDocument(options?: {
|
|
523
|
+
seconds?: number;
|
|
524
|
+
revolutions?: number;
|
|
525
|
+
}): Promise<DocumentLookOutcome> {
|
|
526
|
+
return this.command<DocumentLookOutcome>({ type: 'document-turntable', ...options });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Frame the open document's subject (its selection if it has one). `fit`
|
|
531
|
+
* scales the fitted distance: 1 is the toolbar Frame button's tight fit.
|
|
532
|
+
*/
|
|
533
|
+
async frameDocument(fit?: number): Promise<DocumentCameraPose> {
|
|
534
|
+
return this.command<DocumentCameraPose>({
|
|
535
|
+
type: 'document-frame',
|
|
536
|
+
...(fit === undefined ? {} : { fit }),
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
378
540
|
async setCamera(position: Vec3Value, target: Vec3Value, fov?: number): Promise<void> {
|
|
379
541
|
await this.command({
|
|
380
542
|
type: 'set-camera',
|
|
@@ -437,6 +599,11 @@ export class EditorClient {
|
|
|
437
599
|
// re-derives (or softens) it.
|
|
438
600
|
...(flatness && typeof flatness.dominantFraction === 'number' ? { flatness } : {}),
|
|
439
601
|
...(data.loopRecoveryFrame === true ? { loopRecoveryFrame: true } : {}),
|
|
602
|
+
// Same pass-through rule: the recorded-run notice is written where the
|
|
603
|
+
// pixels are, so nothing here re-derives or softens it.
|
|
604
|
+
...(data.recording && typeof data.recording.notice === 'string'
|
|
605
|
+
? { recording: data.recording }
|
|
606
|
+
: {}),
|
|
440
607
|
};
|
|
441
608
|
}
|
|
442
609
|
|
|
@@ -457,6 +624,15 @@ export class EditorClient {
|
|
|
457
624
|
return this.command<GameplayRecordingCapture>({ type: 'bridge-recording-stop' });
|
|
458
625
|
}
|
|
459
626
|
|
|
627
|
+
/** Read the active capture's monotonic media position. This is the only clock
|
|
628
|
+
* suitable for selecting intervals inside the finalized recording. */
|
|
629
|
+
async getGameplayRecordingTimeline(): Promise<GameplayRecordingTimeline> {
|
|
630
|
+
const timeline = await this.command<GameplayRecordingTimeline>({
|
|
631
|
+
type: 'bridge-recording-timeline',
|
|
632
|
+
});
|
|
633
|
+
return { startedAt: timeline.startedAt, elapsedMs: timeline.elapsedMs };
|
|
634
|
+
}
|
|
635
|
+
|
|
460
636
|
/**
|
|
461
637
|
* Capture an isolated, deterministic four-view preview through the editor's
|
|
462
638
|
* native Asset Lab. The SDK delegates rendering to the editor; it never
|
|
@@ -466,14 +642,22 @@ export class EditorClient {
|
|
|
466
642
|
source: AssetPreviewSource,
|
|
467
643
|
options: AssetPreviewOptions = {},
|
|
468
644
|
): Promise<AssetPreviewCapture> {
|
|
469
|
-
const data = await this.command<AssetPreviewCapture>(
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
645
|
+
const data = await this.command<AssetPreviewCapture>(
|
|
646
|
+
{
|
|
647
|
+
type: 'capture-asset-preview',
|
|
648
|
+
...source,
|
|
649
|
+
...options,
|
|
650
|
+
},
|
|
651
|
+
// Photographing changes nothing, and this is the relay `vgai screenshot
|
|
652
|
+
// <module>` / `project.bake.preview` rides — the lane where a momentary
|
|
653
|
+
// transport failure cost a cold agent three probe modules.
|
|
654
|
+
{ retryTransport: true },
|
|
655
|
+
);
|
|
474
656
|
return {
|
|
475
657
|
width: data.width,
|
|
476
658
|
height: data.height,
|
|
659
|
+
// Absent from editors that predate orientation reporting.
|
|
660
|
+
...(data.orientation ? { orientation: data.orientation } : {}),
|
|
477
661
|
views: data.views,
|
|
478
662
|
contactSheet: data.contactSheet,
|
|
479
663
|
};
|
|
@@ -494,12 +678,15 @@ export class EditorClient {
|
|
|
494
678
|
definition: AssetPreviewShotSetDefinition,
|
|
495
679
|
options: AssetPreviewOptions = {},
|
|
496
680
|
): Promise<LabeledShotSetCapture> {
|
|
497
|
-
const data = await this.command<LabeledShotSetCapture>(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
681
|
+
const data = await this.command<LabeledShotSetCapture>(
|
|
682
|
+
{
|
|
683
|
+
type: 'capture-asset-preview',
|
|
684
|
+
...source,
|
|
685
|
+
...options,
|
|
686
|
+
shotSet: definition,
|
|
687
|
+
},
|
|
688
|
+
{ retryTransport: true },
|
|
689
|
+
);
|
|
503
690
|
return {
|
|
504
691
|
width: data.width,
|
|
505
692
|
height: data.height,
|
|
@@ -588,6 +775,13 @@ export class EditorClient {
|
|
|
588
775
|
await this.command({ type: 'open-asset-tab', path, kind });
|
|
589
776
|
}
|
|
590
777
|
|
|
778
|
+
/** SELECT a project asset — the other half of the browser's
|
|
779
|
+
* selection-vs-open contract (single click selects and fills the
|
|
780
|
+
* Inspector; double click opens a document). */
|
|
781
|
+
async selectAsset(path: string): Promise<void> {
|
|
782
|
+
await this.command({ type: 'select-asset', path });
|
|
783
|
+
}
|
|
784
|
+
|
|
591
785
|
/** Generate a project-owned native Rapier ragdoll from a rigged model asset. */
|
|
592
786
|
async generateRagdoll(assetPath: string): Promise<RagdollGenerationResult> {
|
|
593
787
|
return this.command<RagdollGenerationResult>({ type: 'generate-ragdoll', assetPath });
|
|
@@ -605,6 +799,14 @@ export class EditorClient {
|
|
|
605
799
|
await this.command({ type: 'toggle-console' });
|
|
606
800
|
}
|
|
607
801
|
|
|
802
|
+
/** Switch the editor's NAMED WORKSPACE — the task-named layout memory
|
|
803
|
+
* (`game`/`model`/`sculpt`/`texture`/`animate`/`look`). Resolves once the
|
|
804
|
+
* dock has finished rebuilding, so a following capture photographs the
|
|
805
|
+
* arrangement that was asked for. */
|
|
806
|
+
async setWorkspace(workspace: EditorWorkspaceName): Promise<void> {
|
|
807
|
+
await this.command({ type: 'set-workspace', workspace });
|
|
808
|
+
}
|
|
809
|
+
|
|
608
810
|
async showBuild(): Promise<void> {
|
|
609
811
|
await this.command({ type: 'show-build' });
|
|
610
812
|
}
|
|
@@ -645,6 +847,26 @@ export class EditorClient {
|
|
|
645
847
|
return data.subject;
|
|
646
848
|
}
|
|
647
849
|
|
|
850
|
+
/**
|
|
851
|
+
* One STRUCTURE op on the authored tree — the hierarchy context menu's own
|
|
852
|
+
* verbs, on the same helpers, for a caller with no pointer to right-click
|
|
853
|
+
* with. `id`/`ids` default to the current selection.
|
|
854
|
+
*/
|
|
855
|
+
async structureOp(op: StructureOp, options: StructureOpOptions = {}): Promise<StructureOpResult> {
|
|
856
|
+
return this.command<StructureOpResult>({ type: 'structure-op', op, ...options });
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/** "Extract Component…" — the hierarchy row's action, as a command. Answers
|
|
860
|
+
* the action's own sentence, which NAMES the files it created. */
|
|
861
|
+
async extractComponent(options: { id?: string; name?: string } = {}): Promise<{ hint: string }> {
|
|
862
|
+
return this.command<{ hint: string }>({ type: 'extract-component', ...options });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** "Fork Component…" — extract's twin: one new file, one callsite retargeted. */
|
|
866
|
+
async forkComponent(options: { id?: string } = {}): Promise<{ hint: string }> {
|
|
867
|
+
return this.command<{ hint: string }>({ type: 'fork-component', ...options });
|
|
868
|
+
}
|
|
869
|
+
|
|
648
870
|
/**
|
|
649
871
|
* The HIERARCHY PANEL's actual rendered row tree, as data.
|
|
650
872
|
*
|
|
@@ -672,6 +894,11 @@ export class EditorClient {
|
|
|
672
894
|
return data.hierarchy;
|
|
673
895
|
}
|
|
674
896
|
|
|
897
|
+
/** Run the Hierarchy panel's own Expand All action. */
|
|
898
|
+
async expandHierarchyAll(): Promise<void> {
|
|
899
|
+
await this.command<Record<string, never>>({ type: 'expand-hierarchy-all' });
|
|
900
|
+
}
|
|
901
|
+
|
|
675
902
|
/**
|
|
676
903
|
* Write one editable path through the active Inspector's own IO.
|
|
677
904
|
*
|
|
@@ -758,11 +985,33 @@ export class EditorClient {
|
|
|
758
985
|
return data.view;
|
|
759
986
|
}
|
|
760
987
|
|
|
761
|
-
/**
|
|
762
|
-
|
|
988
|
+
/**
|
|
989
|
+
* Capture the active center document exactly as presented to the user.
|
|
990
|
+
*
|
|
991
|
+
* A number is a SQUARE of that size (the default shape); `{width, height}`
|
|
992
|
+
* asks for a shaped frame — a video-aspect look that needs no crop. Both are
|
|
993
|
+
* bounded by the relay budget; see {@link CaptureDimensions}.
|
|
994
|
+
*/
|
|
995
|
+
/** Photograph the editor PAGE itself — every panel, as the person sees it.
|
|
996
|
+
* The sighted door for chrome: skins, workspaces and contributed panels are
|
|
997
|
+
* judged through this, never through DOM probes. */
|
|
998
|
+
async captureEditorChrome(): Promise<EditorChromeCapture> {
|
|
999
|
+
return this.command<EditorChromeCapture>({ type: 'capture-editor-chrome' });
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** With a view, present and capture it in one request so document discovery
|
|
1003
|
+
* cannot retarget the capture between two client calls. */
|
|
1004
|
+
async captureActiveDocument(
|
|
1005
|
+
size?: CaptureDimensions,
|
|
1006
|
+
view?: EditorView,
|
|
1007
|
+
): Promise<ActiveDocumentCapture> {
|
|
763
1008
|
return this.command<ActiveDocumentCapture>({
|
|
764
1009
|
type: 'capture-active-document',
|
|
765
|
-
...(
|
|
1010
|
+
...(view ? { view } : {}),
|
|
1011
|
+
...(typeof size === 'number' ? { size } : {}),
|
|
1012
|
+
...(typeof size === 'object' && size !== null
|
|
1013
|
+
? { width: size.width, height: size.height }
|
|
1014
|
+
: {}),
|
|
766
1015
|
});
|
|
767
1016
|
}
|
|
768
1017
|
|
|
@@ -778,6 +1027,17 @@ export class EditorClient {
|
|
|
778
1027
|
return this.command<DocumentProbeResult>({ type: 'document-probe', step });
|
|
779
1028
|
}
|
|
780
1029
|
|
|
1030
|
+
/**
|
|
1031
|
+
* Run a wire-carried step against the ACTIVE document's published context
|
|
1032
|
+
* (`packages/editor/src/document-context-registry.ts`) — the REPL door over
|
|
1033
|
+
* an open document, in Edit mode. `src` is the step's own `toString()`;
|
|
1034
|
+
* same serialization contract as `page-script` (no closures survive).
|
|
1035
|
+
*/
|
|
1036
|
+
async documentScript<T = unknown>(src: string): Promise<T> {
|
|
1037
|
+
const outcome = await this.command<{ result: T }>({ type: 'document-script', src });
|
|
1038
|
+
return outcome.result;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
781
1041
|
// --- Display (set semantics) ---
|
|
782
1042
|
|
|
783
1043
|
async setGrid(enabled: boolean): Promise<void> {
|
|
@@ -827,7 +1087,7 @@ export class EditorClient {
|
|
|
827
1087
|
template: ProjectTemplate = 'default',
|
|
828
1088
|
exampleId?: string,
|
|
829
1089
|
): Promise<ProjectInfo> {
|
|
830
|
-
const res = await
|
|
1090
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/create-project`, {
|
|
831
1091
|
method: 'POST',
|
|
832
1092
|
headers: { 'Content-Type': 'application/json' },
|
|
833
1093
|
body: JSON.stringify({ name, location, template, ...(exampleId ? { exampleId } : {}) }),
|
|
@@ -845,7 +1105,7 @@ export class EditorClient {
|
|
|
845
1105
|
}
|
|
846
1106
|
|
|
847
1107
|
async openProject(path: string): Promise<void> {
|
|
848
|
-
const res = await
|
|
1108
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/open-project`, {
|
|
849
1109
|
method: 'POST',
|
|
850
1110
|
headers: { 'Content-Type': 'application/json' },
|
|
851
1111
|
body: JSON.stringify({ path }),
|
|
@@ -857,14 +1117,14 @@ export class EditorClient {
|
|
|
857
1117
|
}
|
|
858
1118
|
|
|
859
1119
|
async getProject(): Promise<ProjectInfo | null> {
|
|
860
|
-
const res = await
|
|
1120
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/project`);
|
|
861
1121
|
const data = (await this.readJson(res)) as { project: ProjectInfo | null; error?: string };
|
|
862
1122
|
if (!res.ok) throw new Error(data.error ?? `Failed to get project: ${res.status}`);
|
|
863
1123
|
return data.project;
|
|
864
1124
|
}
|
|
865
1125
|
|
|
866
1126
|
async listRecentProjects(): Promise<RecentProject[]> {
|
|
867
|
-
const res = await
|
|
1127
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/recent-projects`);
|
|
868
1128
|
const data = (await this.readJson(res)) as { projects: RecentProject[]; error?: string };
|
|
869
1129
|
if (!res.ok) throw new Error(data.error ?? `Failed to list projects: ${res.status}`);
|
|
870
1130
|
return data.projects;
|
|
@@ -876,7 +1136,7 @@ export class EditorClient {
|
|
|
876
1136
|
* The editor server loads callable metadata in Node; modules never enter the
|
|
877
1137
|
* editor browser merely because they were listed. */
|
|
878
1138
|
async listProjectTools(): Promise<ProjectToolCatalog> {
|
|
879
|
-
const res = await
|
|
1139
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/project-tools`);
|
|
880
1140
|
const body = await this.readJson(res);
|
|
881
1141
|
if (!res.ok) throw new Error(`Failed to list project tools: ${res.status}`);
|
|
882
1142
|
return body as ProjectToolCatalog;
|
|
@@ -889,7 +1149,7 @@ export class EditorClient {
|
|
|
889
1149
|
input: unknown = {},
|
|
890
1150
|
options: { confirm?: boolean; instance?: string } = {},
|
|
891
1151
|
): Promise<ProjectToolOutcome> {
|
|
892
|
-
const res = await
|
|
1152
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/project-tools/run`, {
|
|
893
1153
|
method: 'POST',
|
|
894
1154
|
headers: { 'Content-Type': 'application/json' },
|
|
895
1155
|
body: JSON.stringify({
|
|
@@ -914,7 +1174,7 @@ export class EditorClient {
|
|
|
914
1174
|
/** Read the one project-local generation job ledger. Provider-native
|
|
915
1175
|
* request/result shapes remain on their registered operations. */
|
|
916
1176
|
async listGenerationJobs(): Promise<GenerationJobsDocument> {
|
|
917
|
-
const res = await
|
|
1177
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/generations`);
|
|
918
1178
|
const body = await this.readJson(res);
|
|
919
1179
|
if (!res.ok) throw new Error(`Failed to list generation jobs: ${res.status}`);
|
|
920
1180
|
return body as GenerationJobsDocument;
|
|
@@ -923,9 +1183,12 @@ export class EditorClient {
|
|
|
923
1183
|
/** Forget operational job state. Accepted provenance and project assets
|
|
924
1184
|
* are deliberately unaffected. */
|
|
925
1185
|
async forgetGenerationJob(id: string): Promise<boolean> {
|
|
926
|
-
const res = await
|
|
927
|
-
|
|
928
|
-
|
|
1186
|
+
const res = await this.httpFetch(
|
|
1187
|
+
`${this.baseUrl}/__editor/generations/${encodeURIComponent(id)}`,
|
|
1188
|
+
{
|
|
1189
|
+
method: 'DELETE',
|
|
1190
|
+
},
|
|
1191
|
+
);
|
|
929
1192
|
const body = (await this.readJson(res)) as { removed?: boolean; error?: string };
|
|
930
1193
|
if (!res.ok) throw new Error(body.error ?? `Failed to forget generation job: ${res.status}`);
|
|
931
1194
|
return body.removed === true;
|
|
@@ -936,7 +1199,7 @@ export class EditorClient {
|
|
|
936
1199
|
async getLogEntries(): Promise<
|
|
937
1200
|
Array<{ t: number; level: string; msg: string; source?: string }>
|
|
938
1201
|
> {
|
|
939
|
-
const res = await
|
|
1202
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/log-entries`);
|
|
940
1203
|
const data = (await this.readJson(res)) as {
|
|
941
1204
|
entries: Array<{ t: number; level: string; msg: string; source?: string }>;
|
|
942
1205
|
};
|
|
@@ -947,7 +1210,7 @@ export class EditorClient {
|
|
|
947
1210
|
// --- State ---
|
|
948
1211
|
|
|
949
1212
|
async getState(): Promise<EditorState> {
|
|
950
|
-
const res = await
|
|
1213
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/state`);
|
|
951
1214
|
const state = (await this.readJson(res)) as EditorState;
|
|
952
1215
|
if (!res.ok) throw new Error(`Failed to get editor state: ${res.status} ${res.statusText}`);
|
|
953
1216
|
return state;
|
|
@@ -965,7 +1228,7 @@ export class EditorClient {
|
|
|
965
1228
|
* a thrown error the caller treats as "nothing learned", never a hang.
|
|
966
1229
|
*/
|
|
967
1230
|
async getUnresolvedConsole(opts?: { all?: boolean }): Promise<unknown> {
|
|
968
|
-
const res = await
|
|
1231
|
+
const res = await this.httpFetch(
|
|
969
1232
|
`${this.baseUrl}/__editor/console${opts?.all === true ? '?all=1' : ''}`,
|
|
970
1233
|
{ signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS) },
|
|
971
1234
|
);
|
|
@@ -985,7 +1248,7 @@ export class EditorClient {
|
|
|
985
1248
|
readonly reason: string;
|
|
986
1249
|
readonly by: string;
|
|
987
1250
|
}): Promise<unknown> {
|
|
988
|
-
const res = await
|
|
1251
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/console/ack`, {
|
|
989
1252
|
method: 'POST',
|
|
990
1253
|
headers: { 'Content-Type': 'application/json' },
|
|
991
1254
|
body: JSON.stringify(input),
|
|
@@ -995,6 +1258,33 @@ export class EditorClient {
|
|
|
995
1258
|
return body;
|
|
996
1259
|
}
|
|
997
1260
|
|
|
1261
|
+
/**
|
|
1262
|
+
* `fetch` for this client's plain routes, with the ONE thing Node's `fetch`
|
|
1263
|
+
* will not do: name why it failed.
|
|
1264
|
+
*
|
|
1265
|
+
* `readJson` below already owns "the server answered the wrong thing"; this
|
|
1266
|
+
* owns "nothing answered at all", which used to reach the caller as the bare
|
|
1267
|
+
* `TypeError: fetch failed` with the real code buried on `.cause`. No retry
|
|
1268
|
+
* here — these routes create projects, run tools and acknowledge console
|
|
1269
|
+
* conditions, so repeating one is the caller's decision. The relayed
|
|
1270
|
+
* `command` path above has its own opt-in retry for the read-only captures.
|
|
1271
|
+
*/
|
|
1272
|
+
private async httpFetch(url: string, init?: RequestInit): Promise<Response> {
|
|
1273
|
+
try {
|
|
1274
|
+
return await fetch(url, init);
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
if ((error as { name?: string } | null)?.name === 'TimeoutError') throw error;
|
|
1277
|
+
const { code, detail } = describeFetchFailure(error);
|
|
1278
|
+
throw new EditorCommandError(
|
|
1279
|
+
`${init?.method ?? 'GET'} ${url} never reached the editor: ${code}` +
|
|
1280
|
+
`${detail ? ` (${detail})` : ''}. Nothing answered on that port — check the terminal ` +
|
|
1281
|
+
'running `vgai edit`, and `vgai sessions` for the port this client resolved.',
|
|
1282
|
+
code,
|
|
1283
|
+
false,
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
998
1288
|
/** Parse one JSON body and hand it to {@link onEnvelope}.
|
|
999
1289
|
*
|
|
1000
1290
|
* A command/state envelope carries current counts but not the named set. If
|
|
@@ -1033,7 +1323,7 @@ export class EditorClient {
|
|
|
1033
1323
|
'unresolvedConsole' in body
|
|
1034
1324
|
) {
|
|
1035
1325
|
try {
|
|
1036
|
-
const consoleRes = await
|
|
1326
|
+
const consoleRes = await this.httpFetch(`${this.baseUrl}/__editor/console`, {
|
|
1037
1327
|
signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS),
|
|
1038
1328
|
});
|
|
1039
1329
|
if (consoleRes.ok) {
|