@volter/editor-core 0.5.57 → 0.5.58

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/BUNDLED_NOTICES CHANGED
@@ -31,7 +31,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
31
  SOFTWARE.
32
32
 
33
33
 
34
- @volter/editor-project@0.5.57
34
+ @volter/editor-project@0.5.58
35
35
  Declared license: Apache-2.0
36
36
 
37
37
  --- packages/editor-project/LICENSE ---
@@ -240,7 +240,7 @@ Declared license: Apache-2.0
240
240
  limitations under the License.
241
241
 
242
242
 
243
- @volter/editor-sdk@0.5.57
243
+ @volter/editor-sdk@0.5.58
244
244
  Declared license: Apache-2.0
245
245
 
246
246
  --- packages/editor-sdk/LICENSE ---
@@ -461,7 +461,7 @@ The project output-root policy is Apache-2.0, transferred from the original
461
461
  SDK; its exact source is recorded in provenance/editor-server.json.
462
462
 
463
463
 
464
- @volter/editor-threejs@0.5.57
464
+ @volter/editor-threejs@0.5.58
465
465
  Declared license: AGPL-3.0-only AND Apache-2.0
466
466
 
467
467
  --- packages/editor-threejs/LICENSE ---
@@ -80769,7 +80769,9 @@ import { brotliDecompressSync } from "node:zlib";
80769
80769
  var BLENDER_WASM_FILES = [
80770
80770
  "blender_browser.js",
80771
80771
  "blender_browser.wasm",
80772
- "blender_browser.data"
80772
+ "blender_browser.data",
80773
+ "essentials.json",
80774
+ "essentials.bin"
80773
80775
  ];
80774
80776
  var BLENDER_WALI_ARTIFACT = "blender.wasm";
80775
80777
  var BLENDER_WALI_RUNTIME = "runtime";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@volter/editor-core",
3
3
  "author": "Volter AI, Inc.",
4
- "version": "0.5.57",
4
+ "version": "0.5.58",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -82,9 +82,9 @@
82
82
  "three.quarks": "0.16.0",
83
83
  "typescript": "5.9.3",
84
84
  "zod": "4.3.6",
85
- "@volter/editor-project": "0.5.57",
86
- "@volter/editor-sdk": "0.5.57",
87
- "@volter/editor-threejs": "0.5.57",
85
+ "@volter/editor-project": "0.5.58",
86
+ "@volter/editor-sdk": "0.5.58",
87
+ "@volter/editor-threejs": "0.5.58",
88
88
  "vite": "^6.4.3",
89
89
  "express": "^5.2.1",
90
90
  "chokidar": "^5.0.0",
@@ -61,11 +61,13 @@ import { brotliDecompressSync } from 'node:zlib';
61
61
 
62
62
  export type BlenderSkew = 'emscripten' | 'wali';
63
63
 
64
- /** The three files the Emscripten runtime asks for, by the names the glue uses. */
64
+ /** Engine artifacts plus the separately packaged Essentials asset payload. */
65
65
  export const BLENDER_WASM_FILES = [
66
66
  'blender_browser.js',
67
67
  'blender_browser.wasm',
68
68
  'blender_browser.data',
69
+ 'essentials.json',
70
+ 'essentials.bin',
69
71
  ] as const;
70
72
 
71
73
  export type BlenderWasmFile = (typeof BLENDER_WASM_FILES)[number];
@@ -211,14 +211,14 @@ export function buildStaticActions(
211
211
  label: 'Duplicate',
212
212
  category: 'action',
213
213
  shortcut: shortcutFor('edit.duplicate'),
214
- execute: () => void duplicateSelection(store),
214
+ execute: () => duplicateSelection(store),
215
215
  },
216
216
  {
217
217
  id: 'editor.delete',
218
218
  label: 'Delete Selected',
219
219
  category: 'action',
220
220
  shortcut: shortcutFor('edit.delete'),
221
- execute: () => void deleteSelection(store),
221
+ execute: () => deleteSelection(store),
222
222
  },
223
223
  {
224
224
  id: 'editor.select-all',
@@ -320,6 +320,17 @@ export function duplicateAuthoringNode(
320
320
  );
321
321
  }
322
322
 
323
+ export function duplicateManyAuthoringNodes(
324
+ adapter: AuthoringAdapter,
325
+ nodeIds: readonly string[],
326
+ ): StructuralWriteOutcome {
327
+ const provider = adapter.structure;
328
+ if (!provider?.duplicateMany) return;
329
+ return structureWrite(adapter, 'duplicateMany',
330
+ `the editor duplicated ${nodeIds.length} selected node(s) as one operation`,
331
+ () => provider.duplicateMany?.(nodeIds));
332
+ }
333
+
323
334
  export function reparentAuthoringNode(
324
335
  adapter: AuthoringAdapter,
325
336
  nodeId: string,
@@ -139,6 +139,7 @@ import {
139
139
  createAuthoringNode,
140
140
  cutAuthoringNodes,
141
141
  duplicateAuthoringNode,
142
+ duplicateManyAuthoringNodes,
142
143
  groupAuthoringNodes,
143
144
  pasteAuthoringNodes,
144
145
  removeAuthoringNode,
@@ -1943,6 +1944,10 @@ export async function handleCommand(
1943
1944
  return { ok: true, data: { write } };
1944
1945
  }
1945
1946
  case 'duplicate': {
1947
+ if (ids.length > 1 && structure.duplicateMany) {
1948
+ const write = await duplicateManyAuthoringNodes(adapter, ids);
1949
+ return { ok: true, data: { write } };
1950
+ }
1946
1951
  const copy = duplicateAuthoringNode(adapter, needsId());
1947
1952
  return { ok: true, data: { id: copy.id, write: await copy.ack } };
1948
1953
  }
@@ -38,6 +38,10 @@ import { setFilesProvider } from './files/file-provider';
38
38
  import { projectFiles } from './files/project-files';
39
39
  import {
40
40
  onHistoryElement,
41
+ invalidateHistoryResources,
42
+ onHistoryInvalidated,
43
+ notifyHistoryDelegateChanged,
44
+ emitHistoryElement,
41
45
  recordedHistoryElements,
42
46
  setHistoryDelegate,
43
47
  setHistoryDocumentResolver,
@@ -332,8 +336,12 @@ export function installEditorHostDoor(): void {
332
336
  // keyed by the document's own file; `history-service.ts` stays the
333
337
  // RECORDER and stops deciding which entry is next.
334
338
  history: {
339
+ record: emitHistoryElement,
335
340
  setDelegate: setHistoryDelegate,
336
341
  onElement: onHistoryElement,
342
+ invalidate: invalidateHistoryResources,
343
+ onInvalidated: onHistoryInvalidated,
344
+ changed: notifyHistoryDelegateChanged,
337
345
  elements: recordedHistoryElements,
338
346
  focusedResource: () => {
339
347
  const document = activeWorkspaceDocument();
@@ -3,6 +3,7 @@ import {
3
3
  copyAuthoringNodes,
4
4
  cutAuthoringNodes,
5
5
  duplicateAuthoringNode,
6
+ duplicateManyAuthoringNodes,
6
7
  groupAuthoringNodes,
7
8
  pasteAuthoringNodes,
8
9
  removeAuthoringNode,
@@ -216,7 +217,21 @@ async function runDuplicateSelection(store: EditorShellStore): Promise<void> {
216
217
  ? (adapter.hierarchy.node(selection[0] as string)?.label ?? 'selection')
217
218
  : `${selection.length} objects`;
218
219
  showTransientHint(`Duplicating ${label}…`);
219
- for (const id of selection) await duplicateAuthoringNode(adapter, id).ack;
220
+ if (structure.duplicateMany) {
221
+ const ack = await duplicateManyAuthoringNodes(adapter, selection);
222
+ if (ack && !ack.persisted) {
223
+ showTransientHint(ack.destination);
224
+ return;
225
+ }
226
+ } else {
227
+ for (const id of selection) {
228
+ const ack = await duplicateAuthoringNode(adapter, id).ack;
229
+ if (ack && !ack.persisted) {
230
+ showTransientHint(ack.destination);
231
+ return;
232
+ }
233
+ }
234
+ }
220
235
  showTransientHint(`Duplicated ${label}`);
221
236
  }
222
237
 
@@ -200,15 +200,19 @@ export interface VgaiKeyboardHandle {
200
200
  export interface VgaiHistoryHandle {
201
201
  elements(): readonly VgaiHistoryElementHandle[];
202
202
  onElement(listener: (element: VgaiHistoryElementHandle) => void): () => void;
203
+ onInvalidated(listener: (resources: readonly string[]) => void): () => void;
204
+ changed(): void;
203
205
  focusedResource(): string | null;
204
206
  focusedDocument(): { id: string; label: string } | null;
205
207
  /** Hand the frame's OWN undo to the editor, so its Edit menu, palette and
206
208
  * `vgai eval` reach the one stack instead of a cursor nobody drives. */
207
209
  setDelegate(delegate: {
208
- undo(): void;
209
- redo(): void;
210
+ undo(): void | boolean | Promise<void | boolean>;
211
+ redo(): void | boolean | Promise<void | boolean>;
210
212
  canUndo(): boolean;
211
213
  canRedo(): boolean;
214
+ undoLabel?(): string | null;
215
+ redoLabel?(): string | null;
212
216
  }): void;
213
217
  report(level: 'warn' | 'error', message: string): void;
214
218
  }
@@ -1181,6 +1185,8 @@ export async function mountEditor(next: VscodeParts): Promise<{
1181
1185
  const history: VgaiHistoryHandle = {
1182
1186
  elements: () => editorHost().history.elements(),
1183
1187
  onElement: (listener) => editorHost().history.onElement(listener),
1188
+ onInvalidated: (listener) => editorHost().history.onInvalidated(listener),
1189
+ changed: () => editorHost().history.changed(),
1184
1190
  focusedResource: () => editorHost().history.focusedResource(),
1185
1191
  // The document's TITLE is what a refusal should say ("Nothing to undo in
1186
1192
  // MainScene"), and the id is what scopes the stack; both come from the
@@ -80,10 +80,12 @@ export interface HistoryElement {
80
80
  * is worse than one that is absent.
81
81
  */
82
82
  export interface HistoryDelegate {
83
- undo(): void;
84
- redo(): void;
83
+ undo(): void | boolean | Promise<void | boolean>;
84
+ redo(): void | boolean | Promise<void | boolean>;
85
85
  canUndo(): boolean;
86
86
  canRedo(): boolean;
87
+ undoLabel?(): string | null;
88
+ redoLabel?(): string | null;
87
89
  }
88
90
 
89
91
  let delegate: HistoryDelegate | null = null;
@@ -93,6 +95,7 @@ let delegate: HistoryDelegate | null = null;
93
95
  let documentResolver: (() => string | null) | null = null;
94
96
  const ownerListeners = new Set<() => void>();
95
97
  const elementListeners = new Set<(element: HistoryElement) => void>();
98
+ const invalidationListeners = new Set<(resources: readonly string[]) => void>();
96
99
  /** Everything recorded while a delegate was installed, so a LATE subscriber
97
100
  * (the bridge mounts after the editor's first edits are possible) can catch
98
101
  * up rather than start with a stack that is missing its beginning. */
@@ -131,16 +134,24 @@ export function subscribeHistoryDelegate(listener: () => void): () => void {
131
134
  return () => ownerListeners.delete(listener);
132
135
  }
133
136
 
137
+ export function notifyHistoryDelegateChanged(): void {
138
+ for (const listener of ownerListeners) listener();
139
+ }
140
+
134
141
  /**
135
142
  * Offer one committed transaction to the frame. Recorded even before the
136
143
  * delegate arrives — that buffer is what lets a frame that mounts after the
137
144
  * editor's first edits push what it missed, in order, rather than start with a
138
145
  * stack missing its beginning.
139
146
  */
140
- export function emitHistoryElement(element: Omit<HistoryElement, 'document'>): void {
141
- const stamped: HistoryElement = { ...element, document: documentResolver?.() ?? null };
147
+ export function emitHistoryElement(element: Omit<HistoryElement, 'document'> & { document?: string | null }): void {
148
+ const stamped: HistoryElement = {
149
+ ...element,
150
+ document: element.document === undefined ? documentResolver?.() ?? null : element.document,
151
+ };
142
152
  recorded.push(stamped);
143
153
  for (const listener of elementListeners) listener(stamped);
154
+ for (const listener of ownerListeners) listener();
144
155
  }
145
156
 
146
157
  /** Install the reader of "which document is active". Called once by the host
@@ -161,3 +172,16 @@ export function onHistoryElement(listener: (element: HistoryElement) => void): (
161
172
  elementListeners.add(listener);
162
173
  return () => elementListeners.delete(listener);
163
174
  }
175
+
176
+ /** A native document was replaced or its snapshot owner was closed. */
177
+ export function invalidateHistoryResources(resources: readonly string[]): void {
178
+ const expired = new Set(resources);
179
+ recorded = recorded.filter(element => !element.resources.some(path => expired.has(path)));
180
+ for (const listener of invalidationListeners) listener(resources);
181
+ for (const listener of ownerListeners) listener();
182
+ }
183
+
184
+ export function onHistoryInvalidated(listener: (resources: readonly string[]) => void): () => void {
185
+ invalidationListeners.add(listener);
186
+ return () => invalidationListeners.delete(listener);
187
+ }
@@ -1,4 +1,4 @@
1
- import { emitHistoryElement, historyDelegate, historyDelegateInstalled } from './history-delegate';
1
+ import { emitHistoryElement, historyDelegate, historyDelegateInstalled, subscribeHistoryDelegate } from './history-delegate';
2
2
  import { PersistenceCoordinator } from './persistence-coordinator';
3
3
  import { ResourceRegistry } from './resource-registry';
4
4
  import { SnapshotStore } from './snapshot-store';
@@ -236,6 +236,7 @@ export class HistoryService {
236
236
  private droppedTotal = 0;
237
237
  private executionTail: Promise<void> = Promise.resolve();
238
238
  private publicSnapshot: HistorySnapshot;
239
+ private readonly unsubscribeOwner: () => void;
239
240
 
240
241
  constructor(options: HistoryServiceOptions = {}) {
241
242
  this.registry = options.registry ?? new ResourceRegistry();
@@ -257,6 +258,7 @@ export class HistoryService {
257
258
  throw new Error('History limits must be positive.');
258
259
  }
259
260
  this.publicSnapshot = this.buildSnapshot();
261
+ this.unsubscribeOwner = subscribeHistoryDelegate(() => this.notify());
260
262
  }
261
263
 
262
264
  subscribe = (listener: () => void): (() => void) => {
@@ -265,7 +267,17 @@ export class HistoryService {
265
267
  return () => this.listeners.delete(listener);
266
268
  };
267
269
 
268
- getSnapshot = (): HistorySnapshot => this.publicSnapshot;
270
+ getSnapshot = (): HistorySnapshot => {
271
+ // Focus may move between documents without changing any history entry.
272
+ // Keep the external-store snapshot stable unless the native answer changes.
273
+ if (historyDelegateInstalled()) {
274
+ const next = this.buildSnapshot();
275
+ if (next.canUndo !== this.publicSnapshot.canUndo || next.canRedo !== this.publicSnapshot.canRedo ||
276
+ next.undoLabel !== this.publicSnapshot.undoLabel || next.redoLabel !== this.publicSnapshot.redoLabel)
277
+ this.publicSnapshot = next;
278
+ }
279
+ return this.publicSnapshot;
280
+ };
269
281
 
270
282
  subscribeCommits(listener: (transaction: HistoryTransaction) => void): () => void {
271
283
  this.assertNotDisposed();
@@ -447,15 +459,18 @@ export class HistoryService {
447
459
  * ONE stack instead of walking a cursor nobody is driving. A frame that has
448
460
  * not installed its undo yet gets a named refusal rather than silence.
449
461
  */
450
- private delegateOrRefuse(direction: 'undo' | 'redo'): boolean {
462
+ private async delegateOrRefuse(direction: 'undo' | 'redo'): Promise<boolean> {
451
463
  const delegate = historyDelegate();
452
464
  if (delegate) {
453
465
  // A handled keyboard command is not evidence that history moved. In
454
466
  // particular, a native document may have no entry on Code-OSS's stack.
455
467
  if (!(direction === 'undo' ? delegate.canUndo() : delegate.canRedo())) return false;
456
- if (direction === 'undo') delegate.undo();
457
- else delegate.redo();
458
- return true;
468
+ try {
469
+ const moved = direction === 'undo' ? await delegate.undo() : await delegate.redo();
470
+ return moved !== false;
471
+ } finally {
472
+ this.notify();
473
+ }
459
474
  }
460
475
  this.lastErrorValue = historyError(
461
476
  'busy',
@@ -634,6 +649,7 @@ export class HistoryService {
634
649
  dispose(): void {
635
650
  if (this.disposeRequested) return;
636
651
  this.disposeRequested = true;
652
+ this.unsubscribeOwner();
637
653
  this.listeners.clear();
638
654
  this.commitListeners.clear();
639
655
  this.finishDisposeIfIdle();
@@ -1162,8 +1178,8 @@ export class HistoryService {
1162
1178
  canRedo: historyDelegateInstalled()
1163
1179
  ? (historyDelegate()?.canRedo() ?? false)
1164
1180
  : idle && !!redo && redo.status !== 'expired' && !this.blockedValue,
1165
- undoLabel: undo?.label ?? null,
1166
- redoLabel: redo?.label ?? null,
1181
+ undoLabel: historyDelegateInstalled() ? historyDelegate()?.undoLabel?.() ?? null : undo?.label ?? null,
1182
+ redoLabel: historyDelegateInstalled() ? historyDelegate()?.redoLabel?.() ?? null : redo?.label ?? null,
1167
1183
  uniqueSnapshotBytes: this.snapshots.uniqueByteLength,
1168
1184
  lastError: this.lastErrorValue,
1169
1185
  limitWarning: this.limitWarningValue,