@bian-womp/spark-workbench 0.2.65 → 0.2.67

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 (37) hide show
  1. package/lib/cjs/index.cjs +500 -215
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +13 -0
  4. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  5. package/lib/cjs/src/index.d.ts +1 -0
  6. package/lib/cjs/src/index.d.ts.map +1 -1
  7. package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -1
  8. package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/SelectionContextMenu.d.ts +3 -0
  10. package/lib/cjs/src/misc/SelectionContextMenu.d.ts.map +1 -0
  11. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts +18 -0
  13. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts +39 -2
  15. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
  16. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +46 -0
  17. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  18. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  19. package/lib/esm/index.js +495 -216
  20. package/lib/esm/index.js.map +1 -1
  21. package/lib/esm/src/core/InMemoryWorkbench.d.ts +13 -0
  22. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  23. package/lib/esm/src/index.d.ts +1 -0
  24. package/lib/esm/src/index.d.ts.map +1 -1
  25. package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -1
  26. package/lib/esm/src/misc/NodeContextMenu.d.ts.map +1 -1
  27. package/lib/esm/src/misc/SelectionContextMenu.d.ts +3 -0
  28. package/lib/esm/src/misc/SelectionContextMenu.d.ts.map +1 -0
  29. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  30. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts +18 -0
  31. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
  32. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts +39 -2
  33. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
  34. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +46 -0
  35. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  36. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  37. package/package.json +4 -4
package/lib/cjs/index.cjs CHANGED
@@ -131,6 +131,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
131
131
  edges: [],
132
132
  };
133
133
  this.viewport = null;
134
+ this.copiedData = null;
134
135
  }
135
136
  setRegistry(registry) {
136
137
  this.registry = registry;
@@ -307,6 +308,22 @@ class InMemoryWorkbench extends AbstractWorkbench {
307
308
  edges: [...this.selection.edges],
308
309
  };
309
310
  }
311
+ /**
312
+ * Delete all selected nodes and edges.
313
+ */
314
+ deleteSelection() {
315
+ const selection = this.getSelection();
316
+ // Delete all selected nodes (this will also remove connected edges)
317
+ for (const nodeId of selection.nodes) {
318
+ this.removeNode(nodeId);
319
+ }
320
+ // Delete remaining selected edges (edges not connected to deleted nodes)
321
+ for (const edgeId of selection.edges) {
322
+ this.disconnect(edgeId);
323
+ }
324
+ // Clear selection
325
+ this.setSelection({ nodes: [], edges: [] });
326
+ }
310
327
  setViewport(viewport, opts) {
311
328
  this.viewport = { ...viewport };
312
329
  this.emit("graphUiChanged", {
@@ -503,6 +520,18 @@ class InMemoryWorkbench extends AbstractWorkbench {
503
520
  });
504
521
  return { nodeIdMap, edgeIds };
505
522
  }
523
+ /**
524
+ * Get the currently copied graph data.
525
+ */
526
+ getCopiedData() {
527
+ return this.copiedData;
528
+ }
529
+ /**
530
+ * Set the copied graph data (used for paste functionality).
531
+ */
532
+ setCopiedData(data) {
533
+ this.copiedData = data;
534
+ }
506
535
  }
507
536
 
508
537
  class CLIWorkbench {
@@ -3049,6 +3078,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3049
3078
  updateEdgeType,
3050
3079
  triggerExternal,
3051
3080
  uiVersion,
3081
+ overrides,
3052
3082
  }), [
3053
3083
  wb,
3054
3084
  runner,
@@ -3088,10 +3118,272 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3088
3118
  wb,
3089
3119
  runner,
3090
3120
  uiVersion,
3121
+ overrides,
3091
3122
  ]);
3092
3123
  return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
3093
3124
  }
3094
3125
 
3126
+ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
3127
+ const doBake = async (handleId) => {
3128
+ try {
3129
+ const typeId = outputTypesMap?.[nodeId]?.[handleId];
3130
+ const raw = outputsMap?.[nodeId]?.[handleId];
3131
+ if (!typeId || raw === undefined)
3132
+ return;
3133
+ const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
3134
+ const coerceIfNeeded = async (fromType, toType, value) => {
3135
+ if (!toType || toType === fromType || !runner?.coerce)
3136
+ return value;
3137
+ try {
3138
+ return await runner.coerce(fromType, toType, value);
3139
+ }
3140
+ catch {
3141
+ return value;
3142
+ }
3143
+ };
3144
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3145
+ const isArray = typeId.endsWith("[]");
3146
+ const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3147
+ const tArr = isArray ? registry.types.get(typeId) : undefined;
3148
+ const tElem = registry.types.get(baseTypeId);
3149
+ const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3150
+ const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3151
+ const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3152
+ if (singleTarget) {
3153
+ const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3154
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3155
+ const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3156
+ wb.addNode({
3157
+ typeId: singleTarget.nodeTypeId,
3158
+ position: { x: pos.x + 180, y: pos.y },
3159
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
3160
+ return;
3161
+ }
3162
+ if (isArray && arrTarget) {
3163
+ const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3164
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3165
+ const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3166
+ wb.addNode({
3167
+ typeId: arrTarget.nodeTypeId,
3168
+ position: { x: pos.x + 180, y: pos.y },
3169
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
3170
+ return;
3171
+ }
3172
+ if (isArray && elemTarget) {
3173
+ const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3174
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3175
+ const src = unwrap(raw);
3176
+ const items = Array.isArray(src) ? src : [src];
3177
+ const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3178
+ const COLS = 4;
3179
+ const DX = 180;
3180
+ const DY = 160;
3181
+ for (let idx = 0; idx < coercedItems.length; idx++) {
3182
+ const col = idx % COLS;
3183
+ const row = Math.floor(idx / COLS);
3184
+ wb.addNode({
3185
+ typeId: elemTarget.nodeTypeId,
3186
+ position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3187
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3188
+ }
3189
+ return;
3190
+ }
3191
+ }
3192
+ catch { }
3193
+ };
3194
+ return {
3195
+ onDelete: () => {
3196
+ wb.removeNode(nodeId);
3197
+ onClose();
3198
+ },
3199
+ onDuplicate: async () => {
3200
+ const def = wb.export();
3201
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3202
+ if (!n)
3203
+ return onClose();
3204
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3205
+ const inboundHandles = new Set(def.edges
3206
+ .filter((e) => e.target.nodeId === nodeId)
3207
+ .map((e) => e.target.handle));
3208
+ const allInputs = runner.getInputs(def)[nodeId] || {};
3209
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3210
+ const newNodeId = wb.addNode({
3211
+ typeId: n.typeId,
3212
+ params: n.params,
3213
+ position: { x: pos.x + 24, y: pos.y + 24 },
3214
+ resolvedHandles: n.resolvedHandles,
3215
+ }, {
3216
+ inputs: inputsWithoutBindings,
3217
+ copyOutputsFrom: nodeId,
3218
+ dry: true,
3219
+ });
3220
+ // Select the newly duplicated node
3221
+ wb.setSelection({
3222
+ nodes: [newNodeId],
3223
+ edges: [],
3224
+ });
3225
+ onClose();
3226
+ },
3227
+ onDuplicateWithEdges: async () => {
3228
+ const def = wb.export();
3229
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3230
+ if (!n)
3231
+ return onClose();
3232
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3233
+ // Get inputs without bindings (literal values only)
3234
+ const inputs = runner.getInputs(def)[nodeId] || {};
3235
+ // Add the duplicated node
3236
+ const newNodeId = wb.addNode({
3237
+ typeId: n.typeId,
3238
+ params: n.params,
3239
+ position: { x: pos.x + 24, y: pos.y + 24 },
3240
+ resolvedHandles: n.resolvedHandles,
3241
+ }, {
3242
+ inputs,
3243
+ copyOutputsFrom: nodeId,
3244
+ dry: true,
3245
+ });
3246
+ // Find all incoming edges (edges where target is the original node)
3247
+ const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3248
+ // Duplicate each incoming edge to point to the new node
3249
+ for (const edge of incomingEdges) {
3250
+ wb.connect({
3251
+ source: edge.source, // Keep the same source
3252
+ target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3253
+ typeId: edge.typeId,
3254
+ }, { dry: true });
3255
+ }
3256
+ // Select the newly duplicated node and edges
3257
+ wb.setSelection({
3258
+ nodes: [newNodeId],
3259
+ edges: [],
3260
+ });
3261
+ onClose();
3262
+ },
3263
+ onRunPull: async () => {
3264
+ try {
3265
+ await runner.computeNode(nodeId);
3266
+ }
3267
+ catch { }
3268
+ onClose();
3269
+ },
3270
+ onBake: async (handleId) => {
3271
+ await doBake(handleId);
3272
+ onClose();
3273
+ },
3274
+ onCopy: () => {
3275
+ const copyHandler = createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult);
3276
+ copyHandler();
3277
+ onClose();
3278
+ },
3279
+ onCopyId: async () => {
3280
+ try {
3281
+ await navigator.clipboard.writeText(nodeId);
3282
+ }
3283
+ catch { }
3284
+ onClose();
3285
+ },
3286
+ onClose,
3287
+ };
3288
+ }
3289
+ /**
3290
+ * Creates a copy handler that copies the current selection and optionally stores the result.
3291
+ */
3292
+ function createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult) {
3293
+ return () => {
3294
+ const getNodeSize = getDefaultNodeSize
3295
+ ? (nodeId, typeId) => getDefaultNodeSize(typeId)
3296
+ : undefined;
3297
+ const data = wb.copySelection(runner, getNodeSize);
3298
+ if (onCopyResult) {
3299
+ onCopyResult(data);
3300
+ }
3301
+ };
3302
+ }
3303
+ /**
3304
+ * Creates a copy handler for a single node (temporarily selects it, copies, then restores selection).
3305
+ */
3306
+ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult) {
3307
+ return () => {
3308
+ // Select the node first, then copy
3309
+ const currentSelection = wb.getSelection();
3310
+ wb.setSelection({ nodes: [nodeId], edges: [] });
3311
+ const getNodeSize = getDefaultNodeSize
3312
+ ? (nodeId, typeId) => getDefaultNodeSize(typeId)
3313
+ : undefined;
3314
+ const data = wb.copySelection(runner, getNodeSize);
3315
+ // Restore original selection
3316
+ wb.setSelection(currentSelection);
3317
+ if (onCopyResult) {
3318
+ onCopyResult(data);
3319
+ }
3320
+ };
3321
+ }
3322
+ /**
3323
+ * Creates base selection context menu handlers.
3324
+ */
3325
+ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
3326
+ return {
3327
+ onCopy: runner
3328
+ ? createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult)
3329
+ : () => {
3330
+ // No-op if runner not available
3331
+ onClose();
3332
+ },
3333
+ onDelete: () => {
3334
+ wb.deleteSelection();
3335
+ onClose();
3336
+ },
3337
+ onClose,
3338
+ };
3339
+ }
3340
+ /**
3341
+ * Creates base default context menu handlers.
3342
+ */
3343
+ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste) {
3344
+ return {
3345
+ onAddNode,
3346
+ onPaste,
3347
+ onClose,
3348
+ };
3349
+ }
3350
+ function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3351
+ try {
3352
+ const def = wb.export();
3353
+ const node = def.nodes.find((n) => n.nodeId === nodeId);
3354
+ if (!node)
3355
+ return [];
3356
+ const desc = registry.nodes.get(node.typeId);
3357
+ const handles = Object.keys(desc?.outputs || {});
3358
+ const out = [];
3359
+ for (const h of handles) {
3360
+ const tId = outputTypesMap?.[nodeId]?.[h];
3361
+ if (!tId)
3362
+ continue;
3363
+ if (tId.endsWith("[]")) {
3364
+ const base = tId.slice(0, -2);
3365
+ const tArr = registry.types.get(tId);
3366
+ const tElem = registry.types.get(base);
3367
+ const arrT = tArr?.bakeTarget;
3368
+ const elemT = tElem?.bakeTarget;
3369
+ if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3370
+ (elemT && registry.nodes.has(elemT.nodeTypeId)))
3371
+ out.push(h);
3372
+ }
3373
+ else {
3374
+ const t = registry.types.get(tId);
3375
+ const bt = t?.bakeTarget;
3376
+ if (bt && registry.nodes.has(bt.nodeTypeId))
3377
+ out.push(h);
3378
+ }
3379
+ }
3380
+ return out;
3381
+ }
3382
+ catch {
3383
+ return [];
3384
+ }
3385
+ }
3386
+
3095
3387
  function IssueBadge({ level, title, size = 12, className, }) {
3096
3388
  const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
3097
3389
  return (jsxRuntime.jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsxRuntime.jsx(react$1.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react$1.WarningCircleIcon, { size: size, weight: "fill" })) }));
@@ -3304,7 +3596,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3304
3596
  }
3305
3597
  catch { }
3306
3598
  };
3307
- return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), jsxRuntime.jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
3599
+ return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto select-none`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), jsxRuntime.jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
3308
3600
  e.stopPropagation();
3309
3601
  deleteEdgeById(m.data?.edgeId);
3310
3602
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), renderEdgeStatus(), jsxRuntime.jsx("div", { className: "mt-1", children: jsxRuntime.jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
@@ -3372,7 +3664,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3372
3664
  const title = inIssues
3373
3665
  .map((v) => `${v.code}: ${v.message}`)
3374
3666
  .join("; ");
3375
- return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", value: current !== undefined && current !== null
3667
+ return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1 select-text", value: current !== undefined && current !== null
3376
3668
  ? String(current)
3377
3669
  : "", onChange: (e) => {
3378
3670
  const val = e.target.value;
@@ -3386,7 +3678,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3386
3678
  ? `Default: ${placeholder}`
3387
3679
  : "(select)" }), registry.enums
3388
3680
  .get(typeId)
3389
- ?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", placeholder: placeholder
3681
+ ?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1 select-text", placeholder: placeholder
3390
3682
  ? `Default: ${placeholder}`
3391
3683
  : undefined, value: displayValue, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
3392
3684
  if (e.key === "Enter")
@@ -3639,6 +3931,13 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3639
3931
  handlers.onAddNode(typeId, { position: p });
3640
3932
  handlers.onClose();
3641
3933
  };
3934
+ const handlePaste = () => {
3935
+ if (!handlers.onPaste)
3936
+ return;
3937
+ const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
3938
+ handlers.onPaste(p);
3939
+ handlers.onClose();
3940
+ };
3642
3941
  const renderTree = (tree, path = []) => {
3643
3942
  const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
3644
3943
  return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
@@ -3653,10 +3952,10 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3653
3952
  return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wide text-gray-400", children: label }), child.__self && (jsxRuntime.jsx("button", { onClick: () => handleClick(child.__self), className: "block w-full text-left px-3 py-1 hover:bg-gray-100 cursor-pointer", title: child.__self, children: label })), jsxRuntime.jsx("div", { className: "pl-2 border-l border-gray-200 ml-2", children: renderTree(child, [...path, key]) })] }, idKey));
3654
3953
  }) }));
3655
3954
  };
3656
- return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-none 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) => {
3955
+ 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 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3657
3956
  e.preventDefault();
3658
3957
  e.stopPropagation();
3659
- }, 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" })) })] }));
3958
+ }, children: [handlers.onPaste && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed", onClick: handlePaste, children: "Paste" })), handlers.onPaste && jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), 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 select-text", 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" })) })] }));
3660
3959
  }
3661
3960
 
3662
3961
  function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
@@ -3694,209 +3993,55 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
3694
3993
  const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
3695
3994
  (MENU_MIN_WIDTH + PADDING));
3696
3995
  const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
3697
- 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) => {
3996
+ 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 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3698
3997
  e.preventDefault();
3699
3998
  e.stopPropagation();
3700
- }, 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" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), 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" })] }));
3999
+ }, 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" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), 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" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" }), bakeableOutputs.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), 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)))] }))] }));
3701
4000
  }
3702
4001
 
3703
- function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose) {
3704
- const doBake = async (handleId) => {
3705
- try {
3706
- const typeId = outputTypesMap?.[nodeId]?.[handleId];
3707
- const raw = outputsMap?.[nodeId]?.[handleId];
3708
- if (!typeId || raw === undefined)
3709
- return;
3710
- const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
3711
- const coerceIfNeeded = async (fromType, toType, value) => {
3712
- if (!toType || toType === fromType || !runner?.coerce)
3713
- return value;
3714
- try {
3715
- return await runner.coerce(fromType, toType, value);
3716
- }
3717
- catch {
3718
- return value;
3719
- }
3720
- };
3721
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3722
- const isArray = typeId.endsWith("[]");
3723
- const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3724
- const tArr = isArray ? registry.types.get(typeId) : undefined;
3725
- const tElem = registry.types.get(baseTypeId);
3726
- const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3727
- const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3728
- const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3729
- if (singleTarget) {
3730
- const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3731
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3732
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3733
- wb.addNode({
3734
- typeId: singleTarget.nodeTypeId,
3735
- position: { x: pos.x + 180, y: pos.y },
3736
- }, { inputs: { [singleTarget.inputHandle]: coerced } });
3737
- return;
3738
- }
3739
- if (isArray && arrTarget) {
3740
- const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3741
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3742
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3743
- wb.addNode({
3744
- typeId: arrTarget.nodeTypeId,
3745
- position: { x: pos.x + 180, y: pos.y },
3746
- }, { inputs: { [arrTarget.inputHandle]: coerced } });
3747
- return;
3748
- }
3749
- if (isArray && elemTarget) {
3750
- const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3751
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3752
- const src = unwrap(raw);
3753
- const items = Array.isArray(src) ? src : [src];
3754
- const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3755
- const COLS = 4;
3756
- const DX = 180;
3757
- const DY = 160;
3758
- for (let idx = 0; idx < coercedItems.length; idx++) {
3759
- const col = idx % COLS;
3760
- const row = Math.floor(idx / COLS);
3761
- wb.addNode({
3762
- typeId: elemTarget.nodeTypeId,
3763
- position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3764
- }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3765
- }
4002
+ function SelectionContextMenu({ open, clientPos, handlers, }) {
4003
+ const ref = React.useRef(null);
4004
+ // Close on outside click and on ESC
4005
+ React.useEffect(() => {
4006
+ if (!open)
4007
+ return;
4008
+ const onDown = (e) => {
4009
+ if (!ref.current)
3766
4010
  return;
3767
- }
3768
- }
3769
- catch { }
3770
- };
3771
- return {
3772
- onDelete: () => {
3773
- wb.removeNode(nodeId);
3774
- onClose();
3775
- },
3776
- onDuplicate: async () => {
3777
- const def = wb.export();
3778
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3779
- if (!n)
3780
- return onClose();
3781
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3782
- const inboundHandles = new Set(def.edges
3783
- .filter((e) => e.target.nodeId === nodeId)
3784
- .map((e) => e.target.handle));
3785
- const allInputs = runner.getInputs(def)[nodeId] || {};
3786
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3787
- const newNodeId = wb.addNode({
3788
- typeId: n.typeId,
3789
- params: n.params,
3790
- position: { x: pos.x + 24, y: pos.y + 24 },
3791
- resolvedHandles: n.resolvedHandles,
3792
- }, {
3793
- inputs: inputsWithoutBindings,
3794
- copyOutputsFrom: nodeId,
3795
- dry: true,
3796
- });
3797
- // Select the newly duplicated node
3798
- wb.setSelection({
3799
- nodes: [newNodeId],
3800
- edges: [],
3801
- });
3802
- onClose();
3803
- },
3804
- onDuplicateWithEdges: async () => {
3805
- const def = wb.export();
3806
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3807
- if (!n)
3808
- return onClose();
3809
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3810
- // Get inputs without bindings (literal values only)
3811
- const inputs = runner.getInputs(def)[nodeId] || {};
3812
- // Add the duplicated node
3813
- const newNodeId = wb.addNode({
3814
- typeId: n.typeId,
3815
- params: n.params,
3816
- position: { x: pos.x + 24, y: pos.y + 24 },
3817
- resolvedHandles: n.resolvedHandles,
3818
- }, {
3819
- inputs,
3820
- copyOutputsFrom: nodeId,
3821
- dry: true,
3822
- });
3823
- // Find all incoming edges (edges where target is the original node)
3824
- const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3825
- // Duplicate each incoming edge to point to the new node
3826
- for (const edge of incomingEdges) {
3827
- wb.connect({
3828
- source: edge.source, // Keep the same source
3829
- target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3830
- typeId: edge.typeId,
3831
- }, { dry: true });
3832
- }
3833
- // Select the newly duplicated node and edges
3834
- wb.setSelection({
3835
- nodes: [newNodeId],
3836
- edges: [],
3837
- });
3838
- onClose();
3839
- },
3840
- onRunPull: async () => {
3841
- try {
3842
- await runner.computeNode(nodeId);
3843
- }
3844
- catch { }
3845
- onClose();
3846
- },
3847
- onBake: async (handleId) => {
3848
- await doBake(handleId);
3849
- onClose();
3850
- },
3851
- onCopyId: async () => {
3852
- try {
3853
- await navigator.clipboard.writeText(nodeId);
3854
- }
3855
- catch { }
3856
- onClose();
3857
- },
3858
- onClose,
3859
- };
3860
- }
3861
- function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3862
- try {
3863
- const def = wb.export();
3864
- const node = def.nodes.find((n) => n.nodeId === nodeId);
3865
- if (!node)
3866
- return [];
3867
- const desc = registry.nodes.get(node.typeId);
3868
- const handles = Object.keys(desc?.outputs || {});
3869
- const out = [];
3870
- for (const h of handles) {
3871
- const tId = outputTypesMap?.[nodeId]?.[h];
3872
- if (!tId)
3873
- continue;
3874
- if (tId.endsWith("[]")) {
3875
- const base = tId.slice(0, -2);
3876
- const tArr = registry.types.get(tId);
3877
- const tElem = registry.types.get(base);
3878
- const arrT = tArr?.bakeTarget;
3879
- const elemT = tElem?.bakeTarget;
3880
- if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3881
- (elemT && registry.nodes.has(elemT.nodeTypeId)))
3882
- out.push(h);
3883
- }
3884
- else {
3885
- const t = registry.types.get(tId);
3886
- const bt = t?.bakeTarget;
3887
- if (bt && registry.nodes.has(bt.nodeTypeId))
3888
- out.push(h);
3889
- }
3890
- }
3891
- return out;
3892
- }
3893
- catch {
3894
- return [];
3895
- }
4011
+ if (!ref.current.contains(e.target))
4012
+ handlers.onClose();
4013
+ };
4014
+ const onKey = (e) => {
4015
+ if (e.key === "Escape")
4016
+ handlers.onClose();
4017
+ };
4018
+ window.addEventListener("mousedown", onDown, true);
4019
+ window.addEventListener("keydown", onKey);
4020
+ return () => {
4021
+ window.removeEventListener("mousedown", onDown, true);
4022
+ window.removeEventListener("keydown", onKey);
4023
+ };
4024
+ }, [open, handlers]);
4025
+ React.useEffect(() => {
4026
+ if (open)
4027
+ ref.current?.focus();
4028
+ }, [open]);
4029
+ if (!open || !clientPos)
4030
+ return null;
4031
+ // Clamp menu position to viewport
4032
+ const MENU_MIN_WIDTH = 180;
4033
+ const PADDING = 16;
4034
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
4035
+ (MENU_MIN_WIDTH + PADDING));
4036
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 100);
4037
+ 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 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
4038
+ e.preventDefault();
4039
+ e.stopPropagation();
4040
+ }, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" })] }));
3896
4041
  }
3897
4042
 
3898
4043
  const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
3899
- const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, } = useWorkbenchContext();
4044
+ const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, overrides, } = useWorkbenchContext();
3900
4045
  const nodeValidation = validationByNode;
3901
4046
  const edgeValidation = validationByEdge.errors;
3902
4047
  const [registryVersion, setRegistryVersion] = React.useState(0);
@@ -4135,23 +4280,100 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4135
4280
  const [nodeMenuOpen, setNodeMenuOpen] = React.useState(false);
4136
4281
  const [nodeMenuPos, setNodeMenuPos] = React.useState(null);
4137
4282
  const [nodeAtMenu, setNodeAtMenu] = React.useState(null);
4283
+ const [selectionMenuPos, setSelectionMenuPos] = React.useState(null);
4284
+ const [selectionMenuOpen, setSelectionMenuOpen] = React.useState(false);
4285
+ // Compute the rectangular screen-space bounds of the current selection
4286
+ const getSelectionScreenBounds = () => {
4287
+ if (typeof document === "undefined")
4288
+ return null;
4289
+ const selection = wb.getSelection();
4290
+ if (!selection.nodes.length)
4291
+ return null;
4292
+ let bounds = null;
4293
+ for (const nodeId of selection.nodes) {
4294
+ const el = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`);
4295
+ if (!el)
4296
+ continue;
4297
+ const rect = el.getBoundingClientRect();
4298
+ if (!bounds) {
4299
+ bounds = {
4300
+ left: rect.left,
4301
+ top: rect.top,
4302
+ right: rect.right,
4303
+ bottom: rect.bottom,
4304
+ };
4305
+ }
4306
+ else {
4307
+ bounds.left = Math.min(bounds.left, rect.left);
4308
+ bounds.top = Math.min(bounds.top, rect.top);
4309
+ bounds.right = Math.max(bounds.right, rect.right);
4310
+ bounds.bottom = Math.max(bounds.bottom, rect.bottom);
4311
+ }
4312
+ }
4313
+ return bounds;
4314
+ };
4138
4315
  const onContextMenu = (e) => {
4139
4316
  e.preventDefault();
4140
- // Determine if right-clicked over a node by hit-testing selection
4317
+ // First, check if the cursor is inside the rectangular bounds of the current selection
4318
+ const selectionBounds = getSelectionScreenBounds();
4319
+ if (selectionBounds) {
4320
+ const { left, top, right, bottom } = selectionBounds;
4321
+ if (e.clientX >= left &&
4322
+ e.clientX <= right &&
4323
+ e.clientY >= top &&
4324
+ e.clientY <= bottom) {
4325
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4326
+ setSelectionMenuOpen(true);
4327
+ setMenuOpen(false);
4328
+ setNodeMenuOpen(false);
4329
+ return;
4330
+ }
4331
+ }
4332
+ // Determine if right-clicked over a node by hit-testing
4141
4333
  const target = e.target?.closest(".react-flow__node");
4142
4334
  if (target) {
4143
4335
  // Resolve node id from data-id attribute React Flow sets
4144
4336
  const nodeId = target.getAttribute("data-id");
4145
- setNodeAtMenu(nodeId);
4146
- setNodeMenuPos({ x: e.clientX, y: e.clientY });
4147
- setNodeMenuOpen(true);
4148
- setMenuOpen(false);
4337
+ const selection = wb.getSelection();
4338
+ const isSelected = nodeId && selection.nodes.includes(nodeId);
4339
+ if (isSelected) {
4340
+ // Right-clicked on a selected node - show selection menu
4341
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4342
+ setSelectionMenuOpen(true);
4343
+ setMenuOpen(false);
4344
+ setNodeMenuOpen(false);
4345
+ return;
4346
+ }
4347
+ else {
4348
+ // Right-clicked on a non-selected node - show node menu
4349
+ setNodeAtMenu(nodeId);
4350
+ setNodeMenuPos({ x: e.clientX, y: e.clientY });
4351
+ setNodeMenuOpen(true);
4352
+ setMenuOpen(false);
4353
+ setSelectionMenuOpen(false);
4354
+ return;
4355
+ }
4149
4356
  }
4150
- else {
4151
- setMenuPos({ x: e.clientX, y: e.clientY });
4152
- setMenuOpen(true);
4153
- setNodeMenuOpen(false);
4357
+ // Check if right-clicked on a selected edge
4358
+ const edgeTarget = e.target?.closest(".react-flow__edge");
4359
+ if (edgeTarget) {
4360
+ const edgeId = edgeTarget.getAttribute("data-id");
4361
+ const selection = wb.getSelection();
4362
+ const isSelected = edgeId && selection.edges.includes(edgeId);
4363
+ if (isSelected) {
4364
+ // Right-clicked on a selected edge - show selection menu
4365
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4366
+ setSelectionMenuOpen(true);
4367
+ setMenuOpen(false);
4368
+ setNodeMenuOpen(false);
4369
+ return;
4370
+ }
4154
4371
  }
4372
+ // Right-clicked on empty space - show default menu
4373
+ setMenuPos({ x: e.clientX, y: e.clientY });
4374
+ setMenuOpen(true);
4375
+ setNodeMenuOpen(false);
4376
+ setSelectionMenuOpen(false);
4155
4377
  };
4156
4378
  const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
4157
4379
  const onCloseMenu = React.useCallback(() => {
@@ -4160,6 +4382,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4160
4382
  const onCloseNodeMenu = React.useCallback(() => {
4161
4383
  setNodeMenuOpen(false);
4162
4384
  }, []);
4385
+ const onCloseSelectionMenu = React.useCallback(() => {
4386
+ setSelectionMenuOpen(false);
4387
+ }, []);
4163
4388
  React.useEffect(() => {
4164
4389
  const off = runner.on("registry", () => {
4165
4390
  setRegistryVersion((v) => v + 1);
@@ -4167,14 +4392,65 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4167
4392
  return () => off();
4168
4393
  }, [runner]);
4169
4394
  const nodeIds = React.useMemo(() => Array.from(registry.nodes.keys()), [registry, registryVersion]);
4170
- const defaultContextMenuHandlers = React.useMemo(() => ({
4171
- onAddNode: addNodeAt,
4172
- onClose: onCloseMenu,
4173
- }), [addNodeAt, onCloseMenu]);
4395
+ const defaultContextMenuHandlers = React.useMemo(() => {
4396
+ // Get storage from override or use workbench's internal storage
4397
+ const storage = overrides?.getCopiedDataStorage
4398
+ ? overrides.getCopiedDataStorage()
4399
+ : {
4400
+ get: () => wb.getCopiedData(),
4401
+ set: (data) => wb.setCopiedData(data),
4402
+ };
4403
+ const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu,
4404
+ // Paste handler - checks storage dynamically when called
4405
+ // Only provide handler if storage has data or might have data (for dynamic checking)
4406
+ (position) => {
4407
+ const data = storage.get();
4408
+ if (!data)
4409
+ return;
4410
+ wb.pasteCopiedData(data, position);
4411
+ onCloseMenu();
4412
+ });
4413
+ if (overrides?.getDefaultContextMenuHandlers) {
4414
+ return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
4415
+ }
4416
+ return baseHandlers;
4417
+ }, [addNodeAt, onCloseMenu, overrides, wb]);
4418
+ const selectionContextMenuHandlers = React.useMemo(() => {
4419
+ // Get storage from override or use workbench's internal storage
4420
+ const storage = overrides?.getCopiedDataStorage
4421
+ ? overrides.getCopiedDataStorage()
4422
+ : {
4423
+ get: () => wb.getCopiedData(),
4424
+ set: (data) => wb.setCopiedData(data),
4425
+ };
4426
+ const baseHandlers = createSelectionContextMenuHandlers(wb, onCloseSelectionMenu, overrides?.getDefaultNodeSize, (data) => {
4427
+ storage.set(data);
4428
+ }, runner);
4429
+ if (overrides?.getSelectionContextMenuHandlers) {
4430
+ const selection = wb.getSelection();
4431
+ return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
4432
+ getDefaultNodeSize: overrides.getDefaultNodeSize,
4433
+ });
4434
+ }
4435
+ return baseHandlers;
4436
+ }, [wb, runner, overrides, onCloseSelectionMenu]);
4174
4437
  const nodeContextMenuHandlers = React.useMemo(() => {
4175
4438
  if (!nodeAtMenu)
4176
4439
  return null;
4177
- return createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu);
4440
+ // Get storage from override or use workbench's internal storage
4441
+ const storage = overrides?.getCopiedDataStorage
4442
+ ? overrides.getCopiedDataStorage()
4443
+ : {
4444
+ get: () => wb.getCopiedData(),
4445
+ set: (data) => wb.setCopiedData(data),
4446
+ };
4447
+ const baseHandlers = createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu, overrides?.getDefaultNodeSize, (data) => {
4448
+ storage.set(data);
4449
+ });
4450
+ if (overrides?.getNodeContextMenuHandlers) {
4451
+ return overrides.getNodeContextMenuHandlers(wb, nodeAtMenu, baseHandlers);
4452
+ }
4453
+ return baseHandlers;
4178
4454
  }, [
4179
4455
  nodeAtMenu,
4180
4456
  wb,
@@ -4183,6 +4459,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4183
4459
  outputsMap,
4184
4460
  outputTypesMap,
4185
4461
  onCloseNodeMenu,
4462
+ overrides?.getDefaultNodeSize,
4463
+ overrides?.getNodeContextMenuHandlers,
4464
+ overrides?.getCopiedDataStorage,
4186
4465
  ]);
4187
4466
  const canRunPull = React.useMemo(() => engineKind()?.toString() === "pull", [engineKind]);
4188
4467
  const bakeableOutputs = React.useMemo(() => {
@@ -4238,7 +4517,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4238
4517
  }
4239
4518
  }, 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 &&
4240
4519
  nodeContextMenuHandlers &&
4241
- (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 })))] }) }) }));
4520
+ (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 }))), selectionMenuOpen && selectionMenuPos && (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers }))] }) }) }));
4242
4521
  });
4243
4522
 
4244
4523
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
@@ -4792,12 +5071,18 @@ exports.WorkbenchProvider = WorkbenchProvider;
4792
5071
  exports.WorkbenchStudio = WorkbenchStudio;
4793
5072
  exports.computeEffectiveHandles = computeEffectiveHandles;
4794
5073
  exports.countVisibleHandles = countVisibleHandles;
5074
+ exports.createCopyHandler = createCopyHandler;
5075
+ exports.createDefaultContextMenuHandlers = createDefaultContextMenuHandlers;
4795
5076
  exports.createHandleBounds = createHandleBounds;
4796
5077
  exports.createHandleLayout = createHandleLayout;
5078
+ exports.createNodeContextMenuHandlers = createNodeContextMenuHandlers;
5079
+ exports.createNodeCopyHandler = createNodeCopyHandler;
5080
+ exports.createSelectionContextMenuHandlers = createSelectionContextMenuHandlers;
4797
5081
  exports.download = download;
4798
5082
  exports.estimateNodeSize = estimateNodeSize;
4799
5083
  exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
4800
5084
  exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
5085
+ exports.getBakeableOutputs = getBakeableOutputs;
4801
5086
  exports.getHandleBoundsX = getHandleBoundsX;
4802
5087
  exports.getHandleBoundsY = getHandleBoundsY;
4803
5088
  exports.getHandleClassName = getHandleClassName;