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