@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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE RELAY COMMAND VOCABULARY, in one typed table.
|
|
3
|
+
*
|
|
4
|
+
* A relay command's type string used to be answered by two independent
|
|
5
|
+
* switches that had to agree and were never checked against each other:
|
|
6
|
+
* `command-listener.ts`'s dispatch (what the command DOES) and
|
|
7
|
+
* `server/server-utils.ts`'s `relayCommandTimeoutMs` (how long the server
|
|
8
|
+
* waits for it). A command missing from the second one did not fail — it
|
|
9
|
+
* silently took the generic 5s default, which undercut a client's 60s hold, so
|
|
10
|
+
* the uninformative timer won every race and sent a real investigation looking
|
|
11
|
+
* for a dead editor.
|
|
12
|
+
*
|
|
13
|
+
* This table is the single source. Adding a command means adding a ROW here:
|
|
14
|
+
* the dispatch switch keys off `RelayCommandType`, so a case with no row does
|
|
15
|
+
* not compile, and a row with no case fails the exhaustiveness check in that
|
|
16
|
+
* switch's `default`. The server imports the timeout column rather than
|
|
17
|
+
* restating it.
|
|
18
|
+
*
|
|
19
|
+
* `timeoutMs` is the budget for the WORK. Delivery has its own, much shorter
|
|
20
|
+
* budget (`RELAY_DELIVERY_ACK_MS`, clamped by `RELAY_DELIVERY_MAX_WAIT_MS`) —
|
|
21
|
+
* see `server/server-utils.ts`, which explains why the two are separate.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The generic budget. Most commands are a synchronous edit against the live
|
|
26
|
+
* editor and answer in milliseconds; the rows that override this are the ones
|
|
27
|
+
* that boot a game, rasterize something, or hand control to a game's own code.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_RELAY_COMMAND_TIMEOUT_MS = 5_000;
|
|
30
|
+
|
|
31
|
+
interface RelayCommandSpec {
|
|
32
|
+
/** How long the relay waits for this command to FINISH. */
|
|
33
|
+
readonly timeoutMs: number;
|
|
34
|
+
/** Whether command completion owes a tree-scale status derivation. */
|
|
35
|
+
readonly derivedRefresh: RelayCommandDerivedRefresh;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type RelayCommandDerivedRefresh = 'none' | 'if-content-changed' | 'always';
|
|
39
|
+
|
|
40
|
+
const noDerivedRefresh = (timeoutMs = DEFAULT_RELAY_COMMAND_TIMEOUT_MS): RelayCommandSpec => ({
|
|
41
|
+
timeoutMs,
|
|
42
|
+
derivedRefresh: 'none',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const refreshIfContentChanged = (
|
|
46
|
+
timeoutMs = DEFAULT_RELAY_COMMAND_TIMEOUT_MS,
|
|
47
|
+
): RelayCommandSpec => ({ timeoutMs, derivedRefresh: 'if-content-changed' });
|
|
48
|
+
|
|
49
|
+
const alwaysRefresh = (timeoutMs = DEFAULT_RELAY_COMMAND_TIMEOUT_MS): RelayCommandSpec => ({
|
|
50
|
+
timeoutMs,
|
|
51
|
+
derivedRefresh: 'always',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Every command the editor's relay accepts, and how long it waits for each one
|
|
56
|
+
* to answer. Pure and exported so the table is testable without booting a
|
|
57
|
+
* server and standing through a real 5-second expiry.
|
|
58
|
+
*
|
|
59
|
+
* The rule the table encodes: the budget belongs to whoever the ack is
|
|
60
|
+
* actually gated on. An ordinary editor interaction ("select this entity") is
|
|
61
|
+
* gated on the editor and must fail fast — 5s. Everything with its own entry
|
|
62
|
+
* is gated on something slower and outside the editor's control:
|
|
63
|
+
*
|
|
64
|
+
* - `capture-asset-preview` loads a model, renders four views and encodes
|
|
65
|
+
* five PNGs before acknowledging.
|
|
66
|
+
* - `capture-active-document` can wait for an ingested canvas's next render,
|
|
67
|
+
* composite its canvas and DOM layers, sample flatness, and encode the PNG.
|
|
68
|
+
* It is not an ordinary editor read. Measured in Doctor on racing-game under
|
|
69
|
+
* SwiftShader: the 5s default expired while the page kept rasterizing; the
|
|
70
|
+
* walk retried, parked more captures behind it, and `stop` then 504ed behind
|
|
71
|
+
* work the relay had already told its caller was over.
|
|
72
|
+
* - `active-tab` may synchronously mount a component board before its result can
|
|
73
|
+
* be serialized. The Unity FPS import has 178 stories: the page RECEIVED the
|
|
74
|
+
* command in 0.9s but its first board activation answered after the generic
|
|
75
|
+
* 5s budget, while heartbeats continued throughout. This is editor work, but
|
|
76
|
+
* it is not a millisecond chrome toggle when the destination mounts a board.
|
|
77
|
+
* - `select` and `inspect` may synchronously publish the selected subject and
|
|
78
|
+
* compose its inspector after a cold remount. The translated Unity FPS scene
|
|
79
|
+
* has 4,458 authored objects: Doctor measured both commands received by a
|
|
80
|
+
* beating tab yet still working past 5s, which filed seven false `play-stall`
|
|
81
|
+
* errors and made a source value that had survived the remount look unreadable.
|
|
82
|
+
* - `set-inspection-field`, `remove-inspection-field`, and
|
|
83
|
+
* `run-inspection-action` are authored WRITES —
|
|
84
|
+
* the same dialect writer, the same pipe, the same budget; removing an
|
|
85
|
+
* attribute re-parses and rewrites the module exactly as setting one does.
|
|
86
|
+
* For a source-backed root
|
|
87
|
+
* that write is server work the tab waits on: the planner re-parses the
|
|
88
|
+
* game's own module, the writer rewrites the literal, and a vendored game's
|
|
89
|
+
* lock is re-hashed and recorded in the same gesture. Measured on the
|
|
90
|
+
* racing-game ingest, where the tab is also servicing a game that logs every
|
|
91
|
+
* frame: EVERY transform write came back 504 at the 5s default while the
|
|
92
|
+
* same tab answered `select`/`inspect`/`hierarchy` in the same second — so
|
|
93
|
+
* the caller was told "no editor answered" about a write that was merely
|
|
94
|
+
* slow, which is the failure mode this whole table exists to stop.
|
|
95
|
+
* The PLAY verbs and the whole page-bridge family carry their budgets in
|
|
96
|
+
* `@vgai/game`'s own contributed rows now, for the same reasons and with the
|
|
97
|
+
* same numbers: `play` waits on the project's entire async `setup()`; `stop`
|
|
98
|
+
* waits on a teardown chain; `bridge-recording-export` steps a paused game
|
|
99
|
+
* frame by frame and encodes a video; `bridge-call` runs GAME code;
|
|
100
|
+
* `page-script` and `game-eval` run a step the CALLER wrote; and
|
|
101
|
+
* `bridge-screenshot` rasterizes the whole game stack.
|
|
102
|
+
*
|
|
103
|
+
* That family is the reason this table exists as a named thing. Their
|
|
104
|
+
* client (`@vgai/live`'s `RelayTransport`) already applies its own per-leg
|
|
105
|
+
* `AbortSignal.timeout` — 60s for invoke/page-script, 15s for screenshot —
|
|
106
|
+
* and the server's generic 5s was UNDERCUTTING it, so the server's timer won
|
|
107
|
+
* every race. That mattered for the message, not just the duration: the
|
|
108
|
+
* client's own expiry produces `RELAY_UNREACHABLE` plus a "split the hold into
|
|
109
|
+
* shorter calls" hint, while the server's produces "Command timed out —
|
|
110
|
+
* editor connected but did not respond", which names nothing and sent a real
|
|
111
|
+
* investigation looking for a dead editor. Keep each entry >= its client-side
|
|
112
|
+
* counterpart so the informative timer is always the one that fires.
|
|
113
|
+
*
|
|
114
|
+
* Rows without an explicit budget take {@link DEFAULT_RELAY_COMMAND_TIMEOUT_MS}.
|
|
115
|
+
*/
|
|
116
|
+
export const RELAY_COMMANDS = {
|
|
117
|
+
// ---- Selection and framing ---------------------------------------------
|
|
118
|
+
select: noDerivedRefresh(30_000),
|
|
119
|
+
'select-multiple': noDerivedRefresh(),
|
|
120
|
+
'select-all': noDerivedRefresh(),
|
|
121
|
+
'focus-entity': noDerivedRefresh(),
|
|
122
|
+
'frame-entity': noDerivedRefresh(),
|
|
123
|
+
'focus-selection': noDerivedRefresh(),
|
|
124
|
+
'view-preset': noDerivedRefresh(),
|
|
125
|
+
'set-camera': noDerivedRefresh(),
|
|
126
|
+
|
|
127
|
+
// ---- Looking at the open Object3D document ------------------------------
|
|
128
|
+
// These ANIMATE the shared camera and ack when the move ends, so their
|
|
129
|
+
// budget is the move's own wall clock, not an editor edit's. Both refuse a
|
|
130
|
+
// duration past 60 s themselves; the ceiling here is the relay's backstop.
|
|
131
|
+
'document-orbit': noDerivedRefresh(120_000),
|
|
132
|
+
'document-turntable': noDerivedRefresh(120_000),
|
|
133
|
+
'document-frame': noDerivedRefresh(),
|
|
134
|
+
|
|
135
|
+
// ---- Editor chrome ------------------------------------------------------
|
|
136
|
+
'viewport-tab': noDerivedRefresh(),
|
|
137
|
+
// Focusing a panel is dock work, but its ack WAITS for the dock to report
|
|
138
|
+
// the panel focused (`revealStaticPanel`), so keep the relay past that wait.
|
|
139
|
+
'show-panel': noDerivedRefresh(15_000),
|
|
140
|
+
'active-tab': alwaysRefresh(30_000),
|
|
141
|
+
// Opening and presenting can await the inspector and a fresh source build.
|
|
142
|
+
// Keep their relay budget aligned with capture-active-document below.
|
|
143
|
+
'open-asset-tab': alwaysRefresh(30_000),
|
|
144
|
+
// The selection half: it changes what the Inspector shows, so the derived
|
|
145
|
+
// state a caller reads next must be refreshed like the open half's.
|
|
146
|
+
'select-asset': alwaysRefresh(),
|
|
147
|
+
'close-asset-tab': alwaysRefresh(),
|
|
148
|
+
'toggle-command-palette': noDerivedRefresh(),
|
|
149
|
+
'toggle-console': noDerivedRefresh(),
|
|
150
|
+
// RELOAD THE EDITOR PAGE — `@vgai/live`'s `page.reload()`, the one page verb
|
|
151
|
+
// no step can express. It is the HOST's, not a package's: reloading the tab
|
|
152
|
+
// is what the tab is, and until walk 5 it was a `@vgai/game` command
|
|
153
|
+
// contribution, so a project without that package — every model project —
|
|
154
|
+
// answered `unknown command type "page-reload"` for a door `@vgai/live`
|
|
155
|
+
// documents as general (measured on a `model-editor create` scaffold,
|
|
156
|
+
// 2026-09-21). The handler schedules the navigation for the next task so
|
|
157
|
+
// this ack can travel before the channel is torn down; the client waits for
|
|
158
|
+
// the new page load on the server's own tab table.
|
|
159
|
+
'page-reload': alwaysRefresh(),
|
|
160
|
+
// Acks the ARRANGEMENT, not the intent: the dock clears and rebuilds on the
|
|
161
|
+
// next paint, so the handler waits for that rebuild (see
|
|
162
|
+
// `workspace-presets.ts`'s applied signal). Still ordinary editor work.
|
|
163
|
+
'set-workspace': noDerivedRefresh(),
|
|
164
|
+
// A style bundle is chrome appearance; the document is untouched.
|
|
165
|
+
'set-style': noDerivedRefresh(),
|
|
166
|
+
'set-appearance': noDerivedRefresh(),
|
|
167
|
+
'present-view': alwaysRefresh(30_000),
|
|
168
|
+
'current-view': refreshIfContentChanged(),
|
|
169
|
+
// A read of the resolved document table; changes nothing.
|
|
170
|
+
'document-table': noDerivedRefresh(),
|
|
171
|
+
|
|
172
|
+
// ---- Inspection and authoring ------------------------------------------
|
|
173
|
+
// The inspection writes go through the project mutation path, which touches
|
|
174
|
+
// the filesystem and may wait on a collaboration revision.
|
|
175
|
+
inspect: refreshIfContentChanged(30_000),
|
|
176
|
+
hierarchy: refreshIfContentChanged(),
|
|
177
|
+
'expand-hierarchy-all': refreshIfContentChanged(),
|
|
178
|
+
'collapse-hierarchy-all': refreshIfContentChanged(),
|
|
179
|
+
'document-probe': refreshIfContentChanged(),
|
|
180
|
+
// A step against the open document's session can edit it (`ctx.ops.mesh.*`).
|
|
181
|
+
// Project-owned document code can rebuild an entire Model, just as a
|
|
182
|
+
// page-script can rebuild a game. Give both the same execution budget.
|
|
183
|
+
'document-script': refreshIfContentChanged(60_000),
|
|
184
|
+
'set-inspection-field': alwaysRefresh(30_000),
|
|
185
|
+
'remove-inspection-field': alwaysRefresh(30_000),
|
|
186
|
+
// `extract-component` and `fork-component` left this table with their menus
|
|
187
|
+
// (`@vgai/game/contributions/component-verbs.command.ts`), which carries
|
|
188
|
+
// both budgets forward row for row.
|
|
189
|
+
// The hierarchy context menu's structure verbs, over the control door. Every
|
|
190
|
+
// one of them rewrites the game's own source, so they refresh like a write.
|
|
191
|
+
'structure-op': alwaysRefresh(30_000),
|
|
192
|
+
'run-inspection-action': alwaysRefresh(30_000),
|
|
193
|
+
// `editor.command(id, args)` — ONE workbench/view command by id. What it
|
|
194
|
+
// does is the command's, so nothing here can know whether the document
|
|
195
|
+
// changed: `alwaysRefresh` is the honest budget, the same one every other
|
|
196
|
+
// "run something the editor does not model" verb takes.
|
|
197
|
+
'run-command': alwaysRefresh(30_000),
|
|
198
|
+
undo: alwaysRefresh(),
|
|
199
|
+
redo: alwaysRefresh(),
|
|
200
|
+
// `open` WAITS FOR THE PROJECT ADAPTER, for the same reason `play` waits on
|
|
201
|
+
// the project's async `setup()`: on a cold session the adapter's scene table
|
|
202
|
+
// is not there yet, and the door's whole job is to put a document on screen
|
|
203
|
+
// once it is. MEASURED on three cold `vgai edit` sessions over a
|
|
204
|
+
// `--template models` probe — 6025 / 6831 / 6474 ms from tab-connected to
|
|
205
|
+
// the entry being listed, every one of them first refusing with
|
|
206
|
+
// `SCENE_TABLE_UNAVAILABLE`. At the 5 s default the relay's timer won that
|
|
207
|
+
// race and the caller was told "the tab did not respond" about a door that
|
|
208
|
+
// was merely waiting, which is the exact failure this table exists to stop.
|
|
209
|
+
// 30 s sits above `openSceneTableEntryWhenListed`'s own 15 s bound so the
|
|
210
|
+
// INFORMATIVE refusal (the scene table's, naming the known ids) is always
|
|
211
|
+
// the one that fires.
|
|
212
|
+
open: alwaysRefresh(30_000),
|
|
213
|
+
|
|
214
|
+
// ---- Rasterizing --------------------------------------------------------
|
|
215
|
+
// Everything here renders something and reads pixels back.
|
|
216
|
+
'capture-active-document': refreshIfContentChanged(30_000),
|
|
217
|
+
// The whole editor page through the same compositor; the foreignObject leg
|
|
218
|
+
// over a full dock takes seconds, not milliseconds.
|
|
219
|
+
'capture-editor-chrome': refreshIfContentChanged(30_000),
|
|
220
|
+
'capture-viewport': refreshIfContentChanged(),
|
|
221
|
+
'capture-asset-preview': refreshIfContentChanged(30_000),
|
|
222
|
+
|
|
223
|
+
// The ONE door onto a view's verbs (`@volter/editor-sdk/views`, U8 ruling 1).
|
|
224
|
+
// A verb either READS the view or moves its own transform; neither touches
|
|
225
|
+
// a document, so nothing derived refreshes.
|
|
226
|
+
|
|
227
|
+
// ---- Viewport toggles ---------------------------------------------------
|
|
228
|
+
'set-grid': noDerivedRefresh(),
|
|
229
|
+
'set-helpers': noDerivedRefresh(),
|
|
230
|
+
'set-stats': noDerivedRefresh(),
|
|
231
|
+
'set-shading-mode': noDerivedRefresh(),
|
|
232
|
+
'set-helper-type': noDerivedRefresh(),
|
|
233
|
+
'set-transform-mode': noDerivedRefresh(),
|
|
234
|
+
'set-transform-space': noDerivedRefresh(),
|
|
235
|
+
'set-snap': noDerivedRefresh(),
|
|
236
|
+
|
|
237
|
+
// The BLENDER lane's ten verbs are `@vgai/blender`'s
|
|
238
|
+
// `contributions/blender.command.ts` (WORK.md §The workbench, item D) —
|
|
239
|
+
// each row's budget travelled with it, onto the contributed spec the page
|
|
240
|
+
// reports to the server. `capture-story-variants` made the same move, to the
|
|
241
|
+
// kit's own `stories/story-capture-command.ts`: it
|
|
242
|
+
// mounts each CSF export, waits for the DOM to settle, rasterizes it through
|
|
243
|
+
// the `foreignObject` leg and tiles the sheet, so its budget scales with the
|
|
244
|
+
// file's export count rather than with any single render — 120 s, measured
|
|
245
|
+
// on a real 10-story HUD file that expired the default outright, and stated
|
|
246
|
+
// now on the contributed row instead of here.
|
|
247
|
+
} as const satisfies Record<string, RelayCommandSpec>;
|
|
248
|
+
|
|
249
|
+
/** Every command type the relay accepts. The dispatch switch keys off this. */
|
|
250
|
+
export type RelayCommandType = keyof typeof RELAY_COMMANDS;
|
|
251
|
+
|
|
252
|
+
/** The vocabulary as data — what a guard or a tool enumerates. */
|
|
253
|
+
export const RELAY_COMMAND_TYPES = Object.keys(RELAY_COMMANDS) as readonly RelayCommandType[];
|
|
254
|
+
|
|
255
|
+
export function isRelayCommandType(value: unknown): value is RelayCommandType {
|
|
256
|
+
return typeof value === 'string' && Object.hasOwn(RELAY_COMMANDS, value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* How long the relay waits for a command to finish. An unrecognized type gets
|
|
261
|
+
* the generic budget rather than an error: the relay's job is to hand the
|
|
262
|
+
* string to the tab and let the TAB say it does not know it.
|
|
263
|
+
*/
|
|
264
|
+
export function relayCommandTimeoutMs(type: unknown): number {
|
|
265
|
+
return isRelayCommandType(type)
|
|
266
|
+
? RELAY_COMMANDS[type].timeoutMs
|
|
267
|
+
: DEFAULT_RELAY_COMMAND_TIMEOUT_MS;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The status-derivation policy owned by the same command row as its timeout. */
|
|
271
|
+
export function relayCommandDerivedRefresh(type: unknown): RelayCommandDerivedRefresh {
|
|
272
|
+
return isRelayCommandType(type) ? RELAY_COMMANDS[type].derivedRefresh : 'always';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The subset an INGEST session answers itself (`ingest/ingest-play-commands.ts`).
|
|
277
|
+
* A captured ingest is not a host-mounted root, so first-party play would tear
|
|
278
|
+
* its mount down; these five are dispatched against the ingest surface instead.
|
|
279
|
+
*
|
|
280
|
+
* Plain strings, not `RelayCommandType`: the five verbs left this table for
|
|
281
|
+
* `@vgai/game`'s `play.command.ts` when Play left the host (WORK.md §The
|
|
282
|
+
* workbench, P3b), and the ingest latch runs in `handleCommand`'s PROLOGUE —
|
|
283
|
+
* before any contributed handler is looked up — so a captured ingest still
|
|
284
|
+
* answers them first. The latch moves with ingest in P4.
|
|
285
|
+
*/
|
|
286
|
+
export const INGEST_PLAY_COMMAND_TYPES = [
|
|
287
|
+
'play',
|
|
288
|
+
'stop',
|
|
289
|
+
'pause',
|
|
290
|
+
'resume',
|
|
291
|
+
'step',
|
|
292
|
+
] as const satisfies readonly string[];
|
|
293
|
+
|
|
294
|
+
export type IngestPlayCommandType = (typeof INGEST_PLAY_COMMAND_TYPES)[number];
|
|
295
|
+
|
|
296
|
+
export function isIngestPlayCommandType(value: unknown): value is IngestPlayCommandType {
|
|
297
|
+
return (INGEST_PLAY_COMMAND_TYPES as readonly string[]).includes(value as string);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* THE ANSWER SHAPE, the other column of the same vocabulary — what a tab posts
|
|
302
|
+
* back to `/__editor/command-result` for a command it just ran.
|
|
303
|
+
*
|
|
304
|
+
* Same rule as the timeout column above, for the same reason: the relay has two
|
|
305
|
+
* ends in two compilation units — the browser that PRODUCES this
|
|
306
|
+
* (`command-listener.ts`) and the server that settles and re-serves it
|
|
307
|
+
* (`server/server-utils.ts`, `server/routes/control-plane.ts`) — and each used
|
|
308
|
+
* to declare its own copy. They had already drifted on the field that matters
|
|
309
|
+
* most: the browser's said `ok: boolean` and the server's said `ok?: boolean`,
|
|
310
|
+
* so server code could read a result that never says whether it worked. Both
|
|
311
|
+
* files already import this module (so does `ingest/ingest-play-commands.ts`,
|
|
312
|
+
* which restated the shape a third time), so there is no new dependency here.
|
|
313
|
+
*
|
|
314
|
+
* `ok` is REQUIRED because the producer always writes it: every browser exit
|
|
315
|
+
* (`structuredErrorResult`, `commandThrewResult`, every handler's return) and
|
|
316
|
+
* every server-side settle (`settlePendingCommand`'s callers, all of which pair
|
|
317
|
+
* `timedOut: true` with `ok: false`) sets it. A payload arriving from off the
|
|
318
|
+
* wire is narrowed where it enters — `handleCommandResult` — not by weakening
|
|
319
|
+
* this type.
|
|
320
|
+
*
|
|
321
|
+
* `timedOut` is deliberately NOT here: no tab produces it. It is the SERVER's
|
|
322
|
+
* own verdict for a command no tab answered, so it lives on the server's
|
|
323
|
+
* `RelayedCommandResult` extension beside the code that sets it.
|
|
324
|
+
*/
|
|
325
|
+
export interface CommandResult {
|
|
326
|
+
ok: boolean;
|
|
327
|
+
error?: string;
|
|
328
|
+
/** Payload for commands that answer with data (`capture-viewport`'s base64
|
|
329
|
+
* PNG, the debug-seam reads). On the failure path it carries structured
|
|
330
|
+
* markers (`{ code: 'UNKNOWN_COMMAND_TYPE' }`, registered-name lists) the
|
|
331
|
+
* SDK matches on — never message prose. */
|
|
332
|
+
data?: Record<string, unknown>;
|
|
333
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Registered-session discovery; never selects another project's session. */
|
|
2
|
+
import { readLiveRegisteredSessions, servedProjectAnswer } from './registry-format.js';
|
|
3
|
+
export { servedProjectAnswer } from './registry-format.js';
|
|
4
|
+
|
|
5
|
+
export const EDITOR_SESSION_DISCOVERY_TIMEOUT_MS = 2000;
|
|
6
|
+
|
|
7
|
+
export const EDITOR_PROBE_TIMEOUT_MS = 1500;
|
|
8
|
+
|
|
9
|
+
export class EditorTimeoutError extends Error {
|
|
10
|
+
constructor(label: string, ms: number) {
|
|
11
|
+
super(`${label} timed out after ${ms}ms`);
|
|
12
|
+
this.name = 'EditorTimeoutError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
17
|
+
return new Promise<T>((resolvePromise, reject) => {
|
|
18
|
+
const timer = setTimeout(() => reject(new EditorTimeoutError(label, ms)), ms);
|
|
19
|
+
promise.then(
|
|
20
|
+
(v) => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
resolvePromise(v);
|
|
23
|
+
},
|
|
24
|
+
(err) => {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface EditorSessionInfo {
|
|
33
|
+
/** The port this session's dev server is bound to. */
|
|
34
|
+
port: number;
|
|
35
|
+
/** Canonical project root path currently open, or null when none. */
|
|
36
|
+
project: string | null;
|
|
37
|
+
/** Dev-server process id, when known from the local session registry (null for an unregistered/legacy server the caller only probed by port). */
|
|
38
|
+
pid: number | null;
|
|
39
|
+
/** Exact explicitly targeted editor origin/base URL, including protocol and host. */
|
|
40
|
+
url?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Why this server cannot DESCRIBE the `project` it is serving — its
|
|
43
|
+
* manifest's own parse/validation failure, naming the failing key. `null`
|
|
44
|
+
* when the project reads fine, and `undefined` from a probe that did not ask
|
|
45
|
+
* (or a server too old to say). See {@link servedProjectAnswer}: a session in
|
|
46
|
+
* this state is still THIS project's session, and callers must report the
|
|
47
|
+
* reason rather than treat it as no session at all.
|
|
48
|
+
*/
|
|
49
|
+
manifestError?: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function fetchJson(url: string, timeoutMs: number): Promise<unknown | undefined> {
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
55
|
+
if (!res.ok) return undefined;
|
|
56
|
+
return await res.json();
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function baseUrl(session: EditorSessionInfo): string {
|
|
63
|
+
return session.url ?? `http://127.0.0.1:${session.port}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SessionListingTransport { listSessions(timeoutMs: number): Promise<EditorSessionInfo[]>; }
|
|
67
|
+
export class HttpSessionDiscovery implements SessionListingTransport {
|
|
68
|
+
async listSessions(timeoutMs: number): Promise<EditorSessionInfo[]> {
|
|
69
|
+
const registered = readLiveRegisteredSessions();
|
|
70
|
+
const perProbeTimeout = Math.min(EDITOR_PROBE_TIMEOUT_MS, Math.max(200, timeoutMs));
|
|
71
|
+
const probes = await Promise.all(
|
|
72
|
+
registered.map(async (s) => {
|
|
73
|
+
const body = await fetchJson(
|
|
74
|
+
`${baseUrl({ port: s.port, project: null, pid: s.pid })}/__editor/project`,
|
|
75
|
+
perProbeTimeout,
|
|
76
|
+
);
|
|
77
|
+
if (body === undefined) return undefined;
|
|
78
|
+
const served = servedProjectAnswer(body);
|
|
79
|
+
const info: EditorSessionInfo = {
|
|
80
|
+
port: s.port,
|
|
81
|
+
project: served.path,
|
|
82
|
+
pid: s.pid,
|
|
83
|
+
manifestError: served.manifestError,
|
|
84
|
+
};
|
|
85
|
+
return info;
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
return probes.filter((p): p is EditorSessionInfo => p !== undefined);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor's public brand contract.
|
|
3
|
+
*
|
|
4
|
+
* Browser metadata, generated image assets, and the in-product logo all read
|
|
5
|
+
* this module. Keep the mark as geometry rather than a second hand-maintained
|
|
6
|
+
* SVG so every public surface presents the same logo. The angular symbol is
|
|
7
|
+
* shared with the marketing website; VgaiLogo.css owns assembly/loading motion.
|
|
8
|
+
*/
|
|
9
|
+
export const EDITOR_BRAND = {
|
|
10
|
+
name: 'Volter Editor',
|
|
11
|
+
shortName: 'Volter Editor',
|
|
12
|
+
title: 'Volter Editor',
|
|
13
|
+
description:
|
|
14
|
+
'An extensible visual editor with Blender modeling and agent automation.',
|
|
15
|
+
themeColor: '#101318',
|
|
16
|
+
accentColor: '#579EFF',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/** Shared angular three-piece symbol, sourced from https://www.volter.ai/volter-logo.svg. */
|
|
20
|
+
export const VGAI_LOGO = {
|
|
21
|
+
viewBox: '0 0 160 160',
|
|
22
|
+
transform: 'translate(24 20) scale(6.6)',
|
|
23
|
+
pieces: [
|
|
24
|
+
{
|
|
25
|
+
id: 'a',
|
|
26
|
+
path: 'M15.7695 1.16406C16.0141 1.5523 16.1668 2.01298 16.1865 2.52539L16.6719 15.1338C16.7534 17.2536 14.5094 18.667 12.6328 17.6777L10.5869 16.5986L15.7695 1.16406Z',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'b',
|
|
30
|
+
path: 'M5.31836 13.8213L1.4707 11.793C-0.405775 10.8036 -0.507886 8.15396 1.28711 7.02344L1.91699 6.62598L5.31836 13.8213Z',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'c',
|
|
34
|
+
path: 'M8.41309 12.4766L4.79102 4.81543L11.9639 0.298828C12.1707 0.168586 12.3854 0.0706555 12.6025 0.000976562L8.41309 12.4766Z',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
/** Format the live browser window title around the current surface. */
|
|
40
|
+
export function editorDocumentTitle(subject?: string | null): string {
|
|
41
|
+
const normalized = subject?.trim();
|
|
42
|
+
return normalized ? `${normalized} — ${EDITOR_BRAND.name}` : EDITOR_BRAND.name;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Render the shared symbol in one color, preserving all three source paths. */
|
|
46
|
+
export function vgaiLogoPaths(fill = '#F3F4F6'): string {
|
|
47
|
+
return VGAI_LOGO.pieces.map((piece) => `<path d="${piece.path}" fill="${fill}"/>`).join('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The standalone transparent mark for docs, partners, and compositing. */
|
|
51
|
+
export function editorMarkSvg(): string {
|
|
52
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="${VGAI_LOGO.viewBox}" role="img" aria-label="${EDITOR_BRAND.shortName}">
|
|
53
|
+
<g transform="${VGAI_LOGO.transform}">${vgaiLogoPaths()}</g>
|
|
54
|
+
</svg>
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The editor app icon as a standalone SVG.
|
|
60
|
+
*
|
|
61
|
+
* Generated browser/native assets and transient server pages share this
|
|
62
|
+
* exact renderer so none of those surfaces can grow a hand-copied logo.
|
|
63
|
+
*/
|
|
64
|
+
export function editorAppIconSvg(markScale = 1): string {
|
|
65
|
+
const inset = (1 - markScale) * 80;
|
|
66
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160" role="img" aria-label="${EDITOR_BRAND.shortName}">
|
|
67
|
+
<rect width="160" height="160" rx="34" fill="${EDITOR_BRAND.themeColor}"/>
|
|
68
|
+
<g transform="translate(${inset} ${inset}) scale(${markScale})">
|
|
69
|
+
<g transform="${VGAI_LOGO.transform}">${vgaiLogoPaths()}</g>
|
|
70
|
+
</g>
|
|
71
|
+
</svg>
|
|
72
|
+
`;
|
|
73
|
+
}
|