@vgai/live 0.5.23 → 0.5.24
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/dist/.tsbuildinfo +1 -1
- package/dist/vgai-live/src/game-client/bridge-transport.d.ts +9 -0
- package/dist/vgai-live/src/game-client/client.d.ts +32 -0
- package/dist/vgai-live/src/game-client/client.js +74 -1
- package/dist/vgai-live/src/game-client/relay-transport.d.ts +2 -0
- package/dist/vgai-live/src/game-client/relay-transport.js +17 -0
- package/package.json +3 -3
- package/src/game-client/bridge-transport.ts +13 -0
- package/src/game-client/client.ts +92 -1
- package/src/game-client/relay-transport.ts +25 -0
|
@@ -76,6 +76,15 @@ export interface BridgeTransport {
|
|
|
76
76
|
* serialized — inline every value the step needs.
|
|
77
77
|
*/
|
|
78
78
|
runPageScript(src: string, step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
|
|
79
|
+
/**
|
|
80
|
+
* THE MODULE LANE — run a step INSIDE the editor page against
|
|
81
|
+
* `{ page, modules, instanceId }`, where `modules(path)` imports the
|
|
82
|
+
* RUNNING mount's own instance of a project module (never a phantom
|
|
83
|
+
* second copy). Same serialization contract as `runPageScript`: the
|
|
84
|
+
* step's source travels as text, closures do not survive, and the return
|
|
85
|
+
* value must be plain data. `instance` scopes multi-instance sessions.
|
|
86
|
+
*/
|
|
87
|
+
runGameScript(src: string, step: (scope: unknown) => unknown, instance?: string): Promise<BridgeCallOutcome>;
|
|
79
88
|
/**
|
|
80
89
|
* P20 — reload the document showing the game, resolving only once the page
|
|
81
90
|
* is BACK and taking commands again.
|
|
@@ -39,6 +39,13 @@ export declare class PageTransport implements BridgeTransport {
|
|
|
39
39
|
* full honesty-boundary contract; `src` is unused on this leg, kept only
|
|
40
40
|
* to satisfy the shared interface). */
|
|
41
41
|
runPageScript(_src: string, step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
|
|
42
|
+
/** THE MODULE LANE under a real Playwright host: the step's source is
|
|
43
|
+
* evaluated INSIDE the editor page via the same in-page handler the relay
|
|
44
|
+
* op uses (`window.__vgaiGameEval`), because `modules()` only means
|
|
45
|
+
* anything in the page's own module space — a Node-side call could never
|
|
46
|
+
* hand back the running mount's instances. Same serialization contract
|
|
47
|
+
* as the relay leg. */
|
|
48
|
+
runGameScript(src: string, _step: (scope: unknown) => unknown, instance?: string): Promise<BridgeCallOutcome>;
|
|
42
49
|
/** Playwright's own reload already waits for the new document's `load`
|
|
43
50
|
* event, which is exactly the completion signal this method's contract
|
|
44
51
|
* asks for — nothing to reconstruct on this leg. */
|
|
@@ -296,6 +303,31 @@ export declare class GameClient {
|
|
|
296
303
|
* `runPageScript` doc comment for the full contract this method wraps.
|
|
297
304
|
*/
|
|
298
305
|
page<T = unknown>(step: (page: Page) => T | Promise<T>): Promise<T>;
|
|
306
|
+
/**
|
|
307
|
+
* THE MODULE LANE — run literal JS INSIDE the game's page, with the
|
|
308
|
+
* running mount's modules in reach:
|
|
309
|
+
*
|
|
310
|
+
* ```js
|
|
311
|
+
* await game.run(async ({ modules }) => {
|
|
312
|
+
* const { simHost } = await modules('src/sim/host.ts');
|
|
313
|
+
* return simHost().state.day;
|
|
314
|
+
* })
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* `scope` is `{ page, modules, instanceId }`. Serialization contract as
|
|
318
|
+
* `game.page()`: the step travels as source (no closures), and the return
|
|
319
|
+
* value must be plain data. `modules(path)` resolves through the ACTIVE
|
|
320
|
+
* mount's own url space, so what you touch IS the running game — never a
|
|
321
|
+
* phantom second copy. Dev-server sessions only; a shipped build's curated
|
|
322
|
+
* surface is its adapter exports.
|
|
323
|
+
*/
|
|
324
|
+
run<T = unknown>(step: (scope: {
|
|
325
|
+
page: Page;
|
|
326
|
+
modules: (path: string) => Promise<Record<string, unknown>>;
|
|
327
|
+
instanceId: string;
|
|
328
|
+
}) => T | Promise<T>, opts?: {
|
|
329
|
+
instance?: string;
|
|
330
|
+
}): Promise<T>;
|
|
299
331
|
/**
|
|
300
332
|
* Reload the document showing the game, resolving only once it is back and
|
|
301
333
|
* answering commands (see `bridge-transport.ts`'s `reloadPage`).
|
|
@@ -128,6 +128,30 @@ export class PageTransport {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
/** THE MODULE LANE under a real Playwright host: the step's source is
|
|
132
|
+
* evaluated INSIDE the editor page via the same in-page handler the relay
|
|
133
|
+
* op uses (`window.__vgaiGameEval`), because `modules()` only means
|
|
134
|
+
* anything in the page's own module space — a Node-side call could never
|
|
135
|
+
* hand back the running mount's instances. Same serialization contract
|
|
136
|
+
* as the relay leg. */
|
|
137
|
+
async runGameScript(src, _step, instance) {
|
|
138
|
+
try {
|
|
139
|
+
const result = await this.page.evaluate(async (args) => {
|
|
140
|
+
const hook = window['__vgaiGameEval'];
|
|
141
|
+
if (typeof hook !== 'function') {
|
|
142
|
+
throw new Error('game-eval: this page has no __vgaiGameEval hook — is the editor page loaded?');
|
|
143
|
+
}
|
|
144
|
+
return hook(args.src, args.instance);
|
|
145
|
+
}, { src, ...(instance === undefined ? {} : { instance }) });
|
|
146
|
+
return { ok: true, result };
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
error: { code: undefined, message: err instanceof Error ? err.message : String(err) },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
131
155
|
/** Playwright's own reload already waits for the new document's `load`
|
|
132
156
|
* event, which is exactly the completion signal this method's contract
|
|
133
157
|
* asks for — nothing to reconstruct on this leg. */
|
|
@@ -422,6 +446,9 @@ export class GameClient {
|
|
|
422
446
|
/** Per-test tick-rate samples, fed by every `snapshot()` read (a poll the
|
|
423
447
|
* client was making anyway — zero extra page.evaluate round trips). */
|
|
424
448
|
#tps = new TpsAccumulator();
|
|
449
|
+
/** True after the settled run-ticks door proved absent on this page (an older exported
|
|
450
|
+
* game's engine) — see `fastForward`'s `runTicksBatch`. */
|
|
451
|
+
#legacyRunTicksDoor = false;
|
|
425
452
|
/** Hidden-tab recovery (hollowstone field lesson: the engine hard-stops
|
|
426
453
|
* while `document.hidden`). Client-lifetime state so `bringToFront()`
|
|
427
454
|
* fires at most once per test, across ALL waitFor/waitSimTime loops. */
|
|
@@ -504,7 +531,30 @@ export class GameClient {
|
|
|
504
531
|
// names neither the method nor the shape).
|
|
505
532
|
assertValidWaitForBudget(budget, 'fastForward');
|
|
506
533
|
const clock = {
|
|
507
|
-
|
|
534
|
+
// The SETTLED door (`runTicksSettled`, an async bridge method): ticks never race a scene
|
|
535
|
+
// remount's async commit, so which tick first runs a freshly reloaded world is
|
|
536
|
+
// deterministic (see engine/runtime/run-ticks-settled.ts — measured: without it, one
|
|
537
|
+
// drive script produced 7 or 8 post-respawn walked ticks depending on wall timing).
|
|
538
|
+
// Falls back ONCE to the plain sync door for a page whose engine predates the method
|
|
539
|
+
// (an older exported game), and remembers the verdict for the rest of the burst.
|
|
540
|
+
runTicksBatch: async (n, render) => {
|
|
541
|
+
if (this.#legacyRunTicksDoor) {
|
|
542
|
+
await this.callBridgeVoid('runTicks', n, { render });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
await this.callBridgeAsync('runTicksSettled', n, { render });
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
const code = error.code;
|
|
550
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
551
|
+
const doorAbsent = code === 'UNKNOWN_BRIDGE_METHOD' || /is not a function|undefined/i.test(message);
|
|
552
|
+
if (!doorAbsent)
|
|
553
|
+
throw error;
|
|
554
|
+
this.#legacyRunTicksDoor = true;
|
|
555
|
+
await this.callBridgeVoid('runTicks', n, { render });
|
|
556
|
+
}
|
|
557
|
+
},
|
|
508
558
|
readTime: async () => {
|
|
509
559
|
// Raw bridge read — deliberately NOT `this.snapshot()`, which would
|
|
510
560
|
// feed the TpsAccumulator (see fast-forward.ts's module doc, point 2).
|
|
@@ -676,6 +726,29 @@ export class GameClient {
|
|
|
676
726
|
const outcome = await this.#transport.runPageScript(step.toString(), erased);
|
|
677
727
|
return this.unwrap(outcome);
|
|
678
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* THE MODULE LANE — run literal JS INSIDE the game's page, with the
|
|
731
|
+
* running mount's modules in reach:
|
|
732
|
+
*
|
|
733
|
+
* ```js
|
|
734
|
+
* await game.run(async ({ modules }) => {
|
|
735
|
+
* const { simHost } = await modules('src/sim/host.ts');
|
|
736
|
+
* return simHost().state.day;
|
|
737
|
+
* })
|
|
738
|
+
* ```
|
|
739
|
+
*
|
|
740
|
+
* `scope` is `{ page, modules, instanceId }`. Serialization contract as
|
|
741
|
+
* `game.page()`: the step travels as source (no closures), and the return
|
|
742
|
+
* value must be plain data. `modules(path)` resolves through the ACTIVE
|
|
743
|
+
* mount's own url space, so what you touch IS the running game — never a
|
|
744
|
+
* phantom second copy. Dev-server sessions only; a shipped build's curated
|
|
745
|
+
* surface is its adapter exports.
|
|
746
|
+
*/
|
|
747
|
+
async run(step, opts) {
|
|
748
|
+
const erased = (arg) => step(arg);
|
|
749
|
+
const outcome = await this.#transport.runGameScript(step.toString(), erased, opts?.instance);
|
|
750
|
+
return this.unwrap(outcome);
|
|
751
|
+
}
|
|
679
752
|
/**
|
|
680
753
|
* Reload the document showing the game, resolving only once it is back and
|
|
681
754
|
* answering commands (see `bridge-transport.ts`'s `reloadPage`).
|
|
@@ -109,6 +109,8 @@ export declare class RelayTransport implements BridgeTransport {
|
|
|
109
109
|
* `PageTransport` (which DOES call it directly) also implements.
|
|
110
110
|
*/
|
|
111
111
|
runPageScript(src: string, _step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
|
|
112
|
+
/** THE MODULE LANE over the relay — same wire shape as `page-script`. */
|
|
113
|
+
runGameScript(src: string, _step: (scope: unknown) => unknown, instance?: string): Promise<BridgeCallOutcome>;
|
|
112
114
|
/**
|
|
113
115
|
* P20 — order the tab to reload, then wait for EVIDENCE that it came back.
|
|
114
116
|
*
|
|
@@ -270,6 +270,23 @@ export class RelayTransport {
|
|
|
270
270
|
};
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
|
+
/** THE MODULE LANE over the relay — same wire shape as `page-script`. */
|
|
274
|
+
async runGameScript(src, _step, instance) {
|
|
275
|
+
try {
|
|
276
|
+
const body = await this.postCommand({ type: 'game-eval', src, ...(instance === undefined ? {} : { instance }) }, PAGE_SCRIPT_TIMEOUT_MS);
|
|
277
|
+
return this.toBridgeOutcome(body);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
error: {
|
|
283
|
+
code: 'RELAY_UNREACHABLE',
|
|
284
|
+
message: `vgai: could not reach the editor dev server relay at ${this.baseUrl} — ` +
|
|
285
|
+
`${err instanceof Error ? err.message : String(err)}`,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
273
290
|
/**
|
|
274
291
|
* P20 — order the tab to reload, then wait for EVIDENCE that it came back.
|
|
275
292
|
*
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/live",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.24",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"prepack": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@vgai/editor-sdk": "0.5.
|
|
36
|
-
"@vgai/sdk": "0.5.
|
|
35
|
+
"@vgai/editor-sdk": "0.5.24",
|
|
36
|
+
"@vgai/sdk": "0.5.24"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@playwright/test": ">=1.58.2 <2"
|
|
@@ -75,6 +75,19 @@ export interface BridgeTransport {
|
|
|
75
75
|
* serialized — inline every value the step needs.
|
|
76
76
|
*/
|
|
77
77
|
runPageScript(src: string, step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
|
|
78
|
+
/**
|
|
79
|
+
* THE MODULE LANE — run a step INSIDE the editor page against
|
|
80
|
+
* `{ page, modules, instanceId }`, where `modules(path)` imports the
|
|
81
|
+
* RUNNING mount's own instance of a project module (never a phantom
|
|
82
|
+
* second copy). Same serialization contract as `runPageScript`: the
|
|
83
|
+
* step's source travels as text, closures do not survive, and the return
|
|
84
|
+
* value must be plain data. `instance` scopes multi-instance sessions.
|
|
85
|
+
*/
|
|
86
|
+
runGameScript(
|
|
87
|
+
src: string,
|
|
88
|
+
step: (scope: unknown) => unknown,
|
|
89
|
+
instance?: string,
|
|
90
|
+
): Promise<BridgeCallOutcome>;
|
|
78
91
|
/**
|
|
79
92
|
* P20 — reload the document showing the game, resolving only once the page
|
|
80
93
|
* is BACK and taking commands again.
|
|
@@ -158,6 +158,41 @@ export class PageTransport implements BridgeTransport {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/** THE MODULE LANE under a real Playwright host: the step's source is
|
|
162
|
+
* evaluated INSIDE the editor page via the same in-page handler the relay
|
|
163
|
+
* op uses (`window.__vgaiGameEval`), because `modules()` only means
|
|
164
|
+
* anything in the page's own module space — a Node-side call could never
|
|
165
|
+
* hand back the running mount's instances. Same serialization contract
|
|
166
|
+
* as the relay leg. */
|
|
167
|
+
async runGameScript(
|
|
168
|
+
src: string,
|
|
169
|
+
_step: (scope: unknown) => unknown,
|
|
170
|
+
instance?: string,
|
|
171
|
+
): Promise<BridgeCallOutcome> {
|
|
172
|
+
try {
|
|
173
|
+
const result = await this.page.evaluate(
|
|
174
|
+
async (args: { src: string; instance?: string }) => {
|
|
175
|
+
const hook = (window as unknown as Record<string, unknown>)['__vgaiGameEval'] as
|
|
176
|
+
| ((src: string, instance?: string) => Promise<unknown>)
|
|
177
|
+
| undefined;
|
|
178
|
+
if (typeof hook !== 'function') {
|
|
179
|
+
throw new Error(
|
|
180
|
+
'game-eval: this page has no __vgaiGameEval hook — is the editor page loaded?',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return hook(args.src, args.instance);
|
|
184
|
+
},
|
|
185
|
+
{ src, ...(instance === undefined ? {} : { instance }) },
|
|
186
|
+
);
|
|
187
|
+
return { ok: true, result };
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
error: { code: undefined, message: err instanceof Error ? err.message : String(err) },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
161
196
|
/** Playwright's own reload already waits for the new document's `load`
|
|
162
197
|
* event, which is exactly the completion signal this method's contract
|
|
163
198
|
* asks for — nothing to reconstruct on this leg. */
|
|
@@ -528,6 +563,9 @@ export class GameClient {
|
|
|
528
563
|
/** Per-test tick-rate samples, fed by every `snapshot()` read (a poll the
|
|
529
564
|
* client was making anyway — zero extra page.evaluate round trips). */
|
|
530
565
|
readonly #tps = new TpsAccumulator();
|
|
566
|
+
/** True after the settled run-ticks door proved absent on this page (an older exported
|
|
567
|
+
* game's engine) — see `fastForward`'s `runTicksBatch`. */
|
|
568
|
+
#legacyRunTicksDoor = false;
|
|
531
569
|
/** Hidden-tab recovery (hollowstone field lesson: the engine hard-stops
|
|
532
570
|
* while `document.hidden`). Client-lifetime state so `bringToFront()`
|
|
533
571
|
* fires at most once per test, across ALL waitFor/waitSimTime loops. */
|
|
@@ -619,7 +657,29 @@ export class GameClient {
|
|
|
619
657
|
// names neither the method nor the shape).
|
|
620
658
|
assertValidWaitForBudget(budget, 'fastForward');
|
|
621
659
|
const clock: FastForwardClock = {
|
|
622
|
-
|
|
660
|
+
// The SETTLED door (`runTicksSettled`, an async bridge method): ticks never race a scene
|
|
661
|
+
// remount's async commit, so which tick first runs a freshly reloaded world is
|
|
662
|
+
// deterministic (see engine/runtime/run-ticks-settled.ts — measured: without it, one
|
|
663
|
+
// drive script produced 7 or 8 post-respawn walked ticks depending on wall timing).
|
|
664
|
+
// Falls back ONCE to the plain sync door for a page whose engine predates the method
|
|
665
|
+
// (an older exported game), and remembers the verdict for the rest of the burst.
|
|
666
|
+
runTicksBatch: async (n, render) => {
|
|
667
|
+
if (this.#legacyRunTicksDoor) {
|
|
668
|
+
await this.callBridgeVoid('runTicks', n, { render });
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
await this.callBridgeAsync<void>('runTicksSettled', n, { render });
|
|
673
|
+
} catch (error) {
|
|
674
|
+
const code = (error as { code?: string }).code;
|
|
675
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
676
|
+
const doorAbsent =
|
|
677
|
+
code === 'UNKNOWN_BRIDGE_METHOD' || /is not a function|undefined/i.test(message);
|
|
678
|
+
if (!doorAbsent) throw error;
|
|
679
|
+
this.#legacyRunTicksDoor = true;
|
|
680
|
+
await this.callBridgeVoid('runTicks', n, { render });
|
|
681
|
+
}
|
|
682
|
+
},
|
|
623
683
|
readTime: async () => {
|
|
624
684
|
// Raw bridge read — deliberately NOT `this.snapshot()`, which would
|
|
625
685
|
// feed the TpsAccumulator (see fast-forward.ts's module doc, point 2).
|
|
@@ -802,6 +862,37 @@ export class GameClient {
|
|
|
802
862
|
return this.unwrap<T>(outcome);
|
|
803
863
|
}
|
|
804
864
|
|
|
865
|
+
/**
|
|
866
|
+
* THE MODULE LANE — run literal JS INSIDE the game's page, with the
|
|
867
|
+
* running mount's modules in reach:
|
|
868
|
+
*
|
|
869
|
+
* ```js
|
|
870
|
+
* await game.run(async ({ modules }) => {
|
|
871
|
+
* const { simHost } = await modules('src/sim/host.ts');
|
|
872
|
+
* return simHost().state.day;
|
|
873
|
+
* })
|
|
874
|
+
* ```
|
|
875
|
+
*
|
|
876
|
+
* `scope` is `{ page, modules, instanceId }`. Serialization contract as
|
|
877
|
+
* `game.page()`: the step travels as source (no closures), and the return
|
|
878
|
+
* value must be plain data. `modules(path)` resolves through the ACTIVE
|
|
879
|
+
* mount's own url space, so what you touch IS the running game — never a
|
|
880
|
+
* phantom second copy. Dev-server sessions only; a shipped build's curated
|
|
881
|
+
* surface is its adapter exports.
|
|
882
|
+
*/
|
|
883
|
+
async run<T = unknown>(
|
|
884
|
+
step: (scope: {
|
|
885
|
+
page: Page;
|
|
886
|
+
modules: (path: string) => Promise<Record<string, unknown>>;
|
|
887
|
+
instanceId: string;
|
|
888
|
+
}) => T | Promise<T>,
|
|
889
|
+
opts?: { instance?: string },
|
|
890
|
+
): Promise<T> {
|
|
891
|
+
const erased = (arg: unknown) => step(arg as Parameters<typeof step>[0]);
|
|
892
|
+
const outcome = await this.#transport.runGameScript(step.toString(), erased, opts?.instance);
|
|
893
|
+
return this.unwrap<T>(outcome);
|
|
894
|
+
}
|
|
895
|
+
|
|
805
896
|
/**
|
|
806
897
|
* Reload the document showing the game, resolving only once it is back and
|
|
807
898
|
* answering commands (see `bridge-transport.ts`'s `reloadPage`).
|
|
@@ -345,6 +345,31 @@ export class RelayTransport implements BridgeTransport {
|
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
/** THE MODULE LANE over the relay — same wire shape as `page-script`. */
|
|
349
|
+
async runGameScript(
|
|
350
|
+
src: string,
|
|
351
|
+
_step: (scope: unknown) => unknown,
|
|
352
|
+
instance?: string,
|
|
353
|
+
): Promise<BridgeCallOutcome> {
|
|
354
|
+
try {
|
|
355
|
+
const body = await this.postCommand(
|
|
356
|
+
{ type: 'game-eval', src, ...(instance === undefined ? {} : { instance }) },
|
|
357
|
+
PAGE_SCRIPT_TIMEOUT_MS,
|
|
358
|
+
);
|
|
359
|
+
return this.toBridgeOutcome(body);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
error: {
|
|
364
|
+
code: 'RELAY_UNREACHABLE',
|
|
365
|
+
message:
|
|
366
|
+
`vgai: could not reach the editor dev server relay at ${this.baseUrl} — ` +
|
|
367
|
+
`${err instanceof Error ? err.message : String(err)}`,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
348
373
|
/**
|
|
349
374
|
* P20 — order the tab to reload, then wait for EVIDENCE that it came back.
|
|
350
375
|
*
|