@vgai/editor-sdk 0.5.21 → 0.5.23

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/README.md CHANGED
@@ -48,8 +48,7 @@ One export, `EditorClient`, plus its types (`EditorState`, `ProjectInfo`,
48
48
  the independent `bounds` category). Shading targets the active Scene/Game
49
49
  viewport and remains render-only, session-local state.
50
50
  - Transform tools: `setTransformMode`, `setTransformSpace`, `setSnap`
51
- - Scene/project: `openScene`, `createProject`, `openProject`, `getProject`,
52
- `listRecentProjects`
51
+ - Project: `createProject`, `openProject`, `getProject`, `listRecentProjects`
53
52
  - State/logs: `getState`, `waitForState(predicate, timeoutMs)`,
54
53
  `getLogEntries`
55
54
  - Project tools: `listProjectTools`, `runProjectTool`
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.21",
5
+ "version": "0.5.23",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -19,11 +19,12 @@
19
19
  ".": "./src/index.ts",
20
20
  "./contributions": "./src/contributions.ts",
21
21
  "./document-probe": "./src/document-probe.ts",
22
- "./extension": "./src/extension.ts"
22
+ "./extension": "./src/extension.ts",
23
+ "./share": "./src/share.ts"
23
24
  },
24
25
  "dependencies": {
25
26
  "@types/three": "^0.180.0",
26
- "@vgai/sdk": "0.5.21"
27
+ "@vgai/sdk": "0.5.23"
27
28
  },
28
29
  "peerDependencies": {
29
30
  "@vgai/engine": "*",
package/src/client.ts CHANGED
@@ -21,6 +21,7 @@ import type {
21
21
  InspectedHierarchy,
22
22
  InspectedInspection,
23
23
  LabeledShotSetCapture,
24
+ PlayStarted,
24
25
  PresentedEditorView,
25
26
  ProjectInfo,
26
27
  ProjectTemplate,
@@ -294,11 +295,21 @@ export class EditorClient {
294
295
  * `logs/play-*.jsonl` filename and its session-journal line. Findability
295
296
  * only: no registry, no uniqueness, no lookup verb — grep and `ls` are the
296
297
  * query engine. Omitted, the filename keeps its exact unnamed shape. */
297
- async play(opts?: { seed?: number; name?: string | null }): Promise<void> {
298
- await this.command({
298
+ /* `opts.record` (`vgai play --record <name>`) NAMES this run's recording
299
+ * file. It does not ENABLE recording: every relayed play records, with no
300
+ * flag (see `editor/src/play-recording.ts`). Omitted, the clip lands in the
301
+ * rotating `.vgai/recordings/play-latest.webm`; named, in
302
+ * `.vgai/recordings/<name>.webm`, which nothing ever rotates. */
303
+ async play(opts?: {
304
+ seed?: number;
305
+ name?: string | null;
306
+ record?: string | null;
307
+ }): Promise<PlayStarted> {
308
+ return this.command<PlayStarted>({
299
309
  type: 'play',
300
310
  ...(opts?.seed !== undefined ? { seed: opts.seed } : {}),
301
311
  ...(opts?.name ? { name: opts.name } : {}),
312
+ ...(opts?.record ? { record: opts.record } : {}),
302
313
  });
303
314
  }
304
315
 
@@ -307,8 +318,10 @@ export class EditorClient {
307
318
  await this.command({ type: 'play' });
308
319
  }
309
320
 
310
- async stop(): Promise<void> {
311
- await this.command({ type: 'stop' });
321
+ /** Stops play, and finalizes this run's recording before the surface it was
322
+ * photographing is torn down. The capture is absent when nothing recorded. */
323
+ async stop(): Promise<{ recording?: GameplayRecordingCapture }> {
324
+ return this.command<{ recording?: GameplayRecordingCapture }>({ type: 'stop' });
312
325
  }
313
326
 
314
327
  async pause(): Promise<void> {
@@ -806,12 +819,6 @@ export class EditorClient {
806
819
  await this.command({ type: 'set-snap', enabled });
807
820
  }
808
821
 
809
- // --- Scene ---
810
- //
811
- // `openScene(path)` lived here, relaying `{type:'open-scene', path}`. That
812
- // verb now rejects — the `.vscn.json` format it opened is deleted, and a three
813
- // root is opened by activating its root.
814
-
815
822
  // --- Project management ---
816
823
 
817
824
  async createProject(
@@ -962,11 +969,13 @@ export class EditorClient {
962
969
  `${this.baseUrl}/__editor/console${opts?.all === true ? '?all=1' : ''}`,
963
970
  { signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS) },
964
971
  );
965
- const body = await this.readJson(res, true);
972
+ // Status first: a 404's empty body would otherwise die inside readJson
973
+ // with a parse error that hides the one fact the caller classifies on
974
+ // (does this server SERVE the console route at all?).
966
975
  if (!res.ok) {
967
976
  throw new Error(`Failed to read unresolved console: ${res.status}`);
968
977
  }
969
- return body;
978
+ return await this.readJson(res, true);
970
979
  }
971
980
 
972
981
  /** Acknowledge one named console condition. The response is observed and
@@ -993,6 +1002,27 @@ export class EditorClient {
993
1002
  * original request. That makes a subsequent `process.exit()` safe: the
994
1003
  * observer has already received every condition and occurrence count. */
995
1004
  private async readJson(res: Response, consoleComplete = false): Promise<unknown> {
1005
+ // Every one of this client's twelve routes funnels through here, so this is
1006
+ // where "did the editor server answer?" is asked — the same question, and
1007
+ // the same JSON-content-type rule, that
1008
+ // `packages/editor/src/editor-server-response.ts` owns on the browser side.
1009
+ // It is asked again rather than imported because THIS package is published
1010
+ // and depends on neither `@vgai/engine` nor the editor bundle (see
1011
+ // `DEFAULT_URL` above for that policy).
1012
+ //
1013
+ // Not theoretical here: `baseUrl` is whatever `--url`/`VGAI_EDITOR_URL`
1014
+ // says, so the CLI is routinely pointed at a SHARE TUNNEL or a static host
1015
+ // — both of which answer `200 text/html` for a route nothing serves, and
1016
+ // `res.json()` then died as `Unexpected token '<'`, naming neither the URL
1017
+ // nor the cause.
1018
+ const contentType = res.headers.get('content-type') ?? '';
1019
+ if (!contentType.includes('application/json')) {
1020
+ throw new Error(
1021
+ `The editor at ${this.baseUrl} answered with its page fallback ` +
1022
+ `(${contentType || 'no content-type'}) rather than JSON, so no editor server handled ` +
1023
+ 'the request. Check that this URL is a running `vgai edit` session.',
1024
+ );
1025
+ }
996
1026
  const body: unknown = await res.json();
997
1027
  this.observe(body, { unresolvedConsoleComplete: consoleComplete });
998
1028
  if (
package/src/extension.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * Types: `ToolInspectorContributionProps` and friends, same module.
15
15
  * (c) **System adapters** — runtime capabilities (networking, debug, …)
16
16
  * registered from game code via `ctx.registerSystemAdapter?.(kind, impl)`
17
- * (`SystemAdapters` in `@vgai/engine`'s `runtime/types` /
17
+ * (`SystemAdapters` in `@vgai/engine`'s
18
18
  * `adapter/system-adapter`). Deliberately NOT re-exported here: the
19
19
  * engine already publishes that seam and every consumer of it also
20
20
  * imports the engine — an alias would be a dead surface.
package/src/index.ts CHANGED
@@ -73,6 +73,7 @@ export type {
73
73
  InspectionPresentationKind,
74
74
  InspectionSurface,
75
75
  LabeledShotSetCapture,
76
+ PlayStarted,
76
77
  PresentedEditorView,
77
78
  ProjectInfo,
78
79
  ProjectTemplate,
@@ -83,7 +84,9 @@ export type {
83
84
  RagdollGenerationResult,
84
85
  RecentProject,
85
86
  ShadingMode,
87
+ ShotSetPoseMorph,
86
88
  ShotSetPoseRotation,
89
+ ShotSetPoseStep,
87
90
  ShotSetShot,
88
91
  StoryCaptureOptions,
89
92
  StoryVariantCapture,
@@ -98,5 +101,6 @@ export type {
98
101
  export {
99
102
  EDITOR_VIEW_BUILT_IN_UTILITY_IDS,
100
103
  EDITOR_VIEW_TOOL_UTILITY_PREFIX,
104
+ EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS,
101
105
  isEditorViewUtility,
102
106
  } from './types.js';
package/src/share.ts ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * THE share-session wire — `/__editor/share-control`'s payloads, declared once.
3
+ *
4
+ * This route has three clients living in three different compilation units:
5
+ * the editor SERVER that serves it (`packages/editor/server/`), the browser
6
+ * PANEL that polls it (`packages/editor/src/`), and the `vgai share` CLI
7
+ * (`packages/vgai-cli/src/`). Each of them used to declare its own copy of the
8
+ * payload, and the copies had already drifted — silently, in both directions,
9
+ * because an under-declared read of a JSON superset is a type error nowhere:
10
+ *
11
+ * - the CLI's invitation had no `delivery`, so `vgai share` could not report
12
+ * that the invitation email had FAILED to send — the one state the field
13
+ * exists for;
14
+ * - the CLI's audit rows typed `type` and `capability` as bare `string`,
15
+ * which accepts any spelling of an event the server never emits;
16
+ * - the browser panel's invitation had no `inviterAccountId`;
17
+ * - the browser panel had no `audit` at all, and re-declared the tunnel
18
+ * health shape inline instead of naming it.
19
+ *
20
+ * This package is the right home for all three: `@vgai/editor` and `@vgai/cli`
21
+ * both already depend on it, it depends on neither, and the CLI's esbuild
22
+ * bundle inlines it. (The CLI's standing "no editor-package dependency" rule is
23
+ * about `@vgai/editor` — the server+UI package whose express/vite/chokidar
24
+ * graph the standalone bundle must not drag in. This is a types-only module.)
25
+ *
26
+ * Two things deliberately stay OFF this wire, and stay off it structurally
27
+ * rather than by memory:
28
+ *
29
+ * - the invitation TOKEN HASH, which is a local extension on the server
30
+ * (`share-host.ts`'s `StoredShareInvitation extends ShareInvitationRecord`);
31
+ * - a participant's ORGANIZATION list, which the gateway holds on its own
32
+ * `ShareAccountProjection` to resolve team invitations. {@link ShareAccount}
33
+ * is what a participant projects to, so `status()` narrows to it.
34
+ */
35
+
36
+ /**
37
+ * A share role, least authority first.
38
+ *
39
+ * `maintainer` is deliberately absent: it is local-only and has no share role,
40
+ * which is why the editor's `CollaborationRole` is this union PLUS `maintainer`
41
+ * rather than the other way around.
42
+ */
43
+ export type ShareRole = 'viewer' | 'commenter' | 'tester' | 'editor' | 'terminal';
44
+
45
+ export type ShareTunnelProvider = 'cloudflared' | 'ngrok';
46
+
47
+ export type ShareTunnelState = 'stopped' | 'starting' | 'healthy' | 'failed';
48
+
49
+ export interface ShareTunnelHealth {
50
+ checkedAt: string;
51
+ latencyMs: number;
52
+ }
53
+
54
+ /**
55
+ * The capability vocabulary a grant is checked against.
56
+ *
57
+ * Wider than the roles on purpose: `public` and `never-share` are DECLARATIONS
58
+ * a route makes about itself, and `maintain` is the local-only authority no
59
+ * share role reaches — so this union is not derivable from {@link ShareRole}.
60
+ */
61
+ export type ShareCapability =
62
+ | 'public'
63
+ | 'view'
64
+ | 'comment'
65
+ | 'test'
66
+ | 'edit'
67
+ | 'terminal'
68
+ | 'maintain'
69
+ | 'never-share';
70
+
71
+ /** A verified VGAI account, as a share participant projects onto the wire. */
72
+ export interface ShareAccount {
73
+ id: string;
74
+ email: string;
75
+ name?: string;
76
+ }
77
+
78
+ /**
79
+ * How the invitation was DELIVERED, when we delivered it.
80
+ *
81
+ * Present-and-`failed` is the state this field exists for: an invitation
82
+ * created whose email never left is otherwise indistinguishable, on every
83
+ * surface, from one that arrived.
84
+ */
85
+ export interface ShareInvitationDelivery {
86
+ channel: 'clerk-email';
87
+ status: 'sent' | 'failed';
88
+ providerInvitationId?: string;
89
+ sentAt?: string;
90
+ problem?: string;
91
+ }
92
+
93
+ export interface ShareInvitationRecord {
94
+ id: string;
95
+ inviterAccountId: string;
96
+ recipientAccountId?: string;
97
+ recipientEmail?: string;
98
+ recipientOrganizationId?: string;
99
+ recipientOrganizationName?: string;
100
+ recipientOrganizationDomain?: string;
101
+ /** The highest role this invitation may ever be raised to. */
102
+ roleCeiling: ShareRole;
103
+ usePolicy: 'person' | 'team';
104
+ createdAt: string;
105
+ expiresAt: string;
106
+ revokedAt?: string;
107
+ delivery?: ShareInvitationDelivery;
108
+ }
109
+
110
+ export interface ShareParticipant {
111
+ participantId: string;
112
+ account: ShareAccount;
113
+ invitationId: string;
114
+ role: ShareRole;
115
+ roleCeiling: ShareRole;
116
+ credentials: number;
117
+ joined: boolean;
118
+ }
119
+
120
+ export type ShareAuditEventType =
121
+ | 'auth-failed'
122
+ | 'auth-started'
123
+ | 'invite-redeemed'
124
+ | 'capability-denied'
125
+ | 'role-changed'
126
+ | 'participant-kicked'
127
+ | 'invitation-revoked'
128
+ | 'terminal-invoked';
129
+
130
+ export interface ShareAuditEvent {
131
+ at: string;
132
+ type: ShareAuditEventType;
133
+ invitationId?: string;
134
+ participantId?: string;
135
+ accountId?: string;
136
+ capability?: ShareCapability;
137
+ /** The role a `role-changed` event settled on. */
138
+ role?: ShareRole;
139
+ }
140
+
141
+ /** The `/__editor/share-control/status` payload. */
142
+ export interface ShareStatus {
143
+ active: boolean;
144
+ sessionId: string;
145
+ projectRoot: string;
146
+ publicUrl?: string;
147
+ tunnelProvider?: ShareTunnelProvider;
148
+ tunnel: ShareTunnelState;
149
+ health?: ShareTunnelHealth;
150
+ invitations: readonly ShareInvitationRecord[];
151
+ participants: readonly ShareParticipant[];
152
+ audit: readonly ShareAuditEvent[];
153
+ problem?: string;
154
+ }
155
+
156
+ /** The `/__editor/share-control/invitations` reply. */
157
+ export interface CreatedShareInvitation {
158
+ invitation: ShareInvitationRecord;
159
+ url: string;
160
+ }
package/src/types.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * The two viewport tabs — the editor's two MODES, named for what the user is
3
3
  * doing rather than for what happens to be mounted (ARCHITECTURE-CORE
4
- * §Vocabulary, decided 2026-08-02). They were `'scene' | 'game'` until WO-LR1:
5
- * `'scene'` outlived the `.vscn` scene format that named it (a three root is
6
- * TSX now, and asset/story/tool documents live on that tab too), and `'game'`
7
- * named the mounted artifact rather than the mode. Not to be confused with the
4
+ * §Vocabulary, decided 2026-08-02). Deliberately not `'scene' | 'game'`:
5
+ * `'scene'` names a document format (a three root is TSX, and
6
+ * asset/story/tool documents live on that tab too), and `'game'`
7
+ * names the mounted artifact rather than the mode. Not to be confused with the
8
8
  * PLAY TRANSPORT (the five stable controls) — this is which tab is showing.
9
9
  */
10
10
  export type ViewportTab = 'edit' | 'play';
@@ -104,6 +104,9 @@ export interface GameCapture {
104
104
  export interface GameplayRecordingOptions {
105
105
  /** Requested real-time capture cadence. Defaults to 30; range 1–60. */
106
106
  fps?: number;
107
+ /** Names the file under `.vgai/recordings/`. Absent takes the rotating
108
+ * `play-latest.webm`, which the next play overwrites. */
109
+ name?: string | null;
107
110
  }
108
111
 
109
112
  /** Facts fixed when a gameplay recording starts. */
@@ -115,15 +118,34 @@ export interface GameplayRecordingStarted {
115
118
  fps: number;
116
119
  audio: boolean;
117
120
  layers: { canvases: number; domOverlays: number };
121
+ /** Where the bytes are landing — known at START, so a caller can say where
122
+ * the evidence for a run still in progress will be. */
123
+ path: string;
124
+ /** True for the rotating unnamed clip; false for a `--record <name>` keepsake. */
125
+ rotates: boolean;
126
+ /** This run's `logs/play-*.jsonl`, whose event timestamps map to clip offsets. */
127
+ logFile: string | null;
118
128
  }
119
129
 
120
130
  /** Final browser recording. Media chunks stream directly to the project while
121
131
  * recording, so the command result stays small even for a long playthrough. */
122
132
  export interface GameplayRecordingCapture extends GameplayRecordingStarted {
123
- path: string;
124
133
  durationMs: number;
125
134
  droppedFrames: number;
126
135
  frameErrors: number;
136
+ /** The cadence ACHIEVED (frames over wall duration), which diverges from the
137
+ * requested `fps` on a throttled hidden tab. Read this before reasoning
138
+ * about a clip's timing. */
139
+ effectiveFps: number;
140
+ /** True when the tab was hidden for any part of the recording. */
141
+ hidden: boolean;
142
+ }
143
+
144
+ /** What `play` reports back once the run is up and recording. */
145
+ export interface PlayStarted {
146
+ /** Absent only when the recorder could not start; play is up either way and
147
+ * the editor console carries the reason. */
148
+ recording?: GameplayRecordingStarted;
127
149
  }
128
150
 
129
151
  export type AssetPreviewView = 'front' | 'right' | 'top' | 'perspective';
@@ -241,32 +263,60 @@ export interface AssetCompareCapture {
241
263
  }
242
264
 
243
265
  /**
266
+ * THE shot-set contract. This block is the ONE declaration of it.
267
+ *
244
268
  * A project-defined labeled shot set (`vgai screenshot <target> --shots <set>`).
245
269
  * The DEFINITION is project data: a registered project tool named
246
270
  * `project.<set>.previewShots` returns it (installed capabilities register
247
- * theirs — e.g. the humanoid capability contributes its canonical verify set),
248
- * and the editor's generic capture engine renders it — turntable yaw angles
249
- * and skeleton-anchored zoom crops, optionally under a named pose applied to
250
- * a disposable snapshot (see `packages/editor/src/asset-preview.ts`).
271
+ * theirs — the bird, humanoid and walking-castle capabilities each contribute
272
+ * their canonical verify set), and the editor's generic capture engine renders
273
+ * it — turntable yaw angles and skeleton-anchored zoom crops, optionally under
274
+ * a named pose applied to a disposable snapshot. Every shot is labeled so a
275
+ * flat directory of PNGs is self-describing without a manifest file.
276
+ *
277
+ * The contract lives HERE, in the SDK, because it crosses the editor relay:
278
+ * the capability tool that authors a set, the CLI that ships it across, and
279
+ * `packages/editor/src/asset-preview.ts`'s capture engine that renders it are
280
+ * three different programs. Each of those used to declare its own copy — five
281
+ * declarations in total — and the copies had already drifted on what a pose
282
+ * step's `radians` is measured FROM. Every side now type-checks against this
283
+ * one: the capture engine annotates its parser's return with
284
+ * `AssetPreviewShotSetDefinition`, and each capability tool annotates its
285
+ * exported set with it, so a Zod schema that stops matching this shape is a
286
+ * compile error rather than a runtime surprise at the relay.
251
287
  */
252
288
  export interface ShotSetPoseRotation {
253
289
  bone: string;
254
290
  axis: 'x' | 'y' | 'z';
291
+ /**
292
+ * A DELTA about the bone's own local axis, in radians, relative to the
293
+ * loaded GLB's baked rest pose — NOT an absolute local rotation.
294
+ *
295
+ * That is what the capture engine does with it: `bone.rotateX/Y/Z(radians)`,
296
+ * which composes onto the bone's existing local quaternion. The consequence
297
+ * to watch for is that "rest pose" means whatever the GLB baked, not
298
+ * identity — a rig baked mid-gait needs `pose(t) - pose(0)` here, while a
299
+ * rig baked at identity can pass its absolute angle unchanged. Getting that
300
+ * backwards silently double-applies the baked pose.
301
+ */
255
302
  radians: number;
256
303
  }
257
304
 
258
305
  /** The other half of a pose: a named morph target driven to an influence.
259
- * A rig whose meshes carry no such morph is posed by the rotations alone
260
- * the same degrade-don't-throw rule the rotations follow for a missing
261
- * joint. */
306
+ * Morphs are how a rig expresses what a joint cannot an eyelid sliding over
307
+ * an eyeball, a brow band tilting so a set that poses a FACE needs both
308
+ * halves or it can only ever show the jaw. A rig whose meshes carry no such
309
+ * morph is posed by the rotations alone: the same degrade-don't-throw rule
310
+ * the rotations follow for a missing joint. */
262
311
  export interface ShotSetPoseMorph {
263
312
  morph: string;
264
313
  influence: number;
265
314
  }
266
315
 
267
- /** One step of a named pose. A pose is a flat list so a single expression
268
- * (a jaw ROTATION plus a brow MORPH) is one entry in `poses`, not two
269
- * parallel lists the capture engine would have to zip. */
316
+ /** One step of a named pose. A flat union rather than two parallel lists: a
317
+ * single expression is normally one rotation AND one morph (a jaw ROTATION
318
+ * plus a brow MORPH), so keeping them in one ordered list means a definition
319
+ * never has to zip them. */
270
320
  export type ShotSetPoseStep = ShotSetPoseRotation | ShotSetPoseMorph;
271
321
 
272
322
  export type ShotSetShot =
@@ -276,12 +326,19 @@ export type ShotSetShot =
276
326
  view: 'bone-zoom';
277
327
  bones: string[];
278
328
  spanFraction: number;
279
- /** The angle the crop is taken FROM, in the same radian convention as
280
- * a turntable shot's `yaw` (0 the front camera, -PI/2 the subject's
281
- * left). Omitted means 0. The front camera is not a general answer
282
- * for a long subject: a crop anchored on the tail root of an 8 m
283
- * quadruped otherwise photographs the hind legs standing in front of
284
- * it. */
329
+ /**
330
+ * The angle the crop is taken FROM, in the same convention and units as
331
+ * a turntable shot's `yaw` (radians; 0 is the front camera, -PI/2 the
332
+ * subject's left, +PI/2 its right). Omitted means 0 the front-camera
333
+ * framing every bone-zoom shot had before this field existed, so an
334
+ * existing shot set renders unchanged.
335
+ *
336
+ * It exists because the front camera is not a general answer for a long
337
+ * subject: on an 8 m quadruped a crop anchored on the tail root
338
+ * photographs the hind legs standing between the camera and the tail. A
339
+ * junction whose axis runs down the body's length is inspectable only
340
+ * from the side.
341
+ */
285
342
  yaw?: number | undefined;
286
343
  pose?: string | undefined;
287
344
  };
@@ -289,8 +346,8 @@ export type ShotSetShot =
289
346
  export interface AssetPreviewShotSetDefinition {
290
347
  /** The set's name (the CLI's `--shots <name>`), echoed in error messages. */
291
348
  name: string;
292
- /** Joints that must exist on the loaded GLB's OWN skeleton (zoom anchors
293
- * plus a loud failure naming the missing joints never a silent
349
+ /** Joints that must exist on the loaded GLB's OWN skeleton zoom anchors
350
+ * plus a loud failure naming the missing joints (never a silent
294
351
  * bounding-box fallback for a set that promised skeleton anchoring). */
295
352
  requiredBones?: string[];
296
353
  /** Optional clause appended to rig-requirement errors,
@@ -447,12 +504,12 @@ export interface EditorState {
447
504
  *
448
505
  * `census` is the tab's RESOURCE PROFILE, sampled by the page every five
449
506
  * seconds and carried on the heartbeat: what a browser-level renderer death
450
- * would otherwise leave unexplained. `heapUsedMB`/`heapLimitMB` are null off
451
- * Chromium (`performance.memory` is non-standard); the renderer counts are
452
- * absent, never zero, when no game has registered a render-debug adapter.
453
- * `mountEpochs` counts the project module generations accumulated in this
454
- * document; it is absent against an older server. `censusAgeMs` says how
455
- * stale the profile is — a hidden tab is not sampled.
507
+ * would otherwise leave unexplained. It is `@vgai/sdk/tab-census`'s
508
+ * {@link RecordedTabCensus} the census MINUS the `mountEpochs` guarantee,
509
+ * because this is a response and the server that answered it may be older
510
+ * than that field; every other absence (`heapUsedMB` off Chromium, renderer
511
+ * counts with no mounted adapter) is documented on the type itself.
512
+ * `censusAgeMs` says how stale the profile is — a hidden tab is not sampled.
456
513
  */
457
514
  tabs?: Array<{
458
515
  tabId8: string;
@@ -470,16 +527,7 @@ export interface EditorState {
470
527
  unresponsive: boolean;
471
528
  commandListener?: 'ready' | 'not attached' | (string & {});
472
529
  pageErrors?: string[];
473
- census?: {
474
- heapUsedMB: number | null;
475
- heapLimitMB: number | null;
476
- mountEpochs?: number;
477
- canvases: number;
478
- canvasMB: number;
479
- textures?: number;
480
- geometries?: number;
481
- programs?: number;
482
- } | null;
530
+ census?: RecordedTabCensus | null;
483
531
  censusAgeMs?: number | null;
484
532
  }>;
485
533
  /**
@@ -654,8 +702,17 @@ export type AnimationCaptureAction = 'start' | 'stop' | 'review' | 'commit' | 'd
654
702
  export type TransformMode = 'translate' | 'rotate' | 'scale';
655
703
  export type TransformSpace = 'world' | 'local';
656
704
 
657
- /** Stable editor-owned workspace documents that may appear in a shareable
658
- * view. One runtime list owns both URL parsing and the public id type. */
705
+ /**
706
+ * Stable editor-owned workspace documents that may appear in a shareable
707
+ * view. One runtime list owns both URL parsing and the public id type.
708
+ *
709
+ * This SDK cannot import the editor — the dependency runs the other way — so
710
+ * nothing links this list to the documents the editor actually registers, and
711
+ * an id added there is silently unaddressable here (a view carrying it
712
+ * round-trips to nothing). The link is asserted from the side that CAN see
713
+ * both: `packages/editor/test/editor-view-address-space.test.ts`. Adding a
714
+ * document id means adding it here too.
715
+ */
659
716
  export const EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS = [
660
717
  'workspace:scene',
661
718
  'workspace:canvas-scene',
@@ -664,6 +721,7 @@ export const EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS = [
664
721
  'workspace:2d-components',
665
722
  'workspace:ui-components',
666
723
  'workspace:build-profiles',
724
+ 'workspace:asset-budget',
667
725
  'account',
668
726
  'project-tools',
669
727
  ] as const;
@@ -677,13 +735,28 @@ export type EditorViewWorkspaceDocumentId = (typeof EDITOR_VIEW_WORKSPACE_DOCUME
677
735
  * reason: a separate hand-written union and parser allowlist drift silently
678
736
  * (`behavior` was in the union and missing from the allowlist, so a view
679
737
  * carrying it round-tripped to nothing).
738
+ *
739
+ * This must equal the editor's live `BUILT_IN_WORKSPACE_UTILITIES`
740
+ * (`packages/editor/src/workspace-core-utilities.ts`). It had drifted to five
741
+ * of thirteen — every id from `generations` onward was unaddressable in a
742
+ * shared view. As above, the SDK cannot import the editor to derive this, so
743
+ * `packages/editor/test/editor-view-address-space.test.ts` asserts the
744
+ * equality from the side that sees both.
680
745
  */
681
746
  export const EDITOR_VIEW_BUILT_IN_UTILITY_IDS = [
682
- 'profiler',
683
- 'console',
684
747
  'animation',
685
748
  'behavior',
749
+ 'generations',
750
+ 'console',
751
+ 'profiler',
752
+ 'state-watch',
686
753
  'light-explorer',
754
+ 'network',
755
+ 'audio',
756
+ 'build-output',
757
+ 'story-actions',
758
+ 'story-interactions',
759
+ 'story-accessibility',
687
760
  ] as const;
688
761
 
689
762
  export type EditorViewBuiltInUtilityId = (typeof EDITOR_VIEW_BUILT_IN_UTILITY_IDS)[number];
@@ -801,6 +874,7 @@ export type ProjectToolOutcome =
801
874
  };
802
875
 
803
876
  import type { GenerationJob } from '@vgai/sdk/generations';
877
+ import type { RecordedTabCensus } from '@vgai/sdk/tab-census';
804
878
 
805
879
  export type {
806
880
  ProjectToolCatalog,