@vgai/live 0.4.1 → 0.5.0-canary.20260719.0
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 +4 -4
- package/src/editor.ts +22 -0
- package/src/game.ts +5 -4
- package/src/index.ts +17 -6
- package/src/session.ts +84 -6
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
|
+
"version": "0.5.0-canary.20260719.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"build": "tsc -p tsconfig.build.json"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@vgai/editor-sdk": "0.
|
|
34
|
-
"@vgai/
|
|
35
|
-
"@vgai/sdk": "0.
|
|
33
|
+
"@vgai/editor-sdk": "0.5.0-canary.20260719.0",
|
|
34
|
+
"@vgai/e2e": "0.5.0-canary.20260719.0",
|
|
35
|
+
"@vgai/sdk": "0.5.0-canary.20260719.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@playwright/test": ">=1.58.2 <2"
|
package/src/editor.ts
CHANGED
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type {
|
|
16
|
+
ActiveDocumentCapture,
|
|
16
17
|
AssetKind,
|
|
17
18
|
AssetPreviewCapture,
|
|
18
19
|
AssetPreviewOptions,
|
|
19
20
|
EditorClient,
|
|
20
21
|
EditorState,
|
|
22
|
+
EditorView,
|
|
23
|
+
PresentedEditorView,
|
|
21
24
|
ShadingMode,
|
|
22
25
|
ViewPreset,
|
|
23
26
|
ViewportCapture,
|
|
@@ -99,6 +102,25 @@ export class LiveEditor {
|
|
|
99
102
|
return state.savePath;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Make the connected human editor show the same subject/view as the agent.
|
|
107
|
+
* The returned URL is a compact, shareable projection — not a serialized
|
|
108
|
+
* workspace or document payload.
|
|
109
|
+
*/
|
|
110
|
+
async present(view: EditorView): Promise<PresentedEditorView> {
|
|
111
|
+
return this.client.present(view);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The human editor's actual active document, selection, camera and utility. */
|
|
115
|
+
async currentView(): Promise<EditorView> {
|
|
116
|
+
return this.client.currentView();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Capture the same center document the human is currently looking at. */
|
|
120
|
+
async captureActiveDocument(size?: number): Promise<ActiveDocumentCapture> {
|
|
121
|
+
return this.client.captureActiveDocument(size);
|
|
122
|
+
}
|
|
123
|
+
|
|
102
124
|
/**
|
|
103
125
|
* FILE mode — writes the scene file directly. NOT a live wire command:
|
|
104
126
|
* there is no live-editor wire op for this yet (`vgai apply-diff`'s own
|
package/src/game.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `game` — the game-control half of `@vgai/live`'s `{ editor, game, page }`.
|
|
3
|
-
* A `GameClient` (`@vgai/
|
|
3
|
+
* A `GameClient` (`@vgai/e2e`) bound to a `RelayTransport` on the resolved
|
|
4
4
|
* session's port — the SAME session-wire relay (`POST /__editor/command`,
|
|
5
5
|
* `bridge-call`/`bridge-screenshot`/`page-script`) every `EditorClient`
|
|
6
6
|
* method already uses. Never forked: this module only WIRES `GameClient` up,
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* `GameClient` exposes is exposed here, unchanged).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { GameClient, RelayTransport } from '@vgai/
|
|
12
|
+
import { GameClient, RelayTransport } from '@vgai/e2e';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Builds a `GameClient` over a `RelayTransport({ port })`.
|
|
16
16
|
*
|
|
17
17
|
* Fence values (`fenceTick`/`fenceSeq`/`fenceSimSeconds`/`fenceWallMs`) are
|
|
18
18
|
* placeholders — tick 0, seq 0, sim-seconds 0, "now" — rather than a real
|
|
19
|
-
* play-mode-start snapshot. `@vgai/
|
|
19
|
+
* play-mode-start snapshot. `@vgai/e2e`'s own relay fixture
|
|
20
20
|
* (`relay-fixture.ts`'s `waitForRelayBridge`) waits for a FRESH play-mode
|
|
21
21
|
* boot before constructing its `game` client, because a test always starts
|
|
22
22
|
* from a known instant. A `connect()`ed live session has no such instant —
|
|
@@ -28,7 +28,7 @@ import { GameClient, RelayTransport } from '@vgai/probe';
|
|
|
28
28
|
* always empty — relay mode has no separate page handle to listen on (the
|
|
29
29
|
* same documented gap `relay-fixture.ts` carries).
|
|
30
30
|
*/
|
|
31
|
-
export function createGameClient(port: number): GameClient {
|
|
31
|
+
export function createGameClient(port: number, artifactsDir?: string): GameClient {
|
|
32
32
|
return new GameClient({
|
|
33
33
|
transport: new RelayTransport({ port }),
|
|
34
34
|
pageErrors: [],
|
|
@@ -38,5 +38,6 @@ export function createGameClient(port: number): GameClient {
|
|
|
38
38
|
fenceSimSeconds: 0,
|
|
39
39
|
fenceWallMs: Date.now(),
|
|
40
40
|
warmSession: false,
|
|
41
|
+
artifactsDir,
|
|
41
42
|
});
|
|
42
43
|
}
|
package/src/index.ts
CHANGED
|
@@ -31,15 +31,16 @@
|
|
|
31
31
|
* `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
|
|
32
32
|
* PR #166) — write `step` as a literal `async (page) => {...}` and inline
|
|
33
33
|
* every value it needs. KNOWN WIRE LIMITATION: under the relay transport
|
|
34
|
-
* this binds to (the same one `vgai
|
|
34
|
+
* this binds to (the same one `vgai e2e --in-editor` uses), `step` is
|
|
35
35
|
* shipped to the editor dev server as `step.toString()` and reconstructed
|
|
36
36
|
* there — closures over outer variables do NOT survive that trip. See
|
|
37
|
-
* `@vgai/
|
|
37
|
+
* `@vgai/e2e`'s `bridge-transport.ts` (`runPageScript`'s doc comment) and
|
|
38
38
|
* `relay-transport.ts` for the full honesty-boundary contract this wraps.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
+
import { join } from 'node:path';
|
|
42
|
+
import type { GameClient } from '@vgai/e2e';
|
|
41
43
|
import { EditorClient } from '@vgai/editor-sdk';
|
|
42
|
-
import type { GameClient } from '@vgai/probe';
|
|
43
44
|
import { LiveEditor } from './editor.js';
|
|
44
45
|
import { createGameClient } from './game.js';
|
|
45
46
|
import { lazyChainProxy } from './lazy-proxy.js';
|
|
@@ -47,12 +48,19 @@ import { type ResolvedSession, resolveSession, type SessionResolutionDeps } from
|
|
|
47
48
|
import { createLazySession } from './singleton.js';
|
|
48
49
|
import { LiveTools } from './tools.js';
|
|
49
50
|
|
|
50
|
-
export type {
|
|
51
|
-
export type {
|
|
51
|
+
export type { GameClient } from '@vgai/e2e';
|
|
52
|
+
export type {
|
|
53
|
+
ActiveDocumentCapture,
|
|
54
|
+
EditorClient,
|
|
55
|
+
EditorView,
|
|
56
|
+
EditorViewDocument,
|
|
57
|
+
PresentedEditorView,
|
|
58
|
+
} from '@vgai/editor-sdk';
|
|
52
59
|
export type { ApplyDiffInput, ApplyDiffResult, PanelName } from './editor.js';
|
|
53
60
|
export { inferAssetKind, LiveEditor } from './editor.js';
|
|
54
61
|
export { createGameClient } from './game.js';
|
|
55
62
|
export type {
|
|
63
|
+
ProjectSessionHint,
|
|
56
64
|
ResolvedSession,
|
|
57
65
|
SessionListingTransport,
|
|
58
66
|
SessionResolutionDeps,
|
|
@@ -89,7 +97,10 @@ export async function connect(
|
|
|
89
97
|
const resolved = await resolveSession(projectDir, deps);
|
|
90
98
|
const client = new EditorClient({ url: `http://localhost:${resolved.port}` });
|
|
91
99
|
const editor = new LiveEditor(client, resolved.projectRoot);
|
|
92
|
-
const game = createGameClient(
|
|
100
|
+
const game = createGameClient(
|
|
101
|
+
resolved.port,
|
|
102
|
+
join(resolved.projectRoot, '.vgai', 'e2e', 'last-run'),
|
|
103
|
+
);
|
|
93
104
|
const page: PageStep = (step) => game.page(step);
|
|
94
105
|
const tools = new LiveTools(client);
|
|
95
106
|
return { editor, game, page, tools, session: resolved };
|
package/src/session.ts
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* session. `@vgai/live` never starts or stops a session (see the module doc
|
|
5
5
|
* on `index.ts`); this only finds one.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Three pieces, from two different sources:
|
|
8
8
|
*
|
|
9
|
-
* 1.
|
|
9
|
+
* 1. Exact-project discovery through `.vgai/session.json`, written by the
|
|
10
|
+
* editor inside the project and verified against `/__editor/project`.
|
|
11
|
+
* This is the fast path and cannot be delayed by unrelated sessions.
|
|
12
|
+
*
|
|
13
|
+
* 2. Global live-session DISCOVERY + PROBING (`HttpEditorTransport.listSessions`,
|
|
10
14
|
* `withTimeout`, `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`) — reused
|
|
11
15
|
* directly from `@vgai/sdk`'s `editor/transport.ts` (B3,
|
|
12
16
|
* `resolveEditorSession`'s own sibling machinery). That module is a
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
* exactly the hard, easy-to-get-subtly-wrong part worth reusing rather
|
|
17
21
|
* than re-deriving.
|
|
18
22
|
*
|
|
19
|
-
*
|
|
23
|
+
* 3. Project-root discovery (`findProjectRootFrom` below) — LIFTED (not
|
|
20
24
|
* imported) from `packages/vgai-cli/src/project-root.ts`. `@vgai/sdk`'s
|
|
21
25
|
* `resolveEditorSession` takes an already-known `ctx.projectRoot`; it has
|
|
22
26
|
* no "walk up from an arbitrary cwd to find the nearest vgai.game.json"
|
|
@@ -41,7 +45,7 @@
|
|
|
41
45
|
* match means a clear, actionable error, never a guess.
|
|
42
46
|
*/
|
|
43
47
|
|
|
44
|
-
import { existsSync } from 'node:fs';
|
|
48
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
45
49
|
import { dirname, join, resolve } from 'node:path';
|
|
46
50
|
import {
|
|
47
51
|
EDITOR_SESSION_DISCOVERY_TIMEOUT_MS,
|
|
@@ -82,6 +86,67 @@ export interface SessionResolutionDeps {
|
|
|
82
86
|
transport?: SessionListingTransport;
|
|
83
87
|
/** Project-root discovery — defaults to the real fs walk (`findProjectRootFrom` above). Overridable for tests. */
|
|
84
88
|
findProjectRootFrom?: (dir: string) => string | null;
|
|
89
|
+
/** Project-local session hint reader. Defaults to `.vgai/session.json`. */
|
|
90
|
+
readProjectSession?: (projectRoot: string) => ProjectSessionHint | null;
|
|
91
|
+
/** Exact-session verifier. Defaults to GET /__editor/project + canonical project matching. */
|
|
92
|
+
verifyProjectSession?: (hint: ProjectSessionHint, projectRoot: string) => Promise<boolean>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ProjectSessionHint {
|
|
96
|
+
port: number;
|
|
97
|
+
pid: number;
|
|
98
|
+
url: string;
|
|
99
|
+
startedAt: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function canonicalPath(path: string): string {
|
|
103
|
+
try {
|
|
104
|
+
return realpathSync(path);
|
|
105
|
+
} catch {
|
|
106
|
+
return resolve(path);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readProjectSession(projectRoot: string): ProjectSessionHint | null {
|
|
111
|
+
try {
|
|
112
|
+
const value: unknown = JSON.parse(
|
|
113
|
+
readFileSync(join(projectRoot, '.vgai', 'session.json'), 'utf8'),
|
|
114
|
+
);
|
|
115
|
+
if (typeof value !== 'object' || value === null) return null;
|
|
116
|
+
const hint = value as Record<string, unknown>;
|
|
117
|
+
if (
|
|
118
|
+
typeof hint['port'] !== 'number' ||
|
|
119
|
+
!Number.isInteger(hint['port']) ||
|
|
120
|
+
hint['port'] <= 0 ||
|
|
121
|
+
typeof hint['pid'] !== 'number' ||
|
|
122
|
+
typeof hint['url'] !== 'string' ||
|
|
123
|
+
typeof hint['startedAt'] !== 'string'
|
|
124
|
+
) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return hint as unknown as ProjectSessionHint;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function verifyProjectSession(
|
|
134
|
+
hint: ProjectSessionHint,
|
|
135
|
+
projectRoot: string,
|
|
136
|
+
): Promise<boolean> {
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch(`http://localhost:${hint.port}/__editor/project`, {
|
|
139
|
+
signal: AbortSignal.timeout(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS),
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok) return false;
|
|
142
|
+
const body = (await response.json()) as { project?: { path?: string } | null };
|
|
143
|
+
const servedProject = body.project?.path;
|
|
144
|
+
return (
|
|
145
|
+
servedProject !== undefined && canonicalPath(servedProject) === canonicalPath(projectRoot)
|
|
146
|
+
);
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
85
150
|
}
|
|
86
151
|
|
|
87
152
|
/**
|
|
@@ -105,6 +170,19 @@ export async function resolveSession(
|
|
|
105
170
|
);
|
|
106
171
|
}
|
|
107
172
|
|
|
173
|
+
// The editor writes this exact-project hint at boot and removes it on
|
|
174
|
+
// shutdown. Prefer it over a global all-session scan: it is both faster and
|
|
175
|
+
// immune to an unrelated slow/dead registry entry consuming the discovery
|
|
176
|
+
// deadline. The live probe remains authoritative, so a stale file cannot
|
|
177
|
+
// attach us to the wrong project or port.
|
|
178
|
+
const localHint = (deps.readProjectSession ?? readProjectSession)(projectRoot);
|
|
179
|
+
if (
|
|
180
|
+
localHint &&
|
|
181
|
+
(await (deps.verifyProjectSession ?? verifyProjectSession)(localHint, projectRoot))
|
|
182
|
+
) {
|
|
183
|
+
return { port: localHint.port, projectRoot };
|
|
184
|
+
}
|
|
185
|
+
|
|
108
186
|
let sessions: EditorSessionInfo[];
|
|
109
187
|
try {
|
|
110
188
|
sessions = await withTimeout(
|
|
@@ -116,8 +194,8 @@ export async function resolveSession(
|
|
|
116
194
|
sessions = [];
|
|
117
195
|
}
|
|
118
196
|
|
|
119
|
-
const canon =
|
|
120
|
-
const match = sessions.find((s) => s.project !== null &&
|
|
197
|
+
const canon = canonicalPath(projectRoot);
|
|
198
|
+
const match = sessions.find((s) => s.project !== null && canonicalPath(s.project) === canon);
|
|
121
199
|
if (!match) {
|
|
122
200
|
const otherCount = sessions.length;
|
|
123
201
|
throw new Error(
|