@bian-womp/spark-workbench 0.2.54 → 0.2.55

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 (51) hide show
  1. package/lib/cjs/index.cjs +234 -184
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/AbstractWorkbench.d.ts +2 -0
  4. package/lib/cjs/src/core/AbstractWorkbench.d.ts.map +1 -1
  5. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +0 -1
  6. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  7. package/lib/cjs/src/core/ui-extensions.d.ts +44 -47
  8. package/lib/cjs/src/core/ui-extensions.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/DefaultContextMenu.d.ts +2 -12
  10. package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/NodeContextMenu.d.ts +2 -9
  12. package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts +41 -0
  15. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -0
  16. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts +7 -0
  17. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -0
  18. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +3 -2
  19. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  20. package/lib/cjs/src/runtime/IGraphRunner.d.ts +1 -0
  21. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
  22. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +1 -1
  23. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  24. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +0 -1
  25. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  26. package/lib/esm/index.js +235 -185
  27. package/lib/esm/index.js.map +1 -1
  28. package/lib/esm/src/core/AbstractWorkbench.d.ts +2 -0
  29. package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -1
  30. package/lib/esm/src/core/InMemoryWorkbench.d.ts +0 -1
  31. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  32. package/lib/esm/src/core/ui-extensions.d.ts +44 -47
  33. package/lib/esm/src/core/ui-extensions.d.ts.map +1 -1
  34. package/lib/esm/src/misc/DefaultContextMenu.d.ts +2 -12
  35. package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -1
  36. package/lib/esm/src/misc/NodeContextMenu.d.ts +2 -9
  37. package/lib/esm/src/misc/NodeContextMenu.d.ts.map +1 -1
  38. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  39. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts +41 -0
  40. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -0
  41. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts +7 -0
  42. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -0
  43. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +3 -2
  44. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  45. package/lib/esm/src/runtime/IGraphRunner.d.ts +1 -0
  46. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
  47. package/lib/esm/src/runtime/LocalGraphRunner.d.ts +1 -1
  48. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  49. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +0 -1
  50. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  51. package/package.json +4 -4
package/lib/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { GraphBuilder, createEngine, StepEngine, PullEngine, BatchedEngine, isTypedOutput, getTypedOutputValue, getTypedOutputTypeId, isInputPrivate, getInputTypeId, createSimpleGraphRegistry, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry } from '@bian-womp/spark-graph';
1
+ import { generateId, GraphBuilder, createEngine, StepEngine, PullEngine, BatchedEngine, isTypedOutput, getTypedOutputValue, getTypedOutputTypeId, isInputPrivate, getInputTypeId, createSimpleGraphRegistry, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry } from '@bian-womp/spark-graph';
2
2
  import { RuntimeApiClient } from '@bian-womp/spark-remote';
3
3
  import { Position, Handle, useUpdateNodeInternals, useReactFlow, ReactFlowProvider, ReactFlow, Background, BackgroundVariant, MiniMap, Controls } from '@xyflow/react';
4
4
  import React, { useCallback, useState, useRef, useEffect, useMemo, createContext, useContext, useImperativeHandle } from 'react';
@@ -10,71 +10,75 @@ import isEqual from 'lodash/isEqual';
10
10
  class DefaultUIExtensionRegistry {
11
11
  constructor() {
12
12
  this.nodeRenderers = new Map();
13
- this.portRenderers = new Map();
14
- this.edgeRenderers = new Map();
15
13
  }
16
14
  registerNodeRenderer(nodeTypeId, renderer) {
17
- this.nodeRenderers.set(nodeTypeId, renderer);
15
+ if (renderer === undefined) {
16
+ this.nodeRenderers.delete(nodeTypeId);
17
+ }
18
+ else {
19
+ this.nodeRenderers.set(nodeTypeId, renderer);
20
+ }
18
21
  return this;
19
22
  }
20
23
  getNodeRenderer(nodeTypeId) {
21
24
  return this.nodeRenderers.get(nodeTypeId);
22
25
  }
23
- registerPortRenderer(dataTypeId, renderer) {
24
- this.portRenderers.set(dataTypeId, renderer);
25
- return this;
26
- }
27
- getPortRenderer(dataTypeId) {
28
- return this.portRenderers.get(dataTypeId);
26
+ getAllNodeRenderers() {
27
+ const result = {};
28
+ for (const [nodeTypeId, renderer] of this.nodeRenderers.entries()) {
29
+ result[nodeTypeId] = renderer;
30
+ }
31
+ return result;
29
32
  }
30
- registerEdgeRenderer(typeId, renderer) {
31
- this.edgeRenderers.set(typeId, renderer);
33
+ registerIconProvider(provider) {
34
+ this.iconProvider = provider;
32
35
  return this;
33
36
  }
34
- getEdgeRenderer(typeId) {
35
- return this.edgeRenderers.get(typeId);
37
+ getIconProvider() {
38
+ return this.iconProvider;
36
39
  }
37
- setInspector(renderer) {
38
- this.inspector = renderer;
40
+ // React Flow renderers
41
+ registerConnectionLineRenderer(renderer) {
42
+ this.connectionLineRenderer = renderer;
39
43
  return this;
40
44
  }
41
- getInspector() {
42
- return this.inspector;
45
+ getConnectionLineRenderer() {
46
+ return this.connectionLineRenderer;
43
47
  }
44
- setPalette(renderer) {
45
- this.palette = renderer;
48
+ registerMinimapRenderer(renderer) {
49
+ this.minimapRenderer = renderer;
46
50
  return this;
47
51
  }
48
- getPalette() {
49
- return this.palette;
52
+ getMinimapRenderer() {
53
+ return this.minimapRenderer;
50
54
  }
51
- setToolbar(renderer) {
52
- this.toolbar = renderer;
55
+ registerControlsRenderer(renderer) {
56
+ this.controlsRenderer = renderer;
53
57
  return this;
54
58
  }
55
- getToolbar() {
56
- return this.toolbar;
59
+ getControlsRenderer() {
60
+ return this.controlsRenderer;
57
61
  }
58
- setContextMenu(renderer) {
59
- this.contextMenu = renderer;
62
+ registerBackgroundRenderer(renderer) {
63
+ this.backgroundRenderer = renderer;
60
64
  return this;
61
65
  }
62
- getContextMenu() {
63
- return this.contextMenu;
66
+ getBackgroundRenderer() {
67
+ return this.backgroundRenderer;
64
68
  }
65
- setMiniMap(renderer) {
66
- this.miniMap = renderer;
69
+ registerDefaultContextMenuRenderer(renderer) {
70
+ this.defaultContextMenuRenderer = renderer;
67
71
  return this;
68
72
  }
69
- getMiniMap() {
70
- return this.miniMap;
73
+ getDefaultContextMenuRenderer() {
74
+ return this.defaultContextMenuRenderer;
71
75
  }
72
- setIconProvider(provider) {
73
- this.iconProvider = provider;
76
+ registerNodeContextMenuRenderer(renderer) {
77
+ this.nodeContextMenuRenderer = renderer;
74
78
  return this;
75
79
  }
76
- getIconProvider() {
77
- return this.iconProvider;
80
+ getNodeContextMenuRenderer() {
81
+ return this.nodeContextMenuRenderer;
78
82
  }
79
83
  }
80
84
 
@@ -84,6 +88,7 @@ class AbstractWorkbench {
84
88
  this.layout = args.layout;
85
89
  this.storage = args.storage;
86
90
  this.serializer = args.serializer;
91
+ this.genId = args.genId || generateId;
87
92
  }
88
93
  // Expose UI registry to adapters (React Flow, CLI) to allow overrides
89
94
  getUI() {
@@ -162,11 +167,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
162
167
  return { ok: issues.every((i) => i.level !== "error"), issues };
163
168
  }
164
169
  addNode(node) {
165
- const id = node.nodeId ?? this.generateId("n");
170
+ const id = node.nodeId ??
171
+ this.genId("n", new Set(this.def.nodes.map((n) => n.nodeId)));
166
172
  this.def.nodes.push({
167
173
  nodeId: id,
168
174
  typeId: node.typeId,
169
175
  params: node.params,
176
+ initialInputs: node.initialInputs,
177
+ resolvedHandles: node.resolvedHandles,
170
178
  });
171
179
  if (node.position)
172
180
  this.positions[id] = node.position;
@@ -188,7 +196,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
188
196
  this.refreshValidation();
189
197
  }
190
198
  connect(edge) {
191
- const id = edge.id ?? this.generateId("e");
199
+ const id = edge.id ?? this.genId("e", new Set(this.def.edges.map((e) => e.id)));
192
200
  this.def.edges.push({
193
201
  id,
194
202
  source: { ...edge.source },
@@ -329,9 +337,6 @@ class InMemoryWorkbench extends AbstractWorkbench {
329
337
  for (const h of Array.from(set))
330
338
  h(payload);
331
339
  }
332
- generateId(prefix) {
333
- return `${prefix}${Math.random().toString(36).slice(2, 8)}`;
334
- }
335
340
  }
336
341
 
337
342
  class CLIWorkbench {
@@ -463,6 +468,18 @@ class AbstractGraphRunner {
463
468
  }
464
469
  }
465
470
  }
471
+ getInputDefaults(def) {
472
+ const out = {};
473
+ for (const n of def.nodes) {
474
+ const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
475
+ const graphDefaults = n.initialInputs ?? {};
476
+ const merged = { ...dynDefaults, ...graphDefaults };
477
+ if (Object.keys(merged).length > 0) {
478
+ out[n.nodeId] = merged;
479
+ }
480
+ }
481
+ return out;
482
+ }
466
483
  on(event, handler) {
467
484
  if (!this.listeners.has(event))
468
485
  this.listeners.set(event, new Set());
@@ -624,16 +641,6 @@ class LocalGraphRunner extends AbstractGraphRunner {
624
641
  }
625
642
  return out;
626
643
  }
627
- getInputDefaults(def) {
628
- const out = {};
629
- for (const n of def.nodes) {
630
- const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
631
- if (Object.keys(dynDefaults).length > 0) {
632
- out[n.nodeId] = dynDefaults;
633
- }
634
- }
635
- return out;
636
- }
637
644
  async snapshotFull() {
638
645
  const def = undefined; // UI will supply def/positions on download for local
639
646
  const inputs = this.getInputs(this.runtime
@@ -661,11 +668,25 @@ class LocalGraphRunner extends AbstractGraphRunner {
661
668
  if (payload.def)
662
669
  this.build(payload.def);
663
670
  this.setEnvironment?.(payload.environment || {}, { merge: false });
664
- // Hydrate via runtime for exact restore and re-emit
671
+ this.hydrateSnapshotFull(payload);
672
+ }
673
+ hydrateSnapshotFull(snapshot) {
674
+ // Hydrate via runtime for exact restore (this emits events on runtime emitter)
665
675
  this.runtime?.hydrate({
666
- inputs: payload.inputs || {},
667
- outputs: payload.outputs || {},
676
+ inputs: snapshot.inputs || {},
677
+ outputs: snapshot.outputs || {},
668
678
  });
679
+ // Also emit directly from runner to ensure UI gets events even if engine isn't running
680
+ for (const [nodeId, map] of Object.entries(snapshot.inputs || {})) {
681
+ for (const [handle, value] of Object.entries(map || {})) {
682
+ this.emit("value", { nodeId, handle, value, io: "input" });
683
+ }
684
+ }
685
+ for (const [nodeId, map] of Object.entries(snapshot.outputs || {})) {
686
+ for (const [handle, value] of Object.entries(map || {})) {
687
+ this.emit("value", { nodeId, handle, value, io: "output" });
688
+ }
689
+ }
669
690
  }
670
691
  dispose() {
671
692
  super.dispose();
@@ -1222,16 +1243,6 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1222
1243
  }
1223
1244
  return out;
1224
1245
  }
1225
- getInputDefaults(def) {
1226
- const out = {};
1227
- for (const n of def.nodes) {
1228
- const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
1229
- if (Object.keys(dynDefaults).length > 0) {
1230
- out[n.nodeId] = dynDefaults;
1231
- }
1232
- }
1233
- return out;
1234
- }
1235
1246
  dispose() {
1236
1247
  // Idempotent: allow multiple calls safely
1237
1248
  if (this.disposed)
@@ -3212,15 +3223,13 @@ function DefaultNodeContent({ data, isConnectable, }) {
3212
3223
  } })] }));
3213
3224
  }
3214
3225
 
3215
- function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
3216
- const { registry } = useWorkbenchContext();
3226
+ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3217
3227
  const rf = useReactFlow();
3218
- const ids = Array.from(registry.nodes.keys());
3219
3228
  const [query, setQuery] = useState("");
3220
3229
  const q = query.trim().toLowerCase();
3221
3230
  const filteredIds = q
3222
- ? ids.filter((id) => id.toLowerCase().includes(q))
3223
- : ids;
3231
+ ? nodeIds.filter((id) => id.toLowerCase().includes(q))
3232
+ : nodeIds;
3224
3233
  const root = { __children: {} };
3225
3234
  for (const id of filteredIds) {
3226
3235
  const parts = id.split(".");
@@ -3244,11 +3253,11 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
3244
3253
  if (!ref.current)
3245
3254
  return;
3246
3255
  if (!ref.current.contains(e.target))
3247
- onClose();
3256
+ handlers.onClose();
3248
3257
  };
3249
3258
  const onKey = (e) => {
3250
3259
  if (e.key === "Escape")
3251
- onClose();
3260
+ handlers.onClose();
3252
3261
  };
3253
3262
  window.addEventListener("mousedown", onDown, true);
3254
3263
  window.addEventListener("keydown", onKey);
@@ -3256,7 +3265,7 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
3256
3265
  window.removeEventListener("mousedown", onDown, true);
3257
3266
  window.removeEventListener("keydown", onKey);
3258
3267
  };
3259
- }, [open, onClose]);
3268
+ }, [open, handlers]);
3260
3269
  // Focus search input when menu opens
3261
3270
  const inputRef = useRef(null);
3262
3271
  useEffect(() => {
@@ -3275,8 +3284,8 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
3275
3284
  const handleClick = (typeId) => {
3276
3285
  // project() is deprecated; use screenToFlowPosition for screen coordinates
3277
3286
  const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
3278
- onAdd(typeId, p);
3279
- onClose();
3287
+ handlers.onAddNode(typeId, { position: p });
3288
+ handlers.onClose();
3280
3289
  };
3281
3290
  const renderTree = (tree, path = []) => {
3282
3291
  const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
@@ -3298,8 +3307,7 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
3298
3307
  }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsx("div", { className: "px-2 pb-1", children: jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 px-2 py-1 text-sm outline-none focus:border-gray-400", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
3299
3308
  }
3300
3309
 
3301
- function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3302
- const { wb, runner, engineKind, registry, outputsMap, outputTypesMap } = useWorkbenchContext();
3310
+ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
3303
3311
  const ref = useRef(null);
3304
3312
  // outside click + ESC
3305
3313
  useEffect(() => {
@@ -3309,11 +3317,11 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3309
3317
  if (!ref.current)
3310
3318
  return;
3311
3319
  if (!ref.current.contains(e.target))
3312
- onClose();
3320
+ handlers.onClose();
3313
3321
  };
3314
3322
  const onKey = (e) => {
3315
3323
  if (e.key === "Escape")
3316
- onClose();
3324
+ handlers.onClose();
3317
3325
  };
3318
3326
  window.addEventListener("mousedown", onDown, true);
3319
3327
  window.addEventListener("keydown", onKey);
@@ -3321,48 +3329,26 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3321
3329
  window.removeEventListener("mousedown", onDown, true);
3322
3330
  window.removeEventListener("keydown", onKey);
3323
3331
  };
3324
- }, [open, onClose]);
3332
+ }, [open, handlers]);
3325
3333
  useEffect(() => {
3326
3334
  if (open)
3327
3335
  ref.current?.focus();
3328
3336
  }, [open]);
3329
- // Bake helpers
3330
- const getBakeableOutputs = () => {
3331
- try {
3332
- const def = wb.export();
3333
- const node = def.nodes.find((n) => n.nodeId === nodeId);
3334
- if (!node)
3335
- return [];
3336
- const desc = registry.nodes.get(node.typeId);
3337
- const handles = Object.keys(desc?.outputs || {});
3338
- const out = [];
3339
- for (const h of handles) {
3340
- const tId = outputTypesMap?.[nodeId]?.[h];
3341
- if (!tId)
3342
- continue;
3343
- if (tId.endsWith("[]")) {
3344
- const base = tId.slice(0, -2);
3345
- const tArr = registry.types.get(tId);
3346
- const tElem = registry.types.get(base);
3347
- const arrT = tArr?.bakeTarget;
3348
- const elemT = tElem?.bakeTarget;
3349
- if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3350
- (elemT && registry.nodes.has(elemT.nodeTypeId)))
3351
- out.push(h);
3352
- }
3353
- else {
3354
- const t = registry.types.get(tId);
3355
- const bt = t?.bakeTarget;
3356
- if (bt && registry.nodes.has(bt.nodeTypeId))
3357
- out.push(h);
3358
- }
3359
- }
3360
- return out;
3361
- }
3362
- catch {
3363
- return [];
3364
- }
3365
- };
3337
+ if (!open || !clientPos || !nodeId)
3338
+ return null;
3339
+ // clamp
3340
+ const MENU_MIN_WIDTH = 180;
3341
+ const PADDING = 16;
3342
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
3343
+ (MENU_MIN_WIDTH + PADDING));
3344
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
3345
+ return (jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3346
+ e.preventDefault();
3347
+ e.stopPropagation();
3348
+ }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), bakeableOutputs.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h))), jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" })] }));
3349
+ }
3350
+
3351
+ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose) {
3366
3352
  const doBake = async (handleId) => {
3367
3353
  try {
3368
3354
  const typeId = outputTypesMap?.[nodeId]?.[handleId];
@@ -3395,7 +3381,6 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3395
3381
  const newId = wb.addNode({
3396
3382
  typeId: singleTarget.nodeTypeId,
3397
3383
  position: { x: pos.x + 180, y: pos.y },
3398
- params: {},
3399
3384
  });
3400
3385
  runner.update(wb.export());
3401
3386
  await runner.whenIdle();
@@ -3406,12 +3391,9 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3406
3391
  const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3407
3392
  const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3408
3393
  const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3409
- const newId = `n${Math.random().toString(36).slice(2, 8)}`;
3410
- wb.addNode({
3411
- nodeId: newId,
3394
+ const newId = wb.addNode({
3412
3395
  typeId: arrTarget.nodeTypeId,
3413
3396
  position: { x: pos.x + 180, y: pos.y },
3414
- params: {},
3415
3397
  });
3416
3398
  runner.update(wb.export());
3417
3399
  await runner.whenIdle();
@@ -3429,14 +3411,11 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3429
3411
  const DY = 160;
3430
3412
  const nodeIds = [];
3431
3413
  for (let idx = 0; idx < coercedItems.length; idx++) {
3432
- const cv = coercedItems[idx];
3433
3414
  const col = idx % COLS;
3434
3415
  const row = Math.floor(idx / COLS);
3435
3416
  const newId = wb.addNode({
3436
3417
  typeId: elemTarget.nodeTypeId,
3437
3418
  position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3438
- params: {},
3439
- initialInputs: { [elemTarget.inputHandle]: structuredClone(cv) },
3440
3419
  });
3441
3420
  nodeIds.push(newId);
3442
3421
  }
@@ -3454,63 +3433,88 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3454
3433
  }
3455
3434
  catch { }
3456
3435
  };
3457
- // actions
3458
- const handleDelete = useCallback(() => {
3459
- wb.removeNode(nodeId);
3460
- onClose();
3461
- }, [nodeId, wb, onClose]);
3462
- const handleDuplicate = useCallback(() => {
3436
+ return {
3437
+ onDelete: () => {
3438
+ wb.removeNode(nodeId);
3439
+ onClose();
3440
+ },
3441
+ onDuplicate: async () => {
3442
+ const def = wb.export();
3443
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3444
+ if (!n)
3445
+ return onClose();
3446
+ const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3447
+ const newId = wb.addNode({
3448
+ typeId: n.typeId,
3449
+ params: n.params,
3450
+ position: { x: pos.x + 24, y: pos.y + 24 },
3451
+ initialInputs: n.initialInputs,
3452
+ resolvedHandles: n.resolvedHandles,
3453
+ });
3454
+ await runner.whenIdle();
3455
+ runner.setInputs(newId, { ...runner.getInputs(def)[nodeId] });
3456
+ onClose();
3457
+ },
3458
+ onRunPull: async () => {
3459
+ try {
3460
+ await runner.computeNode(nodeId);
3461
+ }
3462
+ catch { }
3463
+ onClose();
3464
+ },
3465
+ onBake: async (handleId) => {
3466
+ await doBake(handleId);
3467
+ onClose();
3468
+ },
3469
+ onCopyId: async () => {
3470
+ try {
3471
+ await navigator.clipboard.writeText(nodeId);
3472
+ }
3473
+ catch { }
3474
+ onClose();
3475
+ },
3476
+ onClose,
3477
+ };
3478
+ }
3479
+ function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3480
+ try {
3463
3481
  const def = wb.export();
3464
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3465
- if (!n)
3466
- return onClose();
3467
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3468
- wb.addNode({
3469
- typeId: n.typeId,
3470
- params: n.params,
3471
- position: { x: pos.x + 24, y: pos.y + 24 },
3472
- });
3473
- onClose();
3474
- }, [nodeId, wb, onClose]);
3475
- useCallback(async (handleId) => {
3476
- await doBake(handleId);
3477
- onClose();
3478
- }, [doBake, onClose]);
3479
- const handleCopyId = useCallback(async () => {
3480
- try {
3481
- await navigator.clipboard.writeText(nodeId);
3482
- }
3483
- catch { }
3484
- onClose();
3485
- }, [nodeId, onClose]);
3486
- const handleRunPull = useCallback(async () => {
3487
- try {
3488
- await runner.computeNode(nodeId);
3482
+ const node = def.nodes.find((n) => n.nodeId === nodeId);
3483
+ if (!node)
3484
+ return [];
3485
+ const desc = registry.nodes.get(node.typeId);
3486
+ const handles = Object.keys(desc?.outputs || {});
3487
+ const out = [];
3488
+ for (const h of handles) {
3489
+ const tId = outputTypesMap?.[nodeId]?.[h];
3490
+ if (!tId)
3491
+ continue;
3492
+ if (tId.endsWith("[]")) {
3493
+ const base = tId.slice(0, -2);
3494
+ const tArr = registry.types.get(tId);
3495
+ const tElem = registry.types.get(base);
3496
+ const arrT = tArr?.bakeTarget;
3497
+ const elemT = tElem?.bakeTarget;
3498
+ if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3499
+ (elemT && registry.nodes.has(elemT.nodeTypeId)))
3500
+ out.push(h);
3501
+ }
3502
+ else {
3503
+ const t = registry.types.get(tId);
3504
+ const bt = t?.bakeTarget;
3505
+ if (bt && registry.nodes.has(bt.nodeTypeId))
3506
+ out.push(h);
3507
+ }
3489
3508
  }
3490
- catch { }
3491
- onClose();
3492
- }, [nodeId, runner, onClose]);
3493
- if (!open || !clientPos || !nodeId)
3494
- return null;
3495
- // clamp
3496
- const MENU_MIN_WIDTH = 180;
3497
- const PADDING = 16;
3498
- const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
3499
- (MENU_MIN_WIDTH + PADDING));
3500
- const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
3501
- const canRunPull = engineKind()?.toString() === "pull";
3502
- const outs = getBakeableOutputs();
3503
- return (jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3504
- e.preventDefault();
3505
- e.stopPropagation();
3506
- }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDuplicate, children: "Duplicate" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), outs.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), outs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: async () => {
3507
- await doBake(h);
3508
- onClose();
3509
- }, children: ["Bake: ", h] }, h))), jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleCopyId, children: "Copy Node ID" })] }));
3509
+ return out;
3510
+ }
3511
+ catch {
3512
+ return [];
3513
+ }
3510
3514
  }
3511
3515
 
3512
3516
  const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
3513
- const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, } = useWorkbenchContext();
3517
+ const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, } = useWorkbenchContext();
3514
3518
  const nodeValidation = validationByNode;
3515
3519
  const edgeValidation = validationByEdge.errors;
3516
3520
  // Keep stable references for nodes/edges to avoid unnecessary updates
@@ -3577,9 +3581,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3577
3581
  },
3578
3582
  }));
3579
3583
  const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, } = useWorkbenchBridge(wb);
3584
+ const ui = wb.getUI();
3580
3585
  const { nodeTypes, resolveNodeType } = useMemo(() => {
3581
3586
  // Build nodeTypes map using UI extension registry
3582
- const ui = wb.getUI();
3583
3587
  const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
3584
3588
  const def = wb.export();
3585
3589
  const ids = new Set([
@@ -3601,7 +3605,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3601
3605
  const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark-${nodeTypeId}` : "spark-default";
3602
3606
  return { nodeTypes: types, resolveNodeType: resolver };
3603
3607
  // Include uiVersion to recompute when custom renderers are registered
3604
- }, [wb, registry, uiVersion]);
3608
+ }, [wb, registry, uiVersion, ui]);
3605
3609
  const { nodes, edges } = useMemo(() => {
3606
3610
  const def = wb.export();
3607
3611
  const sel = wb.getSelection();
@@ -3765,15 +3769,59 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3765
3769
  setNodeMenuOpen(false);
3766
3770
  }
3767
3771
  };
3768
- const addNodeAt = useCallback((typeId, pos) => {
3769
- wb.addNode({ typeId, position: pos });
3770
- }, [wb]);
3772
+ const addNodeAt = useCallback(async (typeId, opts) => {
3773
+ const nodeId = wb.addNode({
3774
+ typeId,
3775
+ initialInputs: opts.initialInputs,
3776
+ position: opts.position,
3777
+ });
3778
+ if (opts.inputs) {
3779
+ runner.update(wb.export());
3780
+ await runner.whenIdle();
3781
+ runner.setInputs(nodeId, opts.inputs);
3782
+ }
3783
+ }, [wb, runner]);
3771
3784
  const onCloseMenu = useCallback(() => {
3772
3785
  setMenuOpen(false);
3773
3786
  }, []);
3774
3787
  const onCloseNodeMenu = useCallback(() => {
3775
3788
  setNodeMenuOpen(false);
3776
3789
  }, []);
3790
+ const nodeIds = useMemo(() => Array.from(registry.nodes.keys()), [registry]);
3791
+ const defaultContextMenuHandlers = useMemo(() => ({
3792
+ onAddNode: addNodeAt,
3793
+ onClose: onCloseMenu,
3794
+ }), [addNodeAt, onCloseMenu]);
3795
+ const nodeContextMenuHandlers = useMemo(() => {
3796
+ if (!nodeAtMenu)
3797
+ return null;
3798
+ return createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu);
3799
+ }, [
3800
+ nodeAtMenu,
3801
+ wb,
3802
+ runner,
3803
+ registry,
3804
+ outputsMap,
3805
+ outputTypesMap,
3806
+ onCloseNodeMenu,
3807
+ ]);
3808
+ const canRunPull = useMemo(() => engineKind()?.toString() === "pull", [engineKind]);
3809
+ const bakeableOutputs = useMemo(() => {
3810
+ if (!nodeAtMenu)
3811
+ return [];
3812
+ return getBakeableOutputs(nodeAtMenu, wb, registry, outputTypesMap);
3813
+ }, [nodeAtMenu, wb, registry, outputTypesMap]);
3814
+ // Get custom renderers from UI extension registry (reactive to uiVersion changes)
3815
+ const { BackgroundRenderer, MinimapRenderer, ControlsRenderer, DefaultContextMenuRenderer, NodeContextMenuRenderer, connectionLineRenderer, } = useMemo(() => {
3816
+ return {
3817
+ BackgroundRenderer: ui.getBackgroundRenderer(),
3818
+ MinimapRenderer: ui.getMinimapRenderer(),
3819
+ ControlsRenderer: ui.getControlsRenderer(),
3820
+ DefaultContextMenuRenderer: ui.getDefaultContextMenuRenderer(),
3821
+ NodeContextMenuRenderer: ui.getNodeContextMenuRenderer(),
3822
+ connectionLineRenderer: ui.getConnectionLineRenderer(),
3823
+ };
3824
+ }, [ui, uiVersion]);
3777
3825
  const onMoveEnd = useCallback(() => {
3778
3826
  if (rfInstanceRef.current) {
3779
3827
  const viewport = rfInstanceRef.current.getViewport();
@@ -3798,7 +3846,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3798
3846
  });
3799
3847
  }
3800
3848
  });
3801
- return (jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsx(ReactFlowProvider, { children: jsxs(ReactFlow, { nodes: throttled.nodes, edges: throttled.edges, nodeTypes: nodeTypes, selectionOnDrag: true, onInit: (inst) => {
3849
+ return (jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsx(ReactFlowProvider, { children: jsxs(ReactFlow, { nodes: throttled.nodes, edges: throttled.edges, nodeTypes: nodeTypes, connectionLineComponent: connectionLineRenderer, selectionOnDrag: true, onInit: (inst) => {
3802
3850
  rfInstanceRef.current = inst;
3803
3851
  const savedViewport = wb.getViewport();
3804
3852
  if (savedViewport) {
@@ -3809,7 +3857,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3809
3857
  zoom: savedViewport.zoom,
3810
3858
  });
3811
3859
  }
3812
- }, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [jsx(Background, { id: "workbench-canvas-background", variant: BackgroundVariant.Dots, gap: 12, size: 1 }), jsx(MiniMap, {}), jsx(Controls, {}), jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: onCloseMenu }), !!nodeAtMenu && (jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: onCloseNodeMenu }))] }) }) }));
3860
+ }, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [BackgroundRenderer ? (jsx(BackgroundRenderer, {})) : (jsx(Background, { id: "workbench-canvas-background", variant: BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsx(MinimapRenderer, {}) : jsx(MiniMap, {}), ControlsRenderer ? jsx(ControlsRenderer, {}) : jsx(Controls, {}), DefaultContextMenuRenderer ? (jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })) : (jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })), !!nodeAtMenu &&
3861
+ nodeContextMenuHandlers &&
3862
+ (NodeContextMenuRenderer ? (jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })))] }) }) }));
3813
3863
  });
3814
3864
 
3815
3865
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {