@volter/editor-sdk 0.5.57
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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +19 -0
- package/package.json +90 -0
- package/src/account.ts +210 -0
- package/src/chrome.ts +83 -0
- package/src/client.ts +1547 -0
- package/src/commands.ts +66 -0
- package/src/contributions.ts +985 -0
- package/src/document-probe.ts +237 -0
- package/src/editor-view.ts +220 -0
- package/src/extension.ts +40 -0
- package/src/generations.ts +178 -0
- package/src/host.ts +1167 -0
- package/src/http-transport.browser.ts +14 -0
- package/src/http-transport.node.ts +19 -0
- package/src/index.ts +128 -0
- package/src/layout-arrangements.ts +5 -0
- package/src/layouts.tsx +108 -0
- package/src/looks.ts +14 -0
- package/src/project/output-roots.ts +73 -0
- package/src/project/tab-census.ts +149 -0
- package/src/project-tool-catalog.ts +96 -0
- package/src/selection.tsx +108 -0
- package/src/services.ts +18 -0
- package/src/session/build-report.ts +19 -0
- package/src/session/collaboration-types.ts +262 -0
- package/src/session/command-table.ts +333 -0
- package/src/session/discovery.ts +90 -0
- package/src/session/editor-brand.ts +73 -0
- package/src/session/editor-compatibility.ts +248 -0
- package/src/session/editor-control-lifecycle.ts +68 -0
- package/src/session/editor-control-protocol.ts +5 -0
- package/src/session/entrypoint-selection-readers.ts +66 -0
- package/src/session/entrypoint-selection-source.ts +120 -0
- package/src/session/game-css-scope.ts +30 -0
- package/src/session/product-create.ts +24 -0
- package/src/session/product-locator.ts +389 -0
- package/src/session/project-module-url.ts +245 -0
- package/src/session/registry-format.ts +203 -0
- package/src/session/relative-path-guard.ts +56 -0
- package/src/session/source-glob.ts +15 -0
- package/src/session/tool-contribution-convention.ts +116 -0
- package/src/session/workbench-locator.ts +650 -0
- package/src/session.ts +41 -0
- package/src/share.ts +160 -0
- package/src/tools/errors.ts +91 -0
- package/src/tools/provider-execution.ts +70 -0
- package/src/tools/registry.ts +341 -0
- package/src/tools/types.ts +159 -0
- package/src/transport.ts +97 -0
- package/src/types.ts +1581 -0
- package/src/views.ts +164 -0
- package/src/widgets/design-system.ts +93 -0
- package/src/widgets/editor-appearance.ts +149 -0
- package/src/widgets/editor-material.ts +83 -0
- package/src/widgets/icon-set-registry.ts +105 -0
- package/src/widgets/index.ts +71 -0
- package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
- package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
- package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
- package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
- package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
- package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
- package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
- package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
- package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
- package/src/widgets/inspector-widgets/color-utils.ts +201 -0
- package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
- package/src/widgets/inspector-widgets/index.ts +24 -0
- package/src/widgets/inspector-widgets/shared.tsx +140 -0
- package/src/widgets/interactive-edit-scope.ts +33 -0
- package/src/widgets/patterns/Dialog.tsx +129 -0
- package/src/widgets/patterns/Fields.tsx +44 -0
- package/src/widgets/patterns/List.tsx +25 -0
- package/src/widgets/patterns/StateSurface.tsx +40 -0
- package/src/widgets/patterns/Surfaces.tsx +122 -0
- package/src/widgets/patterns/Tabs.tsx +80 -0
- package/src/widgets/patterns/Toolbar.tsx +72 -0
- package/src/widgets/patterns/Tree.tsx +72 -0
- package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
- package/src/widgets/primitives/Button.tsx +62 -0
- package/src/widgets/primitives/ColorInput.tsx +78 -0
- package/src/widgets/primitives/DraftTextInput.tsx +63 -0
- package/src/widgets/primitives/EditorIcon.tsx +157 -0
- package/src/widgets/primitives/FormControls.tsx +88 -0
- package/src/widgets/primitives/HoverPreview.tsx +96 -0
- package/src/widgets/primitives/JsonInput.tsx +113 -0
- package/src/widgets/primitives/Layout.tsx +100 -0
- package/src/widgets/primitives/Menu.tsx +140 -0
- package/src/widgets/primitives/NumberInput.tsx +169 -0
- package/src/widgets/primitives/Panel.tsx +80 -0
- package/src/widgets/primitives/SectionHeader.tsx +77 -0
- package/src/widgets/primitives/Text.tsx +54 -0
- package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
- package/src/widgets/primitives/Tooltip.tsx +204 -0
- package/src/widgets/primitives/Vec3Input.tsx +70 -0
- package/src/widgets/primitives/banner-tones.ts +32 -0
- package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
- package/src/widgets/primitives/editor-icons.ts +245 -0
- package/src/widgets/primitives/panel-header-styles.ts +42 -0
- package/src/widgets/theme.ts +2633 -0
- package/src/widgets/z-index.ts +25 -0
package/src/host.ts
ADDED
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE HOST DOOR — what a PACKAGE's contribution may read of the running
|
|
3
|
+
* editor (ARCHITECTURE-CORE §The workbench, "direction": a package imports
|
|
4
|
+
* only other packages' exports, never the host's internals).
|
|
5
|
+
*
|
|
6
|
+
* The editor registers this at boot as MODULE STATE, the same door shape as
|
|
7
|
+
* `registerProjectModuleLoader` in `contributions.ts`: the SDK is one
|
|
8
|
+
* identity in the editor's program (Vite dedupes it), so a contribution
|
|
9
|
+
* served from a package sees the editor's registration. The surface is
|
|
10
|
+
* deliberately small and grows one member per contribution that needs it;
|
|
11
|
+
* a member nobody reads is cut.
|
|
12
|
+
*
|
|
13
|
+
* Reading it outside a host — in a test, or a package's own tooling — throws
|
|
14
|
+
* by name rather than answering with an empty session: an inspector that
|
|
15
|
+
* silently reads "no adapter" is a measurement nobody made.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
AudioAdapter,
|
|
20
|
+
MountedRoot,
|
|
21
|
+
NetworkingAdapter,
|
|
22
|
+
StoriesProvider,
|
|
23
|
+
SystemAdapters,
|
|
24
|
+
} from '@volter/editor-project/adapter';
|
|
25
|
+
import type { ComponentType } from 'react';
|
|
26
|
+
import { useSyncExternalStore } from 'react';
|
|
27
|
+
import type * as THREE from 'three';
|
|
28
|
+
import type { StageTransportHandle } from './transport';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* THE STORY RUNTIME'S DOORWAY — the URL a package dynamic-imports to reach
|
|
32
|
+
* Storybook's `composeStories`/`setProjectAnnotations` and React DOM's
|
|
33
|
+
* `createRoot`/`flushSync` FROM THE PROJECT'S OWN MODULE GRAPH, rather than
|
|
34
|
+
* from the shell's copy.
|
|
35
|
+
*
|
|
36
|
+
* It is here because it is the host's statement about what it serves, and a
|
|
37
|
+
* package may not reach into the host's build tier to read it: the address
|
|
38
|
+
* lived in `packages/editor/vite-plugin-module-doorways.ts`, which serves it,
|
|
39
|
+
* and the story runtime imported it back out through a specifier that stepped
|
|
40
|
+
* out of the editor's `src/` entirely. The plugin still OWNS the
|
|
41
|
+
* doorway — what it serves, and why the mount would otherwise get a second
|
|
42
|
+
* React (its doc comment carries the measured failure) — and now spells the
|
|
43
|
+
* address by importing this constant, so there is one spelling and the package
|
|
44
|
+
* reads it through the published door like any other host fact.
|
|
45
|
+
*/
|
|
46
|
+
export const STORY_RUNTIME_PATH = '/__vgai-story-runtime';
|
|
47
|
+
/** Project-owned React/Three namespace shared by preview consumers and the server. */
|
|
48
|
+
export const R3F_RUNTIME_PATH = '/__vgai-r3f-runtime';
|
|
49
|
+
|
|
50
|
+
/** The live session's system adapters, as the editor inspects them: the
|
|
51
|
+
* instance under inspection when several run, the solo one otherwise. */
|
|
52
|
+
export interface EditorHostSystems {
|
|
53
|
+
/** Every adapter the inspected session registered, as one object. */
|
|
54
|
+
inspected(): SystemAdapters;
|
|
55
|
+
/** Fires when the inspected instance or its adapters change. */
|
|
56
|
+
subscribe(listener: () => void): () => void;
|
|
57
|
+
inspectedNetworking(): NetworkingAdapter | null;
|
|
58
|
+
/** Fires when the inspected networking adapter appears, changes or leaves. */
|
|
59
|
+
subscribeNetworking(listener: () => void): () => void;
|
|
60
|
+
inspectedAudio(): AudioAdapter | null;
|
|
61
|
+
/** Fires when the inspected audio adapter appears, changes or leaves. */
|
|
62
|
+
subscribeAudio(listener: () => void): () => void;
|
|
63
|
+
/** Monotonic counter behind {@link subscribeAudio}, for `useSyncExternalStore`. */
|
|
64
|
+
audioVersion(): number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The editor's ONE shared availability heartbeat (250ms, refcounted): the
|
|
68
|
+
* signal for out-of-band changes a live session makes without notifying a
|
|
69
|
+
* store — an adapter's connection state, a reader appearing. */
|
|
70
|
+
export interface EditorHostAvailability {
|
|
71
|
+
subscribe(listener: () => void): () => void;
|
|
72
|
+
version(): number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface EditorHostWorkspace {
|
|
76
|
+
/** Reveal a bottom-drawer utility by its registered id (`tool:<id>` for a
|
|
77
|
+
* contributed one). */
|
|
78
|
+
showUtility(id: string): void;
|
|
79
|
+
/** Open a contributed `workspace.document` by its contribution id
|
|
80
|
+
* (`my-tool.document`); false when no such document is registered. */
|
|
81
|
+
openContributedDocument(id: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Open a document by ADDRESS — `{ kind, …the kind's own fields }` — through
|
|
84
|
+
* the host's document-open registry. The SDK names NO document kind: the
|
|
85
|
+
* caller spells the address its own package registered (or that another
|
|
86
|
+
* package in the build did), the host routes it, and a kind with no
|
|
87
|
+
* registered opener answers `false` instead of pretending.
|
|
88
|
+
*
|
|
89
|
+
* It is the async form on purpose. A kind may need to SETTLE — re-read the
|
|
90
|
+
* ledger it opens documents out of — before it can answer, which is exactly
|
|
91
|
+
* what a package that just WROTE the subject needs (a generated ragdoll's
|
|
92
|
+
* Setup story, addressed `{ kind: 'story', modulePath, storyName }` the
|
|
93
|
+
* moment the file exists).
|
|
94
|
+
*/
|
|
95
|
+
open(address: { readonly kind: string } & Record<string, unknown>): Promise<boolean>;
|
|
96
|
+
readonly liveDocument: EditorHostLiveDocument;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface LiveDocumentContentProps {
|
|
100
|
+
readonly documentId: string;
|
|
101
|
+
/** Whether the document is the active center tab. */
|
|
102
|
+
readonly active: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** What a lane renders inside the live document: the panel a runtime mounts
|
|
106
|
+
* into, and its document-local toolbar. */
|
|
107
|
+
export interface LiveDocumentContent {
|
|
108
|
+
readonly Content: ComponentType<LiveDocumentContentProps>;
|
|
109
|
+
readonly Toolbar?: ComponentType<LiveDocumentContentProps>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* THE LIVE DOCUMENT — the host's one center document for running content
|
|
114
|
+
* (id `workspace:game`, title `Game`). It exists exactly as long as a
|
|
115
|
+
* runtime does: a lane `acquire`s it before mounting (open + activate, then
|
|
116
|
+
* wait for the panel's container to commit) and the host closes it on the
|
|
117
|
+
* playing → stopped edge, handing focus back. The panel's element is the
|
|
118
|
+
* live CONTAINER every lane mounts into; the content that draws the panel
|
|
119
|
+
* is registered by the package that runs things.
|
|
120
|
+
*/
|
|
121
|
+
export interface EditorHostLiveDocument {
|
|
122
|
+
readonly id: string;
|
|
123
|
+
register(content: LiveDocumentContent): () => void;
|
|
124
|
+
/** Resolves false when the panel did not commit in time (report it as
|
|
125
|
+
* the lane's own start failure — never mount into nothing). */
|
|
126
|
+
acquire(timeoutMs?: number): Promise<boolean>;
|
|
127
|
+
release(): void;
|
|
128
|
+
open(): boolean;
|
|
129
|
+
container(): HTMLElement | null;
|
|
130
|
+
/** The panel's attach/detach halves — detach is keyed by the element so a
|
|
131
|
+
* stale panel's cleanup never empties a newer panel's slot. */
|
|
132
|
+
setContainer(el: HTMLElement): void;
|
|
133
|
+
releaseContainer(el: HTMLElement): void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The open documents, as a contribution may read them. */
|
|
137
|
+
export interface EditorHostDocuments {
|
|
138
|
+
activeId(): string | null;
|
|
139
|
+
/** The active document's kind (`'workspace'`, `'tool-contribution'`, …)
|
|
140
|
+
* and title, or null with none open. */
|
|
141
|
+
active(): { readonly id: string; readonly kind: string; readonly title: string } | null;
|
|
142
|
+
subscribe(listener: () => void): () => void;
|
|
143
|
+
version(): number;
|
|
144
|
+
/**
|
|
145
|
+
* THE DOCUMENT'S OWN PUBLISHED CONTEXT — the one object a document hands the
|
|
146
|
+
* host to be driven through (`editor.document.run(ctx => …)`, the REPL
|
|
147
|
+
* door), or `undefined` when that document published none.
|
|
148
|
+
*
|
|
149
|
+
* NEW (2026-09-19) because the document a package DRIVES is not always the
|
|
150
|
+
* document it opened: `@vgai/blender` presents every Blender frame into the
|
|
151
|
+
* Model document, which `@vgai/blender` contributes and publishes — so the two
|
|
152
|
+
* packages meet at this registry and neither may reach the host's
|
|
153
|
+
* `@editor/document-context-registry` to find it. The value is `unknown` on
|
|
154
|
+
* purpose: what a document publishes is an agreement between the package
|
|
155
|
+
* that renders it and the package that drives it, and the host is not a
|
|
156
|
+
* party to it — the caller narrows, and refuses by name when the shape is
|
|
157
|
+
* not the one it needs.
|
|
158
|
+
*/
|
|
159
|
+
context(documentId: string): unknown;
|
|
160
|
+
/**
|
|
161
|
+
* The same read, where the caller can WAIT. Opening a document activates its
|
|
162
|
+
* tab before an async model import can publish a context, so a driver that
|
|
163
|
+
* read once would mistake a loading document for an unsupported one.
|
|
164
|
+
* Resolves `undefined` when the window elapses.
|
|
165
|
+
*/
|
|
166
|
+
waitForContext(documentId: string, timeoutMs?: number): Promise<unknown>;
|
|
167
|
+
/**
|
|
168
|
+
* THE PUBLISHED CONTEXT HAS MOVED — said by the package that DRIVES this
|
|
169
|
+
* document, which is not always the package that published it.
|
|
170
|
+
*
|
|
171
|
+
* NEW (2026-09-19, WORK.md §Blender in the tab is Blender, "Inspection
|
|
172
|
+
* parity", I1) because a context object is a LIVE HANDLE: `@vgai/blender`
|
|
173
|
+
* reads the engine through its RNA door, and that answer decides which
|
|
174
|
+
* Properties tabs exist for the selected datablock — an armature has a Bone
|
|
175
|
+
* tab, a cube does not. Nothing in the host's own stores moves when the
|
|
176
|
+
* engine answers, so the inspector had no reason to re-compose and the rail
|
|
177
|
+
* stayed at whatever the first render could see. Calling this re-derives the
|
|
178
|
+
* whole inspection, matches included.
|
|
179
|
+
*
|
|
180
|
+
* It is a notification, not a publication: the context object itself is
|
|
181
|
+
* unchanged, and a document that published none is a no-op.
|
|
182
|
+
*/
|
|
183
|
+
contextChanged(documentId: string): void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The project's editor-local state document (`.vgai/editor-state.json`):
|
|
188
|
+
* per-project preferences a contribution keeps — pins, collapsed sections —
|
|
189
|
+
* that are neither the game's data nor a person's global settings. One
|
|
190
|
+
* section per contribution, named by it.
|
|
191
|
+
*/
|
|
192
|
+
export interface EditorHostProjectLocalState {
|
|
193
|
+
ready(): boolean;
|
|
194
|
+
read<T>(section: string): T | undefined;
|
|
195
|
+
write(section: string, value: unknown): void;
|
|
196
|
+
/** The open project's root path, or null on a tier with none. */
|
|
197
|
+
projectRootPath(): string | null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A LANE that mounts something in the tab — Play, an ingested game, a module
|
|
202
|
+
* world — as the host sees it. A package registers its lane and the host asks
|
|
203
|
+
* only these questions; it never names the lane.
|
|
204
|
+
*/
|
|
205
|
+
export interface LiveSession {
|
|
206
|
+
readonly id: string;
|
|
207
|
+
/** `stop` order among lanes, low first (Play before ingest). */
|
|
208
|
+
readonly priority?: number;
|
|
209
|
+
/** A real game owns a canvas right now. */
|
|
210
|
+
mounted(): boolean;
|
|
211
|
+
/** Somebody asked the host to RUN content; a held mount is not playing. */
|
|
212
|
+
playing(): boolean;
|
|
213
|
+
/** Idempotent; a no-op when the lane runs nothing. */
|
|
214
|
+
stop(): void;
|
|
215
|
+
/** The element holding a live instance (the primary when `id` is omitted). */
|
|
216
|
+
instanceContainer(id?: string): HTMLElement | null;
|
|
217
|
+
/** When this lane's most recent run began / ended (ms epoch); the host
|
|
218
|
+
* fences per-run diagnostics on the newest window across lanes. */
|
|
219
|
+
startedAt?(): number | null;
|
|
220
|
+
endedAt?(): number | null;
|
|
221
|
+
/** Why the running content is stale (a source edit the run cannot absorb),
|
|
222
|
+
* or null while it is fresh; `restart` is the lane's own re-entry. */
|
|
223
|
+
restartRequired?(): string | null;
|
|
224
|
+
restart?(): void;
|
|
225
|
+
/** Re-mount with an authored selection while running; absent when the lane
|
|
226
|
+
* cannot. */
|
|
227
|
+
remount?(args: LiveRemountArgs): Promise<{ ok: true } | { ok: false; error: string }>;
|
|
228
|
+
/** The canvas this lane's own render pass draws, when its pixels can only
|
|
229
|
+
* be read from inside that pass (no `preserveDrawingBuffer`); the host's
|
|
230
|
+
* frame capture asks `snapshotFrame` for it. */
|
|
231
|
+
frameCanvas?(): HTMLCanvasElement | null;
|
|
232
|
+
snapshotFrame?(): Promise<CanvasImageSource | null>;
|
|
233
|
+
/** Why authoring is OFF for this lane's content (an ingested native-React
|
|
234
|
+
* game has no scene graph to introspect), or null when it is on; the
|
|
235
|
+
* inspector prints it in place of its sections. */
|
|
236
|
+
authoringRefusal?(): string | null;
|
|
237
|
+
/** A relayed command this lane answers ITSELF, ahead of every handler —
|
|
238
|
+
* an ingested game owns its mount, so `play`/`stop`/`pause`/`resume`/
|
|
239
|
+
* `step` drive its own loop rather than boot a first-party session over
|
|
240
|
+
* it. Null declines; the host then dispatches as usual. */
|
|
241
|
+
command?(cmd: LiveCommand): LiveCommandResult | null;
|
|
242
|
+
/** The scene entries the running content navigates (a contract game's
|
|
243
|
+
* scene table), for `open-scene` on a running lane. */
|
|
244
|
+
scenes?(): LiveSceneTable | null;
|
|
245
|
+
/** The native surface the running content draws on, when the lane knows
|
|
246
|
+
* it (an ingested game's declared surface); the host's coverage grades
|
|
247
|
+
* a root against it. */
|
|
248
|
+
surface?(): 'three' | 'canvas' | 'dom' | null;
|
|
249
|
+
/** The lane's OWN coverage of the running content's contracts (an
|
|
250
|
+
* ingested game's), shown by the inspector on the live document and
|
|
251
|
+
* standing in for the host's native-system grading while it runs. */
|
|
252
|
+
coverage?(): LiveCoverageReport | null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* What the running content answered about a switch it ACCEPTED — read off its
|
|
257
|
+
* own current scene once the switch settled. `error` is the content's own
|
|
258
|
+
* failure (a refused asset load, a throw), reported rather than swallowed; the
|
|
259
|
+
* scene it is actually in is still reported beside it, because that is the
|
|
260
|
+
* question the caller has next.
|
|
261
|
+
*/
|
|
262
|
+
export interface LiveSceneSwitchSettled {
|
|
263
|
+
readonly requested: string;
|
|
264
|
+
readonly current: string | null;
|
|
265
|
+
readonly error?: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* A scene switch, split at the seam where the answer stops being immediate:
|
|
270
|
+
* MEMBERSHIP is decided synchronously against the content's own scene list,
|
|
271
|
+
* and only an accepted switch has a `settled` promise to await.
|
|
272
|
+
*
|
|
273
|
+
* The split is what lets the caller act on the refusal without waiting, and —
|
|
274
|
+
* for the host's held-mount repaint — start drawing frames the instant a
|
|
275
|
+
* switch is accepted rather than after it lands.
|
|
276
|
+
*/
|
|
277
|
+
export type LiveSceneSwitch =
|
|
278
|
+
| { readonly ok: true; readonly settled: Promise<LiveSceneSwitchSettled> }
|
|
279
|
+
| { readonly ok: false; readonly error: string; readonly known: readonly string[] };
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* A running game's own scene table: its stories, plus the switch the host
|
|
283
|
+
* awaits to open one.
|
|
284
|
+
*
|
|
285
|
+
* `goToScene`'s RESULT is stated here rather than left `unknown` for the
|
|
286
|
+
* editor to narrow (as it was until 2026-09-18). The narrowing lived in a lane
|
|
287
|
+
* module — the ingest lane's scenes projection — so the host's `open` verb had
|
|
288
|
+
* to import that lane by name to know what a switch answers, which is how the
|
|
289
|
+
* ingest contract reached the host's live registry. A contract states its own
|
|
290
|
+
* result; a lane implements it.
|
|
291
|
+
*/
|
|
292
|
+
export interface LiveSceneTable extends StoriesProvider {
|
|
293
|
+
goToScene(sceneId: string): LiveSceneSwitch;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** One seam's verdict in a lane's own coverage report — the doctrine's row
|
|
297
|
+
* (ARCHITECTURE-CORE §Adapters never fabricate first-party data: a gap
|
|
298
|
+
* names the mechanism that fills it). */
|
|
299
|
+
export interface LiveCoverageRow {
|
|
300
|
+
readonly seam: string;
|
|
301
|
+
readonly status: 'ok' | 'gap' | 'na' | 'info';
|
|
302
|
+
readonly detail: string;
|
|
303
|
+
readonly missing?: string;
|
|
304
|
+
readonly fix?: string;
|
|
305
|
+
readonly attestedBy?: 'game' | 'host';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface LiveCoverageReport {
|
|
309
|
+
readonly summary: {
|
|
310
|
+
readonly worldId: string;
|
|
311
|
+
readonly rows: number;
|
|
312
|
+
readonly gaps: number;
|
|
313
|
+
readonly ok: number;
|
|
314
|
+
readonly na: number;
|
|
315
|
+
readonly info: number;
|
|
316
|
+
};
|
|
317
|
+
readonly rows: readonly LiveCoverageRow[];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface LiveCommand {
|
|
321
|
+
readonly type: string;
|
|
322
|
+
readonly [key: string]: unknown;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface LiveCommandResult {
|
|
326
|
+
readonly ok: boolean;
|
|
327
|
+
readonly error?: string;
|
|
328
|
+
readonly data?: Record<string, unknown>;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export interface LiveRunWindow {
|
|
332
|
+
readonly startedAt: number;
|
|
333
|
+
readonly endedAt: number | null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export interface EditorHostLive {
|
|
337
|
+
register(session: LiveSession): () => void;
|
|
338
|
+
mounted(): boolean;
|
|
339
|
+
playing(): boolean;
|
|
340
|
+
/** The newest run across lanes, or null before any ran. */
|
|
341
|
+
runWindow(): LiveRunWindow | null;
|
|
342
|
+
restartRequired(): string | null;
|
|
343
|
+
/** Re-enter whichever lane reports a restart is required (or is running). */
|
|
344
|
+
restart(): void;
|
|
345
|
+
/** Fires on registration and whenever a lane says its state moved
|
|
346
|
+
* (`notifyChanged`). */
|
|
347
|
+
subscribe(listener: () => void): () => void;
|
|
348
|
+
version(): number;
|
|
349
|
+
/** A lane's own state moved (restart-required, run window). */
|
|
350
|
+
notifyChanged(): void;
|
|
351
|
+
/** The mounted lane whose render pass owns `canvas`, asked for its pixels. */
|
|
352
|
+
snapshotFrame(canvas: HTMLCanvasElement): Promise<CanvasImageSource | null> | null;
|
|
353
|
+
frameCanvas(): HTMLCanvasElement | null;
|
|
354
|
+
authoringRefusal(): string | null;
|
|
355
|
+
/** The first mounted lane's own answer to `cmd`, or null when none claims it. */
|
|
356
|
+
dispatch(cmd: LiveCommand): LiveCommandResult | null;
|
|
357
|
+
/** The running lane's scene entries, or null. */
|
|
358
|
+
scenes(): LiveSceneTable | null;
|
|
359
|
+
surface(): 'three' | 'canvas' | 'dom' | null;
|
|
360
|
+
coverage(): LiveCoverageReport | null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** A live instance's roots, presented as the authored viewport's subject. */
|
|
364
|
+
export interface ViewportPresentation {
|
|
365
|
+
/** The root whose native subject the viewport shows. */
|
|
366
|
+
readonly worldId: string;
|
|
367
|
+
/** Restores the viewport's prior subject; idempotent. */
|
|
368
|
+
dispose(): void;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The authored viewport's camera rig, while a viewport is mounted. */
|
|
372
|
+
export interface ViewportRig {
|
|
373
|
+
readonly camera: THREE.PerspectiveCamera;
|
|
374
|
+
readonly orbit: { readonly target: THREE.Vector3; enabled: boolean; update(): void };
|
|
375
|
+
/** The editor's own scene (helpers live on its editor layer). */
|
|
376
|
+
readonly scene: THREE.Scene;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* ONE MOUNTED 3D STAGE, named by the document it draws. Every 3D document
|
|
381
|
+
* mounts a stage of its own (ARCHITECTURE-CORE §One stage); this is the door
|
|
382
|
+
* to a particular one, where {@link EditorHostViewport}'s singular members
|
|
383
|
+
* are the door to whichever is primary.
|
|
384
|
+
*/
|
|
385
|
+
export interface EditorHostStage {
|
|
386
|
+
/** The workspace document this stage draws. */
|
|
387
|
+
readonly documentId: string;
|
|
388
|
+
rig(): ViewportRig;
|
|
389
|
+
/** As {@link EditorHostViewport.setHelper}, on this stage alone. */
|
|
390
|
+
setHelper(kind: string, object: THREE.Object3D | null): void;
|
|
391
|
+
/** Runs after THIS stage's own per-frame update; the return unsubscribes. */
|
|
392
|
+
onFrame(fn: (dtSeconds: number) => void): () => void;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* THE AUTHORED VIEWPORT, as a lane that mounts something reaches it: the rig
|
|
397
|
+
* a camera flight drives, the frame loop it rides, and the one verb that
|
|
398
|
+
* swaps the viewport's subject for a live instance's roots (Play's adoption
|
|
399
|
+
* of the running scene). Which native surface the viewport renders is the
|
|
400
|
+
* host's; a lane that finds no rig (a headless tab, a project with no Three
|
|
401
|
+
* root) mounts without one.
|
|
402
|
+
*
|
|
403
|
+
* THE SINGULAR MEMBERS ARE THE PRIMARY STAGE — the stage presenting live
|
|
404
|
+
* roots if one is, else the FOCUSED stage, else the first bound. A camera
|
|
405
|
+
* flight and Play's adoption want exactly that one, which is why they read
|
|
406
|
+
* here and never enumerate.
|
|
407
|
+
*
|
|
408
|
+
* {@link stages} is EVERY mounted 3D stage. A helper is an `Object3D` and an
|
|
409
|
+
* `Object3D` has one parent, so there is no "set it once for all stages": a
|
|
410
|
+
* helper wanted on every stage is set on each stage through `stages()`, and
|
|
411
|
+
* `onStages` is how a lane keeps up with stages that mount and unmount after
|
|
412
|
+
* it made that pass.
|
|
413
|
+
*/
|
|
414
|
+
/** A mounted adapter surface offered to the viewport. Execution, input and
|
|
415
|
+
* ticking stay with the caller; presentation requires only identity and mount.
|
|
416
|
+
* A game's richer root instance can satisfy this interface directly.
|
|
417
|
+
*/
|
|
418
|
+
export interface ViewportRoot {
|
|
419
|
+
readonly id: string;
|
|
420
|
+
readonly mounted: MountedRoot;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface EditorHostViewport {
|
|
424
|
+
rig(): ViewportRig | null;
|
|
425
|
+
/** Null when the viewport has no subject among `roots` (no Three root). */
|
|
426
|
+
presentRoots(roots: readonly ViewportRoot[]): ViewportPresentation | null;
|
|
427
|
+
/** Runs after the viewport's own per-frame update; the return unsubscribes. */
|
|
428
|
+
onFrame(fn: (dtSeconds: number) => void): () => void;
|
|
429
|
+
/**
|
|
430
|
+
* Show an editor-only helper object of `kind` in the authored viewport (a
|
|
431
|
+
* baked navmesh's debug mesh), replacing the previous one of that kind;
|
|
432
|
+
* null clears it. Visibility follows the host's Helpers menu: a kind the
|
|
433
|
+
* menu lists (`navmesh`) toggles on its own, any other follows the master
|
|
434
|
+
* toggle. Kept across viewport remounts.
|
|
435
|
+
*/
|
|
436
|
+
setHelper(kind: string, object: THREE.Object3D | null): void;
|
|
437
|
+
/** Every mounted 3D stage, in bind order. */
|
|
438
|
+
stages(): readonly EditorHostStage[];
|
|
439
|
+
/** Fires whenever a stage mounts or unmounts; the return unsubscribes. */
|
|
440
|
+
onStages(fn: (stages: readonly EditorHostStage[]) => void): () => void;
|
|
441
|
+
readonly transition: EditorHostLiveTransition;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* THE LIVE TRANSITION — the host's hand-off from authoring chrome to a
|
|
446
|
+
* running lane: under the immersive presentation the dock dissolves, the
|
|
447
|
+
* authored viewport's camera flies to the authored game camera, and when the
|
|
448
|
+
* lane reports ready the Scene document cross-fades into the live one. The
|
|
449
|
+
* host decides whether the presentation is immersive and resolves the flight
|
|
450
|
+
* target itself; a lane only says when it starts, when it is ready, and when
|
|
451
|
+
* it ends. Every path out of a run must reach `end()`.
|
|
452
|
+
*/
|
|
453
|
+
export interface EditorHostLiveTransition {
|
|
454
|
+
/** Call BEFORE flipping the session to playing: the flight target is read
|
|
455
|
+
* from the authored scene, which Play's own adoption then replaces. */
|
|
456
|
+
begin(): void;
|
|
457
|
+
/** The lane finished its async boot; `getLiveCamera` is the render camera
|
|
458
|
+
* the flight converges on so the cross-fade is pixel-continuous. */
|
|
459
|
+
ready(getLiveCamera?: () => THREE.Object3D | null): void;
|
|
460
|
+
end(): void;
|
|
461
|
+
/** Once the entry settles (cross-fade done, or torn down early) — at once
|
|
462
|
+
* when none is in flight. One-shot; re-check session state inside. */
|
|
463
|
+
onSettled(fn: () => void): void;
|
|
464
|
+
phase(): 'idle' | 'entering' | 'holding' | 'crossfade' | 'playing';
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** A lane's answer to the host's "re-mount with this selection" (an authored
|
|
468
|
+
* scene entry opened while the lane runs). */
|
|
469
|
+
export interface LiveRemountArgs {
|
|
470
|
+
readonly selection: string;
|
|
471
|
+
readonly key: string;
|
|
472
|
+
readonly regionId: string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* THE LIVE OBJECTS BY NODE ID — the world node IS the entity (CLAUDE.md), so
|
|
477
|
+
* a contribution inspecting a live behavior (an XState machine on an
|
|
478
|
+
* object's userData) resolves the hierarchy's node id to the object itself.
|
|
479
|
+
* The map is the authored viewport's in Edit and the adopted live scene's
|
|
480
|
+
* in Play; a reader must not hold an object across `subscribe` firings.
|
|
481
|
+
*/
|
|
482
|
+
export interface EditorHostHierarchy {
|
|
483
|
+
object(id: string): THREE.Object3D | null;
|
|
484
|
+
objects(): ReadonlyMap<string, THREE.Object3D>;
|
|
485
|
+
/** Fires on any shell-store change (membership included). */
|
|
486
|
+
subscribe(listener: () => void): () => void;
|
|
487
|
+
version(): number;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** The editor's own session state a contribution may read. */
|
|
491
|
+
export interface EditorHostSession {
|
|
492
|
+
/**
|
|
493
|
+
* Contribute fields to the editor's state report (`vgai status`, the SDK's
|
|
494
|
+
* `editor.state`): the collect runs on every report and its keys are
|
|
495
|
+
* spread in. A lane reports what only it knows — its loop's time scale and
|
|
496
|
+
* liveness, its seed — where the host reports the session. Returns the
|
|
497
|
+
* unregister.
|
|
498
|
+
*
|
|
499
|
+
* EVERY registered collect runs on EVERY report, including the interaction
|
|
500
|
+
* path's reusing one — so a facet whose derivation is expensive declares its
|
|
501
|
+
* `reusableKeys` and reads them back off the `reuse` snapshot it is handed.
|
|
502
|
+
* That is what let the COVERAGE REPORT families (`rootCoverage`,
|
|
503
|
+
* `systemCoverage`, `projectCoverage`, `authoringCoverage`) stop being host
|
|
504
|
+
* fields: their 76ms-to-1.3s derivation is exactly what
|
|
505
|
+
* `command-listener.ts`'s `REUSABLE_DERIVED_FACETS` exists to keep off a
|
|
506
|
+
* store notification, and the host's own docblock says the choice is the
|
|
507
|
+
* CALLER's — "only the caller knows whether it is on a user's critical
|
|
508
|
+
* path" — so no facet-side cache could have been the same answer. The
|
|
509
|
+
* declared keys join that set: the host strips them from an interaction
|
|
510
|
+
* PATCH by name (`currentStatePatch`), and the deferred full collect that
|
|
511
|
+
* always follows makes them current again.
|
|
512
|
+
*/
|
|
513
|
+
reportFacet(
|
|
514
|
+
collect: (reuse: Record<string, unknown> | null) => Record<string, unknown>,
|
|
515
|
+
options?: { readonly reusableKeys?: readonly string[] },
|
|
516
|
+
): () => void;
|
|
517
|
+
/**
|
|
518
|
+
* THE SESSION'S PERIODIC SAMPLE — the host's own five-second vitals tick
|
|
519
|
+
* (`coverage/session-vitals.ts`), which a lane may hang a periodic
|
|
520
|
+
* derivation of its own on. Returns the unsubscribe.
|
|
521
|
+
*
|
|
522
|
+
* A lane that wants "every few seconds, look at the session and say
|
|
523
|
+
* something" subscribes HERE rather than starting a second interval: the
|
|
524
|
+
* vitals sampler already owns the cadence, already runs on every realm, and
|
|
525
|
+
* a package-owned timer beside it would sample the same session at a
|
|
526
|
+
* different instant and report two answers for one moment. Listeners run
|
|
527
|
+
* before the host's own reveal failsafe and invariant report, which is the
|
|
528
|
+
* order the coverage union held when it was a host call on this tick.
|
|
529
|
+
*/
|
|
530
|
+
onSample(fn: () => void): () => void;
|
|
531
|
+
/** Every relayed command, by type, as it is dispatched — the signal an
|
|
532
|
+
* idle watchdog reads ("an agent still driving through `vgai eval` is
|
|
533
|
+
* not idle"). Returns the unsubscribe. */
|
|
534
|
+
onCommandDispatched(fn: (type: string) => void): () => void;
|
|
535
|
+
playState(): 'stopped' | 'playing' | 'paused';
|
|
536
|
+
/** `'ephemeral'` while Play holds edits that will not persist; null otherwise. */
|
|
537
|
+
playEditRegime(): 'ephemeral' | null;
|
|
538
|
+
/** Fires on any shell-store change; select what you read. */
|
|
539
|
+
subscribe(listener: () => void): () => void;
|
|
540
|
+
version(): number;
|
|
541
|
+
/**
|
|
542
|
+
* Whether a PROJECT SESSION is open at all — the editor has a project and the
|
|
543
|
+
* shell that edits it, so a document can be opened and something can be
|
|
544
|
+
* presented into it.
|
|
545
|
+
*
|
|
546
|
+
* NEW (2026-09-19). `@vgai/blender` refuses every verb but its own status
|
|
547
|
+
* read without one, so that a call arriving at a session-less page answers at
|
|
548
|
+
* once instead of booting a Blender worker (gigabytes) into a page with
|
|
549
|
+
* nowhere to show it. It took the same answer from the host's
|
|
550
|
+
* `@editor/shell-store-door`; this is the question, without the store.
|
|
551
|
+
*/
|
|
552
|
+
open(): boolean;
|
|
553
|
+
/**
|
|
554
|
+
* THIS PAGE'S SESSION ENDED — the tombstone every end goes through, graceful
|
|
555
|
+
* (`vgai close`) or not (the server died, another session took the port).
|
|
556
|
+
* Returns the unsubscribe.
|
|
557
|
+
*
|
|
558
|
+
* NEW (2026-09-19), and it is a RELEASE hook: a page told `tab-close` keeps
|
|
559
|
+
* running (Chrome refuses `window.close()` for a tab a person opened), so a
|
|
560
|
+
* package holding something the page cannot pay for holds it forever. The
|
|
561
|
+
* measurement that bought it: two orphaned editor tabs held 13 GB and 7 GB of
|
|
562
|
+
* resident Blender worker between them and put the box into a swap storm.
|
|
563
|
+
* A lane that owns a worker, a socket or a device ends it here.
|
|
564
|
+
*/
|
|
565
|
+
onEnded(fn: () => void): () => void;
|
|
566
|
+
/**
|
|
567
|
+
* PUBLISH (or retract, with null) THIS LANE'S OUT-OF-PROCESS WORKER METER, so
|
|
568
|
+
* the tab census carries it out on the heartbeat — the one channel that still
|
|
569
|
+
* beats through a blocked main thread, which is why `reportFacet` above
|
|
570
|
+
* cannot answer this: a wedged tab is exactly the tab whose state report
|
|
571
|
+
* never arrives.
|
|
572
|
+
*
|
|
573
|
+
* A READ rather than a snapshot: an outstanding call's age has to be computed
|
|
574
|
+
* at the instant it is reported, and the package is the side that has the
|
|
575
|
+
* clock (a blocked worker cannot report on itself, and the side that POSTED
|
|
576
|
+
* the call still knows when it did).
|
|
577
|
+
*
|
|
578
|
+
* The HOST owns the rest of the measurement — the main thread's own long
|
|
579
|
+
* tasks, and which of them overlapped the call — and starts measuring when a
|
|
580
|
+
* meter arrives. That half was `@editor/blender-tab-metrics`, a host module
|
|
581
|
+
* the package used to start and publish into; the page's stalls were never
|
|
582
|
+
* the package's to observe. ONE meter rides the census, so a second lane
|
|
583
|
+
* publishing here replaces the first.
|
|
584
|
+
*/
|
|
585
|
+
reportWorkerCallMeter(read: (() => EditorHostWorkerCallMetrics) | null): void;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* A LANE'S WORKER CALLS, as numbers — what the tab census carries so that
|
|
590
|
+
* `vgai status` can say a tab stopped answering and why.
|
|
591
|
+
*
|
|
592
|
+
* Times are milliseconds on `performance.now()`; counters are monotonic since
|
|
593
|
+
* the lane's runtime was constructed. MEASUREMENT ONLY: nothing here cancels,
|
|
594
|
+
* kills or budgets a call.
|
|
595
|
+
*/
|
|
596
|
+
export interface EditorHostWorkerCallMetrics {
|
|
597
|
+
/** Age of the OLDEST outstanding call, or null when the worker is idle — the
|
|
598
|
+
* only field with a number during a wedge. */
|
|
599
|
+
readonly inFlightMs: number | null;
|
|
600
|
+
/** Duration of the newest completed call; null before the first one. */
|
|
601
|
+
readonly lastCallMs: number | null;
|
|
602
|
+
/** The longest call yet, counting an outstanding one at its current age. */
|
|
603
|
+
readonly maxCallMs: number | null;
|
|
604
|
+
/** Calls past 5s, and past 30s, since the runtime was constructed. */
|
|
605
|
+
readonly callsOver5s: number;
|
|
606
|
+
readonly callsOver30s: number;
|
|
607
|
+
/** The newest call's window (`end` null while it is outstanding); the host
|
|
608
|
+
* intersects its own long tasks with it. */
|
|
609
|
+
readonly lastCallWindow: { readonly start: number; readonly end: number | null } | null;
|
|
610
|
+
/** The lane's own out-of-process memory in MB (a wasm module's linear
|
|
611
|
+
* memory), or null when it has none to report. */
|
|
612
|
+
readonly wasmMemoryMB: number | null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** The open project's declared SHAPE, as a contribution may gate on it. */
|
|
616
|
+
export interface EditorHostProject {
|
|
617
|
+
/** Whether the project declares at least one root that plays. */
|
|
618
|
+
mounts(): boolean;
|
|
619
|
+
/**
|
|
620
|
+
* The engine version the OPEN project is pinned to
|
|
621
|
+
* (`vgai.project.json`'s `engine.version`), or null when no project is
|
|
622
|
+
* open — the same one source `ProjectHeader.tsx` renders, so a package's
|
|
623
|
+
* version readout can never disagree with the host's.
|
|
624
|
+
*
|
|
625
|
+
* A primitive rather than the project object on purpose: `ActiveProject`
|
|
626
|
+
* is a host internal, and the SDK's door grows one member per
|
|
627
|
+
* contribution that needs it (`@vgai/blender`'s `workspace.status` version
|
|
628
|
+
* item is the reader). Paired with {@link subscribe}, this is the whole
|
|
629
|
+
* "current project + change" the door owes a contribution — and it is
|
|
630
|
+
* stable enough for `useSyncExternalStore` without a snapshot cache.
|
|
631
|
+
*/
|
|
632
|
+
engineVersion(): string | null;
|
|
633
|
+
subscribe(listener: () => void): () => void;
|
|
634
|
+
/**
|
|
635
|
+
* Runs once the open project's authoring surfaces are READY — its scene
|
|
636
|
+
* loaded and the edit-mode composite installed — the moment a lane may
|
|
637
|
+
* auto-launch what the manifest declares (an ingest root). Listeners run
|
|
638
|
+
* in registration order, each awaited; a project opened later fires it
|
|
639
|
+
* again. Returns the unsubscribe.
|
|
640
|
+
*/
|
|
641
|
+
onReady(fn: () => void | Promise<void>): () => void;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export interface EditorHostNotification {
|
|
645
|
+
/** A stable id replaces an earlier notification with the same id. */
|
|
646
|
+
readonly id?: string;
|
|
647
|
+
readonly tone: 'info' | 'warning' | 'error';
|
|
648
|
+
/** One line, bold — what happened. */
|
|
649
|
+
readonly title: string;
|
|
650
|
+
/** The rest, plain — what it means, what to do. */
|
|
651
|
+
readonly detail?: string;
|
|
652
|
+
readonly actions?: readonly {
|
|
653
|
+
readonly label: string;
|
|
654
|
+
readonly run: () => void;
|
|
655
|
+
readonly primary?: boolean;
|
|
656
|
+
}[];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** The editor's console — the session-held set `vgai console` prints. A
|
|
660
|
+
* contribution's diagnostics go here, never to `console.*`, so they reach
|
|
661
|
+
* every door whether or not anyone looks at the tab. */
|
|
662
|
+
export interface EditorHostConsole {
|
|
663
|
+
log(message: string, source: string): void;
|
|
664
|
+
warn(message: string, source: string): void;
|
|
665
|
+
error(message: string, source: string): void;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* THE SETTINGS DOOR — one dotted `vgai.*` key at a time, with the LAYER each
|
|
670
|
+
* value came from, and the one write that lands where it wins.
|
|
671
|
+
*
|
|
672
|
+
* ARCHITECTURE-CORE §The core is Code-OSS: *"the settings layers and settings
|
|
673
|
+
* UI → the configuration service (the ADAPTER layer between user and
|
|
674
|
+
* workspace … is the one addition)"*. The same two-owner shape as
|
|
675
|
+
* {@link EditorHostKeyboard}, {@link EditorHostHistory} and
|
|
676
|
+
* {@link EditorHostFiles}: `'host'` is standalone `vgai edit`, where
|
|
677
|
+
* `settings-store.ts`'s three layers ARE the settings; `'frame'` is the
|
|
678
|
+
* Code-OSS frame, where `IConfigurationService` is.
|
|
679
|
+
*
|
|
680
|
+
* ## The keys are `vgai.*`, and the prefix is part of the key
|
|
681
|
+
*
|
|
682
|
+
* `vgai.appearance.palette`, `vgai.keymap`, `vgai.devicePreview.preset` — the
|
|
683
|
+
* flat dotted names `@volter/editor-project/settings/keys` derives from the settings
|
|
684
|
+
* schema, which is also what the fork's `contributes.configuration` is
|
|
685
|
+
* generated from. One spelling in this door, in `.vscode/settings.json`, in
|
|
686
|
+
* VS Code's Settings editor and in what `vgai eval` prints, because the moment
|
|
687
|
+
* there are two a reader has to know which side of which seam they are on to
|
|
688
|
+
* know which to type.
|
|
689
|
+
*
|
|
690
|
+
* ## The adapter layer, and why `inspect` names it
|
|
691
|
+
*
|
|
692
|
+
* "Project over ADAPTER over user" (§Adapters and contributions are code) is
|
|
693
|
+
* the one thing the configuration service does not already have, and under the
|
|
694
|
+
* frame it is the service's own MEMORY target — the top layer — written by the
|
|
695
|
+
* fork when the project's adapter loads and cleared the moment `inspect` shows
|
|
696
|
+
* a workspace or folder value for that key. So `inspect(key)` answers with the
|
|
697
|
+
* FOUR layers a person can act on, and a caller that wants to know whether a
|
|
698
|
+
* gesture will stick asks it rather than guessing from the effective value.
|
|
699
|
+
*/
|
|
700
|
+
export interface EditorHostSettings {
|
|
701
|
+
/**
|
|
702
|
+
* Install VS Code's configuration service as the settings. The frame calls
|
|
703
|
+
* this once its own services exist, which is AFTER the editor mounts (a
|
|
704
|
+
* `ServicesAccessor` is valid only for the synchronous part of an
|
|
705
|
+
* invocation). Until it does, the door falls back to the editor's own layers
|
|
706
|
+
* rather than refusing: the editor paints in that window, and a palette read
|
|
707
|
+
* there is a real read with nowhere else to go.
|
|
708
|
+
*/
|
|
709
|
+
setProvider(provider: EditorHostSettingsProvider): void;
|
|
710
|
+
/** The EFFECTIVE value — project over adapter over user over default — or
|
|
711
|
+
* `undefined` when no layer carries it. */
|
|
712
|
+
get(key: string): unknown;
|
|
713
|
+
/**
|
|
714
|
+
* Write one key.
|
|
715
|
+
*
|
|
716
|
+
* With no `target`, the write goes WHERE IT WINS: the project when this
|
|
717
|
+
* project's adapter or its own settings already declare the key, the user
|
|
718
|
+
* layer otherwise. That is the whole of `updatePreferenceSettings`'s rule,
|
|
719
|
+
* and it is here rather than in each caller because writing `appearance` to
|
|
720
|
+
* the user layer in a project whose adapter declares a style is a gesture
|
|
721
|
+
* that silently does nothing.
|
|
722
|
+
*/
|
|
723
|
+
set(key: string, value: unknown, target?: EditorHostSettingsTarget): void;
|
|
724
|
+
/** Every layer's own value for this key, plus the effective one. A layer
|
|
725
|
+
* that is silent about the key answers `undefined` — never the value from
|
|
726
|
+
* the layer under it. */
|
|
727
|
+
inspect(key: string): EditorHostSettingsInspection;
|
|
728
|
+
subscribe(listener: () => void): () => void;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** The two layers a person's gesture can land in. The adapter layer is the
|
|
732
|
+
* project's own CODE and the default layer is the build's, so neither is a
|
|
733
|
+
* write target. */
|
|
734
|
+
export type EditorHostSettingsTarget = 'user' | 'project';
|
|
735
|
+
|
|
736
|
+
export interface EditorHostSettingsInspection {
|
|
737
|
+
/** The built-in value, when the layer that declares the key carries one.
|
|
738
|
+
* The standalone layers carry none, so this is `undefined` there. */
|
|
739
|
+
readonly default: unknown;
|
|
740
|
+
/** `~/.vgai/settings.json` standalone; the USER target under the frame. */
|
|
741
|
+
readonly user: unknown;
|
|
742
|
+
/** What `vgai.adapter.ts` DECLARES (`editor: { style, keymap }`); the
|
|
743
|
+
* MEMORY target under the frame. */
|
|
744
|
+
readonly adapter: unknown;
|
|
745
|
+
/** `<project>/.vgai/settings.json` standalone; the WORKSPACE (and folder)
|
|
746
|
+
* target under the frame. */
|
|
747
|
+
readonly project: unknown;
|
|
748
|
+
/** Project over adapter over user over default. */
|
|
749
|
+
readonly effective: unknown;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* THE FRAME'S HALF — what the Code-OSS bridge installs, backed by
|
|
754
|
+
* `IConfigurationService`. Keys are the same `vgai.*` names the door takes.
|
|
755
|
+
*
|
|
756
|
+
* There is no `owner`/`setOwner` here and no optional member: unlike
|
|
757
|
+
* {@link EditorHostFileProvider}, a configuration service can answer every one
|
|
758
|
+
* of these for every key, so a member the frame "cannot answer" would be a
|
|
759
|
+
* defect rather than a shape.
|
|
760
|
+
*/
|
|
761
|
+
export interface EditorHostSettingsProvider {
|
|
762
|
+
get(key: string): unknown;
|
|
763
|
+
inspect(key: string): EditorHostSettingsInspection;
|
|
764
|
+
set(key: string, value: unknown, target: EditorHostSettingsTarget): void;
|
|
765
|
+
/** Fires when any `vgai.*` value changes in any layer. Returns the
|
|
766
|
+
* unsubscribe. */
|
|
767
|
+
subscribe(listener: () => void): () => void;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* THE KEYBOARD DOOR — who owns the keyboard, and the chord-independent table
|
|
772
|
+
* of what the editor's keyboard actions DO.
|
|
773
|
+
*
|
|
774
|
+
* ARCHITECTURE-CORE §The core is Code-OSS rule 3: *"Keyboard ownership is VS
|
|
775
|
+
* Code's. One keybinding system … Two listeners cannot both own the
|
|
776
|
+
* keyboard."* Under the Code-OSS frame the workbench's keybinding service is
|
|
777
|
+
* the one keyboard: the fork's contribution registers one `vgai.<action id>`
|
|
778
|
+
* command per entry of `actions()`, gives each the chords `keymaps()` reports
|
|
779
|
+
* under a `when` clause over its own context keys, and dispatches through
|
|
780
|
+
* `invoke`. The editor installs no `keydown` listener of its own at all.
|
|
781
|
+
*/
|
|
782
|
+
export interface EditorHostKeyboard {
|
|
783
|
+
/**
|
|
784
|
+
* Every action the editor has a live handler for right now, with the scope
|
|
785
|
+
* its chord belongs to — `'stage'` (the focused stage alone), `'panel'`
|
|
786
|
+
* (any of the editor's own parts) or `'global'`. The viewport set appears
|
|
787
|
+
* only while a three stage is mounted, so this is a live list, not a
|
|
788
|
+
* catalogue; `subscribe`/`version` report when it moves.
|
|
789
|
+
*/
|
|
790
|
+
actions(): readonly { readonly id: string; readonly scope: 'stage' | 'panel' | 'global' }[];
|
|
791
|
+
/**
|
|
792
|
+
* Every registered keymap and the chords it assigns each action — the
|
|
793
|
+
* editor's own `vgai` table and whatever a project's packages contribute
|
|
794
|
+
* (Blender's G/R/S). A keymap is a SET of keybinding rules to the frame:
|
|
795
|
+
* the same commands, different chords, gated on `activeKeymap()`.
|
|
796
|
+
*/
|
|
797
|
+
keymaps(): readonly {
|
|
798
|
+
readonly id: string;
|
|
799
|
+
readonly title: string;
|
|
800
|
+
readonly chords: Readonly<
|
|
801
|
+
Record<
|
|
802
|
+
string,
|
|
803
|
+
readonly {
|
|
804
|
+
readonly key: string;
|
|
805
|
+
readonly code?: string;
|
|
806
|
+
readonly mod?: boolean;
|
|
807
|
+
readonly shift?: boolean;
|
|
808
|
+
readonly alt?: boolean;
|
|
809
|
+
}[]
|
|
810
|
+
>
|
|
811
|
+
>;
|
|
812
|
+
}[];
|
|
813
|
+
/** The keymap the PROJECT selected (its adapter's `editor.keymap`, its own
|
|
814
|
+
* settings over it). The frame publishes it as a context key and never
|
|
815
|
+
* keeps a second setting of its own. */
|
|
816
|
+
activeKeymap(): string;
|
|
817
|
+
/** Run one action by id. `false` when nothing handles it now, or its own
|
|
818
|
+
* gate refused — never silent. */
|
|
819
|
+
invoke(id: string): boolean;
|
|
820
|
+
subscribe(listener: () => void): () => void;
|
|
821
|
+
version(): number;
|
|
822
|
+
/**
|
|
823
|
+
* WHAT THE FOCUSED STAGE IS SHOWING, for the frame's context keys. It is
|
|
824
|
+
* here rather than beside `viewport` because it exists for exactly one
|
|
825
|
+
* reader: the `when` clauses that decide which keyboard action a chord
|
|
826
|
+
* reaches. `surface` is the stage's (`stage-context.ts`); `mode` is the
|
|
827
|
+
* document's own interaction mode when it reports one (Blender's
|
|
828
|
+
* object/edit/sculpt) and `null` when nothing does.
|
|
829
|
+
*/
|
|
830
|
+
stage(): {
|
|
831
|
+
readonly surface: 'three' | 'canvas' | 'dom' | null;
|
|
832
|
+
readonly mode: string | null;
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* ONE RECORDED EDIT, as whoever owns undo sees it — the SDK's spelling of
|
|
838
|
+
* `packages/editor/src/history/history-delegate.ts`'s `HistoryElement`.
|
|
839
|
+
*
|
|
840
|
+
* It is deliberately an `IResourceUndoRedoElement` (one file) or an
|
|
841
|
+
* `IWorkspaceUndoRedoElement` (several) without naming either: the frame does
|
|
842
|
+
* that translation, so no editor module imports VS Code and no file under the
|
|
843
|
+
* fork's `src/vs/` imports an editor module.
|
|
844
|
+
*/
|
|
845
|
+
export interface EditorHostHistoryElement {
|
|
846
|
+
readonly id: string;
|
|
847
|
+
/** User-presentable, already trimmed ("Transform Selection"). This is what
|
|
848
|
+
* the frame's Edit menu shows after "Undo". */
|
|
849
|
+
readonly label: string;
|
|
850
|
+
/**
|
|
851
|
+
* The PROJECT-RELATIVE files this edit changed, in the order the
|
|
852
|
+
* transaction declared them — `src/prefabs/Crate.tsx`, not a URI and not an
|
|
853
|
+
* opaque key, because only the frame knows the workspace folder they
|
|
854
|
+
* resolve against, and resolving them there is what puts a gizmo drag and a
|
|
855
|
+
* keystroke in the same file's text editor on ONE resource's stack.
|
|
856
|
+
*
|
|
857
|
+
* EMPTY for a session-scoped edit (a live journal on a held canvas surface,
|
|
858
|
+
* a play run), which has no file at all. Placing such an element is the
|
|
859
|
+
* frame's decision, named there — never silently attached to whatever
|
|
860
|
+
* document happened to be open.
|
|
861
|
+
*/
|
|
862
|
+
readonly resources: readonly string[];
|
|
863
|
+
/**
|
|
864
|
+
* THE WORKSPACE DOCUMENT THIS EDIT WAS MADE IN, at the moment it was
|
|
865
|
+
* recorded — the id, or null when nothing was active.
|
|
866
|
+
*
|
|
867
|
+
* MEASURED (2026-09-19, the game template inside the frame): a three root's
|
|
868
|
+
* document is NOT one file. Its adapter's own source path is the root entry
|
|
869
|
+
* `src/world.tsx`, while a gizmo drag on the scene's HeroBox instance writes
|
|
870
|
+
* `src/scenes/MainScene.tsx` — so "the document's resource" is a SET that
|
|
871
|
+
* grows with what the person edits, and asking the undo service about the
|
|
872
|
+
* entry file alone would find nothing to undo. The document id is the stable
|
|
873
|
+
* thing; WHICH file its next undo acts on is the newest element recorded in
|
|
874
|
+
* it. It is also what keeps a component view's stack apart from the main
|
|
875
|
+
* scene's.
|
|
876
|
+
*/
|
|
877
|
+
readonly document: string | null;
|
|
878
|
+
/** Revert this one entry. `false` when the editor refused (a conflict, an
|
|
879
|
+
* expired resource, blocked history) — never a silent no-op. */
|
|
880
|
+
undo(): Promise<boolean>;
|
|
881
|
+
redo(): Promise<boolean>;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* UNDO, for the Code-OSS frame (ARCHITECTURE-CORE §The core is Code-OSS:
|
|
886
|
+
* *"history-service.ts → IUndoRedoService … there is one Cmd+Z"*).
|
|
887
|
+
*
|
|
888
|
+
* The same two halves as {@link EditorHostKeyboard}, for the same reason: the
|
|
889
|
+
* STANDALONE `vgai edit` shape fills this with the editor's own
|
|
890
|
+
* `history-service.ts` cursor, and the FRAME takes ownership before the editor
|
|
891
|
+
* mounts and pushes every {@link EditorHostHistoryElement} into VS Code's
|
|
892
|
+
* `IUndoRedoService` instead. Nothing here caps anything by bytes — snapshot
|
|
893
|
+
* size stays the adapter's concern, stated where the snapshot is taken.
|
|
894
|
+
*/
|
|
895
|
+
/** The frame's own undo, for every editor affordance that is not a chord. */
|
|
896
|
+
export interface EditorHostHistoryDelegate {
|
|
897
|
+
undo(): void;
|
|
898
|
+
redo(): void;
|
|
899
|
+
canUndo(): boolean;
|
|
900
|
+
canRedo(): boolean;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export interface EditorHostHistory {
|
|
904
|
+
/**
|
|
905
|
+
* Install the frame's own undo as the one stack. Called once its service
|
|
906
|
+
* exists, which is AFTER the editor mounts.
|
|
907
|
+
*
|
|
908
|
+
* The delegate is the OTHER direction of this door: the editor has undo
|
|
909
|
+
* affordances that are not the keyboard — its Edit menu's "Undo <label>",
|
|
910
|
+
* the command palette, `vgai eval`'s undo verb — and every one of them must
|
|
911
|
+
* reach the ONE stack. Without it the Edit menu still names the step (the
|
|
912
|
+
* label comes from the last recorded entry) while the click refuses, which
|
|
913
|
+
* is worse than no menu item at all.
|
|
914
|
+
*/
|
|
915
|
+
setDelegate(delegate: EditorHostHistoryDelegate): void;
|
|
916
|
+
/**
|
|
917
|
+
* Every entry as it is recorded, once a delegate is installed. Returns the
|
|
918
|
+
* removal. Pair it with {@link elements}: the bridge mounts after edits are
|
|
919
|
+
* already possible, so it pushes what it missed first, in order.
|
|
920
|
+
*/
|
|
921
|
+
onElement(listener: (element: EditorHostHistoryElement) => void): () => void;
|
|
922
|
+
/** Everything recorded so far, oldest first. */
|
|
923
|
+
elements(): readonly EditorHostHistoryElement[];
|
|
924
|
+
/**
|
|
925
|
+
* THE FOCUSED DOCUMENT'S OWN FILE, project-relative — the first resource a
|
|
926
|
+
* ⌘Z with focus on a vgai stage tries, and the one a refusal names. `null`
|
|
927
|
+
* when nothing is open and the active adapter writes nowhere.
|
|
928
|
+
*
|
|
929
|
+
* It is deliberately NOT the whole answer, because a three root's document
|
|
930
|
+
* spans several files (see {@link EditorHostHistoryElement.document}); the
|
|
931
|
+
* frame falls back to the newest element recorded IN THAT DOCUMENT, which is
|
|
932
|
+
* what keeps a component view's ⌘Z off the main scene's stack — the whole
|
|
933
|
+
* of "Component-view Ctrl+Z acts on the MAIN scene's history" (WORK.md
|
|
934
|
+
* §The core is Code-OSS, U4's absorb list).
|
|
935
|
+
*/
|
|
936
|
+
focusedResource(): string | null;
|
|
937
|
+
/** The HOST shape's undo/redo — the editor's own cursor. Under frame
|
|
938
|
+
* ownership these refuse by name; the frame drives elements instead. */
|
|
939
|
+
undo(): Promise<boolean>;
|
|
940
|
+
redo(): Promise<boolean>;
|
|
941
|
+
canUndo(): boolean;
|
|
942
|
+
canRedo(): boolean;
|
|
943
|
+
/** What the next undo/redo would be called, for a menu that shows it. */
|
|
944
|
+
undoLabel(): string | null;
|
|
945
|
+
redoLabel(): string | null;
|
|
946
|
+
subscribe(listener: () => void): () => void;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* THE FILE DOOR — reading and writing the OPEN PROJECT'S OWN FILES, over
|
|
951
|
+
* project-relative paths, with the same two-owner shape as
|
|
952
|
+
* {@link EditorHostKeyboard} and {@link EditorHostHistory}.
|
|
953
|
+
*
|
|
954
|
+
* ARCHITECTURE-CORE §The core is Code-OSS: *"the storage backends → file
|
|
955
|
+
* system providers, the dev server as one provider"* — and the rule above it
|
|
956
|
+
* that governs HOW: **everything VS Code already does is USED, not rebuilt.**
|
|
957
|
+
*
|
|
958
|
+
* ## Why this is a CALL and not a file-system provider (U5, measured)
|
|
959
|
+
*
|
|
960
|
+
* Both product shapes already have a real file service over the project:
|
|
961
|
+
* DESKTOP opens the project folder as the workspace folder on Electron's own
|
|
962
|
+
* disk provider, and WEB + SERVER (the REH) serves the same folder over
|
|
963
|
+
* `vscode-remote://`. A `vgai-session:` provider mounting the session's
|
|
964
|
+
* `/__editor/*` routes would be a SECOND path to bytes the workbench can
|
|
965
|
+
* already reach — more code, a second cache, and two notions of when a file
|
|
966
|
+
* changed. So under the frame this door CALLS `IFileService` (and
|
|
967
|
+
* `ITextFileService` for text), and the session's file routes stay exactly
|
|
968
|
+
* what the STANDALONE shape speaks.
|
|
969
|
+
*
|
|
970
|
+
* ## Why the frame's write is the point (U4's open, closed here)
|
|
971
|
+
*
|
|
972
|
+
* A vgai element's REDO used to be lost while a text model for that file was
|
|
973
|
+
* open: the editor's undo wrote the file through its own transport, which is
|
|
974
|
+
* an EXTERNAL change to the workbench, so Monaco reloaded and
|
|
975
|
+
* `modelService.updateModel` pushed a fresh text element — and `pushElement`
|
|
976
|
+
* destroys the redo future. A write made THROUGH the workbench is the
|
|
977
|
+
* workbench's own: the open model is updated in place and no reload fires, so
|
|
978
|
+
* the future survives. That is the reason this door exists rather than a
|
|
979
|
+
* fourth storage backend.
|
|
980
|
+
*
|
|
981
|
+
* ## Paths
|
|
982
|
+
*
|
|
983
|
+
* PROJECT-RELATIVE, `/`-separated, no leading slash — `src/scenes/Main.tsx`,
|
|
984
|
+
* `public/models/hero.glb` — exactly the spelling
|
|
985
|
+
* {@link EditorHostHistoryElement.resources} uses, and for the same reason:
|
|
986
|
+
* only the frame knows the workspace folder they resolve against
|
|
987
|
+
* (`URI.joinPath(workspaceFolder.uri, path)`), and resolving them there is
|
|
988
|
+
* what lands a write on the same URI Monaco holds for that file.
|
|
989
|
+
*
|
|
990
|
+
* This is deliberately NOT `StorageBackend`'s spelling, which means two
|
|
991
|
+
* different things by tier — measured 2026-09-19: `HttpStorage` is rooted at
|
|
992
|
+
* `<project>/public/` while a project-rooted backend is rooted at the
|
|
993
|
+
* PROJECT ROOT, which is why the session grew four separate purpose-scoped
|
|
994
|
+
* project-root routes beside it (`/__editor/vgai-file`,
|
|
995
|
+
* `/__editor/project-resource`, `/__editor/data-file`,
|
|
996
|
+
* `/__editor/source-files`). One spelling, here.
|
|
997
|
+
*/
|
|
998
|
+
export interface EditorHostFiles {
|
|
999
|
+
/**
|
|
1000
|
+
* Install the workbench's file service as the project's files. Called once
|
|
1001
|
+
* its own services exist, which is AFTER the editor mounts — a
|
|
1002
|
+
* `ServicesAccessor` is valid only for the synchronous part of an
|
|
1003
|
+
* invocation, so the provider cannot be built before the mount it is handed
|
|
1004
|
+
* to (docs/CODE-OSS.md records that trap). Until it lands the door falls
|
|
1005
|
+
* back to the session's transports rather than refusing: a write in that
|
|
1006
|
+
* window is a real write with nowhere else to go.
|
|
1007
|
+
*/
|
|
1008
|
+
setProvider(provider: EditorHostFileProvider): void;
|
|
1009
|
+
/** Read a UTF-8 text file. Rejects BY NAME if it is missing or a
|
|
1010
|
+
* directory. */
|
|
1011
|
+
read(path: string): Promise<string>;
|
|
1012
|
+
/** Read raw bytes. */
|
|
1013
|
+
readBytes(path: string): Promise<Uint8Array>;
|
|
1014
|
+
/**
|
|
1015
|
+
* Write a file, creating parent directories as needed.
|
|
1016
|
+
*
|
|
1017
|
+
* Under the frame this is the workbench's own write: when a text model is
|
|
1018
|
+
* open for the file it is updated IN PLACE and saved, so no external-change
|
|
1019
|
+
* reload fires and no text undo element lands on top of ours.
|
|
1020
|
+
*/
|
|
1021
|
+
write(path: string, data: string | Uint8Array): Promise<void>;
|
|
1022
|
+
exists(path: string): Promise<boolean>;
|
|
1023
|
+
/** Shallow directory listing. */
|
|
1024
|
+
list(dir: string): Promise<readonly EditorHostFileEntry[]>;
|
|
1025
|
+
/** Change events for the project's files. Returns the unsubscribe. */
|
|
1026
|
+
watch(listener: (event: EditorHostFileEvent) => void): () => void;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export interface EditorHostFileEntry {
|
|
1030
|
+
/** Base name, no slashes. */
|
|
1031
|
+
readonly name: string;
|
|
1032
|
+
/** Project-relative path. */
|
|
1033
|
+
readonly path: string;
|
|
1034
|
+
readonly type: 'file' | 'dir';
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export interface EditorHostFileEvent {
|
|
1038
|
+
readonly type: 'create' | 'update' | 'remove';
|
|
1039
|
+
/** Project-relative path. */
|
|
1040
|
+
readonly path: string;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* THE FRAME'S HALF — what the Code-OSS bridge installs, backed by
|
|
1045
|
+
* `IFileService`/`ITextFileService`. Every member takes the same
|
|
1046
|
+
* project-relative paths the door does; the frame joins them onto the
|
|
1047
|
+
* workspace folder.
|
|
1048
|
+
*
|
|
1049
|
+
* A member the frame cannot answer is ABSENT rather than faked, and the door
|
|
1050
|
+
* falls back to the host transport for it — the anti-shim rule applied to a
|
|
1051
|
+
* file API, because a fabricated listing reads exactly like a real empty
|
|
1052
|
+
* folder.
|
|
1053
|
+
*/
|
|
1054
|
+
export interface EditorHostFileProvider {
|
|
1055
|
+
read(path: string): Promise<string>;
|
|
1056
|
+
readBytes?(path: string): Promise<Uint8Array>;
|
|
1057
|
+
write(path: string, data: string | Uint8Array): Promise<void>;
|
|
1058
|
+
exists(path: string): Promise<boolean>;
|
|
1059
|
+
list?(dir: string): Promise<readonly EditorHostFileEntry[]>;
|
|
1060
|
+
watch?(listener: (event: EditorHostFileEvent) => void): () => void;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* THE STAGE TRANSPORT DOOR — reach the transport of the stage a document runs
|
|
1065
|
+
* on, holding only that document's id.
|
|
1066
|
+
*
|
|
1067
|
+
* Shaped like `documents` beside it: a lookup plus a subscription, because a
|
|
1068
|
+
* stage mounting or unmounting changes what `for` answers and a look drawn
|
|
1069
|
+
* over it must re-render when it does. `null` for a document with no stage of
|
|
1070
|
+
* its own (a tool tab, a text document).
|
|
1071
|
+
*/
|
|
1072
|
+
export interface EditorHostTransport {
|
|
1073
|
+
for(documentId: string): StageTransportHandle | null;
|
|
1074
|
+
subscribe(listener: () => void): () => void;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* The transport vocabulary, re-exported through the host door so a skew
|
|
1079
|
+
* package reaches it without importing the editor's source or an engine
|
|
1080
|
+
* value. See `./transport.ts` for what each member means.
|
|
1081
|
+
*/
|
|
1082
|
+
export type {
|
|
1083
|
+
StageTransportHandle,
|
|
1084
|
+
StageTransportSnapshot,
|
|
1085
|
+
TransportPlaybackState,
|
|
1086
|
+
TransportSubject,
|
|
1087
|
+
} from './transport';
|
|
1088
|
+
|
|
1089
|
+
/** Text logs and source diagnostics rendered by the native workbench. */
|
|
1090
|
+
export interface EditorHostOutputDiagnostic {
|
|
1091
|
+
readonly path: string;
|
|
1092
|
+
readonly line: number;
|
|
1093
|
+
readonly column: number;
|
|
1094
|
+
readonly message: string;
|
|
1095
|
+
readonly severity: 'error' | 'warning';
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
export interface EditorHostOutput {
|
|
1099
|
+
/** Replace a named channel's text and its current diagnostics. Paths are project-relative. */
|
|
1100
|
+
write(
|
|
1101
|
+
id: string,
|
|
1102
|
+
label: string,
|
|
1103
|
+
text: string,
|
|
1104
|
+
diagnostics: readonly EditorHostOutputDiagnostic[],
|
|
1105
|
+
): void;
|
|
1106
|
+
show(id: string): void;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
export interface EditorHost {
|
|
1110
|
+
readonly output: EditorHostOutput;
|
|
1111
|
+
readonly console: EditorHostConsole;
|
|
1112
|
+
readonly live: EditorHostLive;
|
|
1113
|
+
readonly viewport: EditorHostViewport;
|
|
1114
|
+
readonly hierarchy: EditorHostHierarchy;
|
|
1115
|
+
readonly systems: EditorHostSystems;
|
|
1116
|
+
readonly session: EditorHostSession;
|
|
1117
|
+
readonly project: EditorHostProject;
|
|
1118
|
+
/** Raise a notification in the editor's own tray. Returns the dismiss. */
|
|
1119
|
+
notify(notification: EditorHostNotification): () => void;
|
|
1120
|
+
readonly availability: EditorHostAvailability;
|
|
1121
|
+
readonly workspace: EditorHostWorkspace;
|
|
1122
|
+
readonly documents: EditorHostDocuments;
|
|
1123
|
+
readonly projectLocalState: EditorHostProjectLocalState;
|
|
1124
|
+
readonly keyboard: EditorHostKeyboard;
|
|
1125
|
+
readonly history: EditorHostHistory;
|
|
1126
|
+
readonly files: EditorHostFiles;
|
|
1127
|
+
readonly settings: EditorHostSettings;
|
|
1128
|
+
readonly transport: EditorHostTransport;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* ONE registration across every copy of this module. Under the packaged
|
|
1133
|
+
* runtime the editor shell is a prebuilt bundle with this SDK inlined, while
|
|
1134
|
+
* a package's contribution is served from the project's own installed SDK —
|
|
1135
|
+
* two module instances, so plain module state would be an empty registry on
|
|
1136
|
+
* the contribution's side (measured 2026-09-17: "No editor host is
|
|
1137
|
+
* registered" from `@vgai/game`'s connection pill on a registry install).
|
|
1138
|
+
* The layout host (`layouts.tsx`) solved the same split with a `Symbol.for`
|
|
1139
|
+
* key on `globalThis`; this door does the same.
|
|
1140
|
+
*/
|
|
1141
|
+
const HOST_KEY = Symbol.for('vgai.editor.host');
|
|
1142
|
+
const hosts = globalThis as typeof globalThis & { [HOST_KEY]?: EditorHost | null };
|
|
1143
|
+
|
|
1144
|
+
/** The editor's boot registers itself; `null` unregisters (tests). */
|
|
1145
|
+
export function registerEditorHost(next: EditorHost | null): void {
|
|
1146
|
+
hosts[HOST_KEY] = next;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
export function editorHost(): EditorHost {
|
|
1150
|
+
const host = hosts[HOST_KEY];
|
|
1151
|
+
if (!host)
|
|
1152
|
+
throw new Error(
|
|
1153
|
+
'No editor host is registered: this contribution is running outside the editor ' +
|
|
1154
|
+
'(`registerEditorHost` from `@volter/editor-sdk/host` is called by the editor at boot).',
|
|
1155
|
+
);
|
|
1156
|
+
return host;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Re-render only when `select()`'s value changes, sampled on the host's
|
|
1161
|
+
* availability tick — the shape every session-state gate in the editor uses,
|
|
1162
|
+
* so a package's status item costs the same as a built-in one.
|
|
1163
|
+
*/
|
|
1164
|
+
export function useHostAvailabilitySelector<T>(select: () => T): T {
|
|
1165
|
+
const { availability } = editorHost();
|
|
1166
|
+
return useSyncExternalStore(availability.subscribe, select, select);
|
|
1167
|
+
}
|