@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.
Files changed (104) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +19 -0
  4. package/package.json +90 -0
  5. package/src/account.ts +210 -0
  6. package/src/chrome.ts +83 -0
  7. package/src/client.ts +1547 -0
  8. package/src/commands.ts +66 -0
  9. package/src/contributions.ts +985 -0
  10. package/src/document-probe.ts +237 -0
  11. package/src/editor-view.ts +220 -0
  12. package/src/extension.ts +40 -0
  13. package/src/generations.ts +178 -0
  14. package/src/host.ts +1167 -0
  15. package/src/http-transport.browser.ts +14 -0
  16. package/src/http-transport.node.ts +19 -0
  17. package/src/index.ts +128 -0
  18. package/src/layout-arrangements.ts +5 -0
  19. package/src/layouts.tsx +108 -0
  20. package/src/looks.ts +14 -0
  21. package/src/project/output-roots.ts +73 -0
  22. package/src/project/tab-census.ts +149 -0
  23. package/src/project-tool-catalog.ts +96 -0
  24. package/src/selection.tsx +108 -0
  25. package/src/services.ts +18 -0
  26. package/src/session/build-report.ts +19 -0
  27. package/src/session/collaboration-types.ts +262 -0
  28. package/src/session/command-table.ts +333 -0
  29. package/src/session/discovery.ts +90 -0
  30. package/src/session/editor-brand.ts +73 -0
  31. package/src/session/editor-compatibility.ts +248 -0
  32. package/src/session/editor-control-lifecycle.ts +68 -0
  33. package/src/session/editor-control-protocol.ts +5 -0
  34. package/src/session/entrypoint-selection-readers.ts +66 -0
  35. package/src/session/entrypoint-selection-source.ts +120 -0
  36. package/src/session/game-css-scope.ts +30 -0
  37. package/src/session/product-create.ts +24 -0
  38. package/src/session/product-locator.ts +389 -0
  39. package/src/session/project-module-url.ts +245 -0
  40. package/src/session/registry-format.ts +203 -0
  41. package/src/session/relative-path-guard.ts +56 -0
  42. package/src/session/source-glob.ts +15 -0
  43. package/src/session/tool-contribution-convention.ts +116 -0
  44. package/src/session/workbench-locator.ts +650 -0
  45. package/src/session.ts +41 -0
  46. package/src/share.ts +160 -0
  47. package/src/tools/errors.ts +91 -0
  48. package/src/tools/provider-execution.ts +70 -0
  49. package/src/tools/registry.ts +341 -0
  50. package/src/tools/types.ts +159 -0
  51. package/src/transport.ts +97 -0
  52. package/src/types.ts +1581 -0
  53. package/src/views.ts +164 -0
  54. package/src/widgets/design-system.ts +93 -0
  55. package/src/widgets/editor-appearance.ts +149 -0
  56. package/src/widgets/editor-material.ts +83 -0
  57. package/src/widgets/icon-set-registry.ts +105 -0
  58. package/src/widgets/index.ts +71 -0
  59. package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
  60. package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
  61. package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
  62. package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
  63. package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
  64. package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
  65. package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
  66. package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
  67. package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
  68. package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
  69. package/src/widgets/inspector-widgets/color-utils.ts +201 -0
  70. package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
  71. package/src/widgets/inspector-widgets/index.ts +24 -0
  72. package/src/widgets/inspector-widgets/shared.tsx +140 -0
  73. package/src/widgets/interactive-edit-scope.ts +33 -0
  74. package/src/widgets/patterns/Dialog.tsx +129 -0
  75. package/src/widgets/patterns/Fields.tsx +44 -0
  76. package/src/widgets/patterns/List.tsx +25 -0
  77. package/src/widgets/patterns/StateSurface.tsx +40 -0
  78. package/src/widgets/patterns/Surfaces.tsx +122 -0
  79. package/src/widgets/patterns/Tabs.tsx +80 -0
  80. package/src/widgets/patterns/Toolbar.tsx +72 -0
  81. package/src/widgets/patterns/Tree.tsx +72 -0
  82. package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
  83. package/src/widgets/primitives/Button.tsx +62 -0
  84. package/src/widgets/primitives/ColorInput.tsx +78 -0
  85. package/src/widgets/primitives/DraftTextInput.tsx +63 -0
  86. package/src/widgets/primitives/EditorIcon.tsx +157 -0
  87. package/src/widgets/primitives/FormControls.tsx +88 -0
  88. package/src/widgets/primitives/HoverPreview.tsx +96 -0
  89. package/src/widgets/primitives/JsonInput.tsx +113 -0
  90. package/src/widgets/primitives/Layout.tsx +100 -0
  91. package/src/widgets/primitives/Menu.tsx +140 -0
  92. package/src/widgets/primitives/NumberInput.tsx +169 -0
  93. package/src/widgets/primitives/Panel.tsx +80 -0
  94. package/src/widgets/primitives/SectionHeader.tsx +77 -0
  95. package/src/widgets/primitives/Text.tsx +54 -0
  96. package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
  97. package/src/widgets/primitives/Tooltip.tsx +204 -0
  98. package/src/widgets/primitives/Vec3Input.tsx +70 -0
  99. package/src/widgets/primitives/banner-tones.ts +32 -0
  100. package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
  101. package/src/widgets/primitives/editor-icons.ts +245 -0
  102. package/src/widgets/primitives/panel-header-styles.ts +42 -0
  103. package/src/widgets/theme.ts +2633 -0
  104. 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
+ }