@vgai/editor-sdk 0.5.42 → 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 +324 -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 +14 -1
- package/src/types.ts +221 -11
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,8 +10,13 @@ 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,
|
|
@@ -33,6 +38,9 @@ import type {
|
|
|
33
38
|
ShadingMode,
|
|
34
39
|
StoryCaptureOptions,
|
|
35
40
|
StoryVariantCapture,
|
|
41
|
+
StructureOp,
|
|
42
|
+
StructureOpOptions,
|
|
43
|
+
StructureOpResult,
|
|
36
44
|
TransformMode,
|
|
37
45
|
TransformSpace,
|
|
38
46
|
Vec3Value,
|
|
@@ -115,6 +123,59 @@ const COMMAND_DEADLINE_MS = 150_000;
|
|
|
115
123
|
*/
|
|
116
124
|
const CONSOLE_DRAIN_TIMEOUT_MS = 1_500;
|
|
117
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
|
+
|
|
118
179
|
/** Context accompanying one {@link EditorClient} response observation. */
|
|
119
180
|
export interface EditorEnvelopeObservation {
|
|
120
181
|
/** True only when this body came from the console-ledger endpoint and its
|
|
@@ -206,6 +267,9 @@ export class EditorClient {
|
|
|
206
267
|
* The observer must not throw; anything it raises is swallowed, because a
|
|
207
268
|
* reporting hook may never break the command it is reporting on.
|
|
208
269
|
*/
|
|
270
|
+
private readonly transport:
|
|
271
|
+
| ((body: Record<string, unknown>) => Promise<Record<string, unknown>>)
|
|
272
|
+
| null;
|
|
209
273
|
private readonly onEnvelope:
|
|
210
274
|
| ((body: unknown, observation: EditorEnvelopeObservation) => void)
|
|
211
275
|
| null;
|
|
@@ -213,6 +277,14 @@ export class EditorClient {
|
|
|
213
277
|
constructor(opts?: {
|
|
214
278
|
url?: string;
|
|
215
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>>;
|
|
216
288
|
}) {
|
|
217
289
|
if (opts !== undefined && (typeof opts !== 'object' || opts === null || Array.isArray(opts))) {
|
|
218
290
|
throw new TypeError(
|
|
@@ -220,7 +292,7 @@ export class EditorClient {
|
|
|
220
292
|
);
|
|
221
293
|
}
|
|
222
294
|
const unknownOptions = Object.keys(opts ?? {}).filter(
|
|
223
|
-
(key) => key !== 'url' && key !== 'onEnvelope',
|
|
295
|
+
(key) => key !== 'url' && key !== 'onEnvelope' && key !== 'transport',
|
|
224
296
|
);
|
|
225
297
|
if (unknownOptions.length > 0) {
|
|
226
298
|
throw new Error(
|
|
@@ -229,6 +301,7 @@ export class EditorClient {
|
|
|
229
301
|
}
|
|
230
302
|
this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, '');
|
|
231
303
|
this.onEnvelope = opts?.onEnvelope ?? null;
|
|
304
|
+
this.transport = opts?.transport ?? null;
|
|
232
305
|
this.game = {
|
|
233
306
|
state: async (name: string): Promise<unknown> => {
|
|
234
307
|
// `keys` narrows the relay to the one provider asked for, so a panel
|
|
@@ -246,28 +319,82 @@ export class EditorClient {
|
|
|
246
319
|
};
|
|
247
320
|
}
|
|
248
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
|
+
*/
|
|
249
330
|
private async command<T extends object = Record<string, never>>(
|
|
250
331
|
body: Record<string, unknown>,
|
|
332
|
+
options?: { retryTransport?: boolean },
|
|
251
333
|
): Promise<T> {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
+
}
|
|
271
398
|
}
|
|
272
399
|
const data = (await this.readJson(res)) as { ok: boolean; error?: string; code?: string } & T;
|
|
273
400
|
if (!data.ok) {
|
|
@@ -376,6 +503,40 @@ export class EditorClient {
|
|
|
376
503
|
await this.command({ type: 'view-preset', preset });
|
|
377
504
|
}
|
|
378
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
|
+
|
|
379
540
|
async setCamera(position: Vec3Value, target: Vec3Value, fov?: number): Promise<void> {
|
|
380
541
|
await this.command({
|
|
381
542
|
type: 'set-camera',
|
|
@@ -438,6 +599,11 @@ export class EditorClient {
|
|
|
438
599
|
// re-derives (or softens) it.
|
|
439
600
|
...(flatness && typeof flatness.dominantFraction === 'number' ? { flatness } : {}),
|
|
440
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
|
+
: {}),
|
|
441
607
|
};
|
|
442
608
|
}
|
|
443
609
|
|
|
@@ -476,14 +642,22 @@ export class EditorClient {
|
|
|
476
642
|
source: AssetPreviewSource,
|
|
477
643
|
options: AssetPreviewOptions = {},
|
|
478
644
|
): Promise<AssetPreviewCapture> {
|
|
479
|
-
const data = await this.command<AssetPreviewCapture>(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
);
|
|
484
656
|
return {
|
|
485
657
|
width: data.width,
|
|
486
658
|
height: data.height,
|
|
659
|
+
// Absent from editors that predate orientation reporting.
|
|
660
|
+
...(data.orientation ? { orientation: data.orientation } : {}),
|
|
487
661
|
views: data.views,
|
|
488
662
|
contactSheet: data.contactSheet,
|
|
489
663
|
};
|
|
@@ -504,12 +678,15 @@ export class EditorClient {
|
|
|
504
678
|
definition: AssetPreviewShotSetDefinition,
|
|
505
679
|
options: AssetPreviewOptions = {},
|
|
506
680
|
): Promise<LabeledShotSetCapture> {
|
|
507
|
-
const data = await this.command<LabeledShotSetCapture>(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
+
);
|
|
513
690
|
return {
|
|
514
691
|
width: data.width,
|
|
515
692
|
height: data.height,
|
|
@@ -598,6 +775,13 @@ export class EditorClient {
|
|
|
598
775
|
await this.command({ type: 'open-asset-tab', path, kind });
|
|
599
776
|
}
|
|
600
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
|
+
|
|
601
785
|
/** Generate a project-owned native Rapier ragdoll from a rigged model asset. */
|
|
602
786
|
async generateRagdoll(assetPath: string): Promise<RagdollGenerationResult> {
|
|
603
787
|
return this.command<RagdollGenerationResult>({ type: 'generate-ragdoll', assetPath });
|
|
@@ -615,6 +799,14 @@ export class EditorClient {
|
|
|
615
799
|
await this.command({ type: 'toggle-console' });
|
|
616
800
|
}
|
|
617
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
|
+
|
|
618
810
|
async showBuild(): Promise<void> {
|
|
619
811
|
await this.command({ type: 'show-build' });
|
|
620
812
|
}
|
|
@@ -655,6 +847,26 @@ export class EditorClient {
|
|
|
655
847
|
return data.subject;
|
|
656
848
|
}
|
|
657
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
|
+
|
|
658
870
|
/**
|
|
659
871
|
* The HIERARCHY PANEL's actual rendered row tree, as data.
|
|
660
872
|
*
|
|
@@ -773,11 +985,33 @@ export class EditorClient {
|
|
|
773
985
|
return data.view;
|
|
774
986
|
}
|
|
775
987
|
|
|
776
|
-
/**
|
|
777
|
-
|
|
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> {
|
|
778
1008
|
return this.command<ActiveDocumentCapture>({
|
|
779
1009
|
type: 'capture-active-document',
|
|
780
|
-
...(
|
|
1010
|
+
...(view ? { view } : {}),
|
|
1011
|
+
...(typeof size === 'number' ? { size } : {}),
|
|
1012
|
+
...(typeof size === 'object' && size !== null
|
|
1013
|
+
? { width: size.width, height: size.height }
|
|
1014
|
+
: {}),
|
|
781
1015
|
});
|
|
782
1016
|
}
|
|
783
1017
|
|
|
@@ -793,6 +1027,17 @@ export class EditorClient {
|
|
|
793
1027
|
return this.command<DocumentProbeResult>({ type: 'document-probe', step });
|
|
794
1028
|
}
|
|
795
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
|
+
|
|
796
1041
|
// --- Display (set semantics) ---
|
|
797
1042
|
|
|
798
1043
|
async setGrid(enabled: boolean): Promise<void> {
|
|
@@ -842,7 +1087,7 @@ export class EditorClient {
|
|
|
842
1087
|
template: ProjectTemplate = 'default',
|
|
843
1088
|
exampleId?: string,
|
|
844
1089
|
): Promise<ProjectInfo> {
|
|
845
|
-
const res = await
|
|
1090
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/create-project`, {
|
|
846
1091
|
method: 'POST',
|
|
847
1092
|
headers: { 'Content-Type': 'application/json' },
|
|
848
1093
|
body: JSON.stringify({ name, location, template, ...(exampleId ? { exampleId } : {}) }),
|
|
@@ -860,7 +1105,7 @@ export class EditorClient {
|
|
|
860
1105
|
}
|
|
861
1106
|
|
|
862
1107
|
async openProject(path: string): Promise<void> {
|
|
863
|
-
const res = await
|
|
1108
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/open-project`, {
|
|
864
1109
|
method: 'POST',
|
|
865
1110
|
headers: { 'Content-Type': 'application/json' },
|
|
866
1111
|
body: JSON.stringify({ path }),
|
|
@@ -872,14 +1117,14 @@ export class EditorClient {
|
|
|
872
1117
|
}
|
|
873
1118
|
|
|
874
1119
|
async getProject(): Promise<ProjectInfo | null> {
|
|
875
|
-
const res = await
|
|
1120
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/project`);
|
|
876
1121
|
const data = (await this.readJson(res)) as { project: ProjectInfo | null; error?: string };
|
|
877
1122
|
if (!res.ok) throw new Error(data.error ?? `Failed to get project: ${res.status}`);
|
|
878
1123
|
return data.project;
|
|
879
1124
|
}
|
|
880
1125
|
|
|
881
1126
|
async listRecentProjects(): Promise<RecentProject[]> {
|
|
882
|
-
const res = await
|
|
1127
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/recent-projects`);
|
|
883
1128
|
const data = (await this.readJson(res)) as { projects: RecentProject[]; error?: string };
|
|
884
1129
|
if (!res.ok) throw new Error(data.error ?? `Failed to list projects: ${res.status}`);
|
|
885
1130
|
return data.projects;
|
|
@@ -891,7 +1136,7 @@ export class EditorClient {
|
|
|
891
1136
|
* The editor server loads callable metadata in Node; modules never enter the
|
|
892
1137
|
* editor browser merely because they were listed. */
|
|
893
1138
|
async listProjectTools(): Promise<ProjectToolCatalog> {
|
|
894
|
-
const res = await
|
|
1139
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/project-tools`);
|
|
895
1140
|
const body = await this.readJson(res);
|
|
896
1141
|
if (!res.ok) throw new Error(`Failed to list project tools: ${res.status}`);
|
|
897
1142
|
return body as ProjectToolCatalog;
|
|
@@ -904,7 +1149,7 @@ export class EditorClient {
|
|
|
904
1149
|
input: unknown = {},
|
|
905
1150
|
options: { confirm?: boolean; instance?: string } = {},
|
|
906
1151
|
): Promise<ProjectToolOutcome> {
|
|
907
|
-
const res = await
|
|
1152
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/project-tools/run`, {
|
|
908
1153
|
method: 'POST',
|
|
909
1154
|
headers: { 'Content-Type': 'application/json' },
|
|
910
1155
|
body: JSON.stringify({
|
|
@@ -929,7 +1174,7 @@ export class EditorClient {
|
|
|
929
1174
|
/** Read the one project-local generation job ledger. Provider-native
|
|
930
1175
|
* request/result shapes remain on their registered operations. */
|
|
931
1176
|
async listGenerationJobs(): Promise<GenerationJobsDocument> {
|
|
932
|
-
const res = await
|
|
1177
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/generations`);
|
|
933
1178
|
const body = await this.readJson(res);
|
|
934
1179
|
if (!res.ok) throw new Error(`Failed to list generation jobs: ${res.status}`);
|
|
935
1180
|
return body as GenerationJobsDocument;
|
|
@@ -938,9 +1183,12 @@ export class EditorClient {
|
|
|
938
1183
|
/** Forget operational job state. Accepted provenance and project assets
|
|
939
1184
|
* are deliberately unaffected. */
|
|
940
1185
|
async forgetGenerationJob(id: string): Promise<boolean> {
|
|
941
|
-
const res = await
|
|
942
|
-
|
|
943
|
-
|
|
1186
|
+
const res = await this.httpFetch(
|
|
1187
|
+
`${this.baseUrl}/__editor/generations/${encodeURIComponent(id)}`,
|
|
1188
|
+
{
|
|
1189
|
+
method: 'DELETE',
|
|
1190
|
+
},
|
|
1191
|
+
);
|
|
944
1192
|
const body = (await this.readJson(res)) as { removed?: boolean; error?: string };
|
|
945
1193
|
if (!res.ok) throw new Error(body.error ?? `Failed to forget generation job: ${res.status}`);
|
|
946
1194
|
return body.removed === true;
|
|
@@ -951,7 +1199,7 @@ export class EditorClient {
|
|
|
951
1199
|
async getLogEntries(): Promise<
|
|
952
1200
|
Array<{ t: number; level: string; msg: string; source?: string }>
|
|
953
1201
|
> {
|
|
954
|
-
const res = await
|
|
1202
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/log-entries`);
|
|
955
1203
|
const data = (await this.readJson(res)) as {
|
|
956
1204
|
entries: Array<{ t: number; level: string; msg: string; source?: string }>;
|
|
957
1205
|
};
|
|
@@ -962,7 +1210,7 @@ export class EditorClient {
|
|
|
962
1210
|
// --- State ---
|
|
963
1211
|
|
|
964
1212
|
async getState(): Promise<EditorState> {
|
|
965
|
-
const res = await
|
|
1213
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/state`);
|
|
966
1214
|
const state = (await this.readJson(res)) as EditorState;
|
|
967
1215
|
if (!res.ok) throw new Error(`Failed to get editor state: ${res.status} ${res.statusText}`);
|
|
968
1216
|
return state;
|
|
@@ -980,7 +1228,7 @@ export class EditorClient {
|
|
|
980
1228
|
* a thrown error the caller treats as "nothing learned", never a hang.
|
|
981
1229
|
*/
|
|
982
1230
|
async getUnresolvedConsole(opts?: { all?: boolean }): Promise<unknown> {
|
|
983
|
-
const res = await
|
|
1231
|
+
const res = await this.httpFetch(
|
|
984
1232
|
`${this.baseUrl}/__editor/console${opts?.all === true ? '?all=1' : ''}`,
|
|
985
1233
|
{ signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS) },
|
|
986
1234
|
);
|
|
@@ -1000,7 +1248,7 @@ export class EditorClient {
|
|
|
1000
1248
|
readonly reason: string;
|
|
1001
1249
|
readonly by: string;
|
|
1002
1250
|
}): Promise<unknown> {
|
|
1003
|
-
const res = await
|
|
1251
|
+
const res = await this.httpFetch(`${this.baseUrl}/__editor/console/ack`, {
|
|
1004
1252
|
method: 'POST',
|
|
1005
1253
|
headers: { 'Content-Type': 'application/json' },
|
|
1006
1254
|
body: JSON.stringify(input),
|
|
@@ -1010,6 +1258,33 @@ export class EditorClient {
|
|
|
1010
1258
|
return body;
|
|
1011
1259
|
}
|
|
1012
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
|
+
|
|
1013
1288
|
/** Parse one JSON body and hand it to {@link onEnvelope}.
|
|
1014
1289
|
*
|
|
1015
1290
|
* A command/state envelope carries current counts but not the named set. If
|
|
@@ -1048,7 +1323,7 @@ export class EditorClient {
|
|
|
1048
1323
|
'unresolvedConsole' in body
|
|
1049
1324
|
) {
|
|
1050
1325
|
try {
|
|
1051
|
-
const consoleRes = await
|
|
1326
|
+
const consoleRes = await this.httpFetch(`${this.baseUrl}/__editor/console`, {
|
|
1052
1327
|
signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS),
|
|
1053
1328
|
});
|
|
1054
1329
|
if (consoleRes.ok) {
|