@bian-womp/spark-workbench 0.2.65 → 0.2.66

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 +453 -215
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +4 -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 +41 -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 +448 -216
  20. package/lib/esm/index.js.map +1 -1
  21. package/lib/esm/src/core/InMemoryWorkbench.d.ts +4 -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 +41 -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
@@ -307,6 +307,22 @@ class InMemoryWorkbench extends AbstractWorkbench {
307
307
  edges: [...this.selection.edges],
308
308
  };
309
309
  }
310
+ /**
311
+ * Delete all selected nodes and edges.
312
+ */
313
+ deleteSelection() {
314
+ const selection = this.getSelection();
315
+ // Delete all selected nodes (this will also remove connected edges)
316
+ for (const nodeId of selection.nodes) {
317
+ this.removeNode(nodeId);
318
+ }
319
+ // Delete remaining selected edges (edges not connected to deleted nodes)
320
+ for (const edgeId of selection.edges) {
321
+ this.disconnect(edgeId);
322
+ }
323
+ // Clear selection
324
+ this.setSelection({ nodes: [], edges: [] });
325
+ }
310
326
  setViewport(viewport, opts) {
311
327
  this.viewport = { ...viewport };
312
328
  this.emit("graphUiChanged", {
@@ -3049,6 +3065,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3049
3065
  updateEdgeType,
3050
3066
  triggerExternal,
3051
3067
  uiVersion,
3068
+ overrides,
3052
3069
  }), [
3053
3070
  wb,
3054
3071
  runner,
@@ -3088,10 +3105,272 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3088
3105
  wb,
3089
3106
  runner,
3090
3107
  uiVersion,
3108
+ overrides,
3091
3109
  ]);
3092
3110
  return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
3093
3111
  }
3094
3112
 
3113
+ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
3114
+ const doBake = async (handleId) => {
3115
+ try {
3116
+ const typeId = outputTypesMap?.[nodeId]?.[handleId];
3117
+ const raw = outputsMap?.[nodeId]?.[handleId];
3118
+ if (!typeId || raw === undefined)
3119
+ return;
3120
+ const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
3121
+ const coerceIfNeeded = async (fromType, toType, value) => {
3122
+ if (!toType || toType === fromType || !runner?.coerce)
3123
+ return value;
3124
+ try {
3125
+ return await runner.coerce(fromType, toType, value);
3126
+ }
3127
+ catch {
3128
+ return value;
3129
+ }
3130
+ };
3131
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3132
+ const isArray = typeId.endsWith("[]");
3133
+ const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3134
+ const tArr = isArray ? registry.types.get(typeId) : undefined;
3135
+ const tElem = registry.types.get(baseTypeId);
3136
+ const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3137
+ const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3138
+ const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3139
+ if (singleTarget) {
3140
+ const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3141
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3142
+ const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3143
+ wb.addNode({
3144
+ typeId: singleTarget.nodeTypeId,
3145
+ position: { x: pos.x + 180, y: pos.y },
3146
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
3147
+ return;
3148
+ }
3149
+ if (isArray && arrTarget) {
3150
+ const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3151
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3152
+ const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3153
+ wb.addNode({
3154
+ typeId: arrTarget.nodeTypeId,
3155
+ position: { x: pos.x + 180, y: pos.y },
3156
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
3157
+ return;
3158
+ }
3159
+ if (isArray && elemTarget) {
3160
+ const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3161
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3162
+ const src = unwrap(raw);
3163
+ const items = Array.isArray(src) ? src : [src];
3164
+ const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3165
+ const COLS = 4;
3166
+ const DX = 180;
3167
+ const DY = 160;
3168
+ for (let idx = 0; idx < coercedItems.length; idx++) {
3169
+ const col = idx % COLS;
3170
+ const row = Math.floor(idx / COLS);
3171
+ wb.addNode({
3172
+ typeId: elemTarget.nodeTypeId,
3173
+ position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3174
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3175
+ }
3176
+ return;
3177
+ }
3178
+ }
3179
+ catch { }
3180
+ };
3181
+ return {
3182
+ onDelete: () => {
3183
+ wb.removeNode(nodeId);
3184
+ onClose();
3185
+ },
3186
+ onDuplicate: async () => {
3187
+ const def = wb.export();
3188
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3189
+ if (!n)
3190
+ return onClose();
3191
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3192
+ const inboundHandles = new Set(def.edges
3193
+ .filter((e) => e.target.nodeId === nodeId)
3194
+ .map((e) => e.target.handle));
3195
+ const allInputs = runner.getInputs(def)[nodeId] || {};
3196
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3197
+ const newNodeId = wb.addNode({
3198
+ typeId: n.typeId,
3199
+ params: n.params,
3200
+ position: { x: pos.x + 24, y: pos.y + 24 },
3201
+ resolvedHandles: n.resolvedHandles,
3202
+ }, {
3203
+ inputs: inputsWithoutBindings,
3204
+ copyOutputsFrom: nodeId,
3205
+ dry: true,
3206
+ });
3207
+ // Select the newly duplicated node
3208
+ wb.setSelection({
3209
+ nodes: [newNodeId],
3210
+ edges: [],
3211
+ });
3212
+ onClose();
3213
+ },
3214
+ onDuplicateWithEdges: async () => {
3215
+ const def = wb.export();
3216
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3217
+ if (!n)
3218
+ return onClose();
3219
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3220
+ // Get inputs without bindings (literal values only)
3221
+ const inputs = runner.getInputs(def)[nodeId] || {};
3222
+ // Add the duplicated node
3223
+ const newNodeId = wb.addNode({
3224
+ typeId: n.typeId,
3225
+ params: n.params,
3226
+ position: { x: pos.x + 24, y: pos.y + 24 },
3227
+ resolvedHandles: n.resolvedHandles,
3228
+ }, {
3229
+ inputs,
3230
+ copyOutputsFrom: nodeId,
3231
+ dry: true,
3232
+ });
3233
+ // Find all incoming edges (edges where target is the original node)
3234
+ const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3235
+ // Duplicate each incoming edge to point to the new node
3236
+ for (const edge of incomingEdges) {
3237
+ wb.connect({
3238
+ source: edge.source, // Keep the same source
3239
+ target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3240
+ typeId: edge.typeId,
3241
+ }, { dry: true });
3242
+ }
3243
+ // Select the newly duplicated node and edges
3244
+ wb.setSelection({
3245
+ nodes: [newNodeId],
3246
+ edges: [],
3247
+ });
3248
+ onClose();
3249
+ },
3250
+ onRunPull: async () => {
3251
+ try {
3252
+ await runner.computeNode(nodeId);
3253
+ }
3254
+ catch { }
3255
+ onClose();
3256
+ },
3257
+ onBake: async (handleId) => {
3258
+ await doBake(handleId);
3259
+ onClose();
3260
+ },
3261
+ onCopy: () => {
3262
+ const copyHandler = createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult);
3263
+ copyHandler();
3264
+ onClose();
3265
+ },
3266
+ onCopyId: async () => {
3267
+ try {
3268
+ await navigator.clipboard.writeText(nodeId);
3269
+ }
3270
+ catch { }
3271
+ onClose();
3272
+ },
3273
+ onClose,
3274
+ };
3275
+ }
3276
+ /**
3277
+ * Creates a copy handler that copies the current selection and optionally stores the result.
3278
+ */
3279
+ function createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult) {
3280
+ return () => {
3281
+ const getNodeSize = getDefaultNodeSize
3282
+ ? (nodeId, typeId) => getDefaultNodeSize(typeId)
3283
+ : undefined;
3284
+ const data = wb.copySelection(runner, getNodeSize);
3285
+ if (onCopyResult) {
3286
+ onCopyResult(data);
3287
+ }
3288
+ };
3289
+ }
3290
+ /**
3291
+ * Creates a copy handler for a single node (temporarily selects it, copies, then restores selection).
3292
+ */
3293
+ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult) {
3294
+ return () => {
3295
+ // Select the node first, then copy
3296
+ const currentSelection = wb.getSelection();
3297
+ wb.setSelection({ nodes: [nodeId], edges: [] });
3298
+ const getNodeSize = getDefaultNodeSize
3299
+ ? (nodeId, typeId) => getDefaultNodeSize(typeId)
3300
+ : undefined;
3301
+ const data = wb.copySelection(runner, getNodeSize);
3302
+ // Restore original selection
3303
+ wb.setSelection(currentSelection);
3304
+ if (onCopyResult) {
3305
+ onCopyResult(data);
3306
+ }
3307
+ };
3308
+ }
3309
+ /**
3310
+ * Creates base selection context menu handlers.
3311
+ */
3312
+ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
3313
+ return {
3314
+ onCopy: runner
3315
+ ? createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult)
3316
+ : () => {
3317
+ // No-op if runner not available
3318
+ onClose();
3319
+ },
3320
+ onDelete: () => {
3321
+ wb.deleteSelection();
3322
+ onClose();
3323
+ },
3324
+ onClose,
3325
+ };
3326
+ }
3327
+ /**
3328
+ * Creates base default context menu handlers.
3329
+ */
3330
+ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste) {
3331
+ return {
3332
+ onAddNode,
3333
+ onPaste,
3334
+ onClose,
3335
+ };
3336
+ }
3337
+ function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3338
+ try {
3339
+ const def = wb.export();
3340
+ const node = def.nodes.find((n) => n.nodeId === nodeId);
3341
+ if (!node)
3342
+ return [];
3343
+ const desc = registry.nodes.get(node.typeId);
3344
+ const handles = Object.keys(desc?.outputs || {});
3345
+ const out = [];
3346
+ for (const h of handles) {
3347
+ const tId = outputTypesMap?.[nodeId]?.[h];
3348
+ if (!tId)
3349
+ continue;
3350
+ if (tId.endsWith("[]")) {
3351
+ const base = tId.slice(0, -2);
3352
+ const tArr = registry.types.get(tId);
3353
+ const tElem = registry.types.get(base);
3354
+ const arrT = tArr?.bakeTarget;
3355
+ const elemT = tElem?.bakeTarget;
3356
+ if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3357
+ (elemT && registry.nodes.has(elemT.nodeTypeId)))
3358
+ out.push(h);
3359
+ }
3360
+ else {
3361
+ const t = registry.types.get(tId);
3362
+ const bt = t?.bakeTarget;
3363
+ if (bt && registry.nodes.has(bt.nodeTypeId))
3364
+ out.push(h);
3365
+ }
3366
+ }
3367
+ return out;
3368
+ }
3369
+ catch {
3370
+ return [];
3371
+ }
3372
+ }
3373
+
3095
3374
  function IssueBadge({ level, title, size = 12, className, }) {
3096
3375
  const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
3097
3376
  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 +3583,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3304
3583
  }
3305
3584
  catch { }
3306
3585
  };
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) => {
3586
+ 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
3587
  e.stopPropagation();
3309
3588
  deleteEdgeById(m.data?.edgeId);
3310
3589
  }, 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 +3651,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3372
3651
  const title = inIssues
3373
3652
  .map((v) => `${v.code}: ${v.message}`)
3374
3653
  .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
3654
+ 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
3655
  ? String(current)
3377
3656
  : "", onChange: (e) => {
3378
3657
  const val = e.target.value;
@@ -3386,7 +3665,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3386
3665
  ? `Default: ${placeholder}`
3387
3666
  : "(select)" }), registry.enums
3388
3667
  .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
3668
+ ?.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
3669
  ? `Default: ${placeholder}`
3391
3670
  : undefined, value: displayValue, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
3392
3671
  if (e.key === "Enter")
@@ -3639,6 +3918,13 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3639
3918
  handlers.onAddNode(typeId, { position: p });
3640
3919
  handlers.onClose();
3641
3920
  };
3921
+ const handlePaste = () => {
3922
+ if (!handlers.onPaste)
3923
+ return;
3924
+ const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
3925
+ handlers.onPaste(p);
3926
+ handlers.onClose();
3927
+ };
3642
3928
  const renderTree = (tree, path = []) => {
3643
3929
  const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
3644
3930
  return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
@@ -3653,10 +3939,10 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3653
3939
  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
3940
  }) }));
3655
3941
  };
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) => {
3942
+ 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
3943
  e.preventDefault();
3658
3944
  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" })) })] }));
3945
+ }, 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
3946
  }
3661
3947
 
3662
3948
  function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
@@ -3694,209 +3980,55 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
3694
3980
  const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
3695
3981
  (MENU_MIN_WIDTH + PADDING));
3696
3982
  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) => {
3983
+ 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
3984
  e.preventDefault();
3699
3985
  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" })] }));
3986
+ }, 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
3987
  }
3702
3988
 
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
- }
3989
+ function SelectionContextMenu({ open, clientPos, handlers, }) {
3990
+ const ref = React.useRef(null);
3991
+ // Close on outside click and on ESC
3992
+ React.useEffect(() => {
3993
+ if (!open)
3994
+ return;
3995
+ const onDown = (e) => {
3996
+ if (!ref.current)
3766
3997
  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
- }
3998
+ if (!ref.current.contains(e.target))
3999
+ handlers.onClose();
4000
+ };
4001
+ const onKey = (e) => {
4002
+ if (e.key === "Escape")
4003
+ handlers.onClose();
4004
+ };
4005
+ window.addEventListener("mousedown", onDown, true);
4006
+ window.addEventListener("keydown", onKey);
4007
+ return () => {
4008
+ window.removeEventListener("mousedown", onDown, true);
4009
+ window.removeEventListener("keydown", onKey);
4010
+ };
4011
+ }, [open, handlers]);
4012
+ React.useEffect(() => {
4013
+ if (open)
4014
+ ref.current?.focus();
4015
+ }, [open]);
4016
+ if (!open || !clientPos)
4017
+ return null;
4018
+ // Clamp menu position to viewport
4019
+ const MENU_MIN_WIDTH = 180;
4020
+ const PADDING = 16;
4021
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
4022
+ (MENU_MIN_WIDTH + PADDING));
4023
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 100);
4024
+ 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) => {
4025
+ e.preventDefault();
4026
+ e.stopPropagation();
4027
+ }, 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
4028
  }
3897
4029
 
3898
4030
  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();
4031
+ const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, overrides, } = useWorkbenchContext();
3900
4032
  const nodeValidation = validationByNode;
3901
4033
  const edgeValidation = validationByEdge.errors;
3902
4034
  const [registryVersion, setRegistryVersion] = React.useState(0);
@@ -4135,23 +4267,100 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4135
4267
  const [nodeMenuOpen, setNodeMenuOpen] = React.useState(false);
4136
4268
  const [nodeMenuPos, setNodeMenuPos] = React.useState(null);
4137
4269
  const [nodeAtMenu, setNodeAtMenu] = React.useState(null);
4270
+ const [selectionMenuPos, setSelectionMenuPos] = React.useState(null);
4271
+ const [selectionMenuOpen, setSelectionMenuOpen] = React.useState(false);
4272
+ // Compute the rectangular screen-space bounds of the current selection
4273
+ const getSelectionScreenBounds = () => {
4274
+ if (typeof document === "undefined")
4275
+ return null;
4276
+ const selection = wb.getSelection();
4277
+ if (!selection.nodes.length)
4278
+ return null;
4279
+ let bounds = null;
4280
+ for (const nodeId of selection.nodes) {
4281
+ const el = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`);
4282
+ if (!el)
4283
+ continue;
4284
+ const rect = el.getBoundingClientRect();
4285
+ if (!bounds) {
4286
+ bounds = {
4287
+ left: rect.left,
4288
+ top: rect.top,
4289
+ right: rect.right,
4290
+ bottom: rect.bottom,
4291
+ };
4292
+ }
4293
+ else {
4294
+ bounds.left = Math.min(bounds.left, rect.left);
4295
+ bounds.top = Math.min(bounds.top, rect.top);
4296
+ bounds.right = Math.max(bounds.right, rect.right);
4297
+ bounds.bottom = Math.max(bounds.bottom, rect.bottom);
4298
+ }
4299
+ }
4300
+ return bounds;
4301
+ };
4138
4302
  const onContextMenu = (e) => {
4139
4303
  e.preventDefault();
4140
- // Determine if right-clicked over a node by hit-testing selection
4304
+ // First, check if the cursor is inside the rectangular bounds of the current selection
4305
+ const selectionBounds = getSelectionScreenBounds();
4306
+ if (selectionBounds) {
4307
+ const { left, top, right, bottom } = selectionBounds;
4308
+ if (e.clientX >= left &&
4309
+ e.clientX <= right &&
4310
+ e.clientY >= top &&
4311
+ e.clientY <= bottom) {
4312
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4313
+ setSelectionMenuOpen(true);
4314
+ setMenuOpen(false);
4315
+ setNodeMenuOpen(false);
4316
+ return;
4317
+ }
4318
+ }
4319
+ // Determine if right-clicked over a node by hit-testing
4141
4320
  const target = e.target?.closest(".react-flow__node");
4142
4321
  if (target) {
4143
4322
  // Resolve node id from data-id attribute React Flow sets
4144
4323
  const nodeId = target.getAttribute("data-id");
4145
- setNodeAtMenu(nodeId);
4146
- setNodeMenuPos({ x: e.clientX, y: e.clientY });
4147
- setNodeMenuOpen(true);
4148
- setMenuOpen(false);
4324
+ const selection = wb.getSelection();
4325
+ const isSelected = nodeId && selection.nodes.includes(nodeId);
4326
+ if (isSelected) {
4327
+ // Right-clicked on a selected node - show selection menu
4328
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4329
+ setSelectionMenuOpen(true);
4330
+ setMenuOpen(false);
4331
+ setNodeMenuOpen(false);
4332
+ return;
4333
+ }
4334
+ else {
4335
+ // Right-clicked on a non-selected node - show node menu
4336
+ setNodeAtMenu(nodeId);
4337
+ setNodeMenuPos({ x: e.clientX, y: e.clientY });
4338
+ setNodeMenuOpen(true);
4339
+ setMenuOpen(false);
4340
+ setSelectionMenuOpen(false);
4341
+ return;
4342
+ }
4149
4343
  }
4150
- else {
4151
- setMenuPos({ x: e.clientX, y: e.clientY });
4152
- setMenuOpen(true);
4153
- setNodeMenuOpen(false);
4344
+ // Check if right-clicked on a selected edge
4345
+ const edgeTarget = e.target?.closest(".react-flow__edge");
4346
+ if (edgeTarget) {
4347
+ const edgeId = edgeTarget.getAttribute("data-id");
4348
+ const selection = wb.getSelection();
4349
+ const isSelected = edgeId && selection.edges.includes(edgeId);
4350
+ if (isSelected) {
4351
+ // Right-clicked on a selected edge - show selection menu
4352
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4353
+ setSelectionMenuOpen(true);
4354
+ setMenuOpen(false);
4355
+ setNodeMenuOpen(false);
4356
+ return;
4357
+ }
4154
4358
  }
4359
+ // Right-clicked on empty space - show default menu
4360
+ setMenuPos({ x: e.clientX, y: e.clientY });
4361
+ setMenuOpen(true);
4362
+ setNodeMenuOpen(false);
4363
+ setSelectionMenuOpen(false);
4155
4364
  };
4156
4365
  const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
4157
4366
  const onCloseMenu = React.useCallback(() => {
@@ -4160,6 +4369,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4160
4369
  const onCloseNodeMenu = React.useCallback(() => {
4161
4370
  setNodeMenuOpen(false);
4162
4371
  }, []);
4372
+ const onCloseSelectionMenu = React.useCallback(() => {
4373
+ setSelectionMenuOpen(false);
4374
+ }, []);
4163
4375
  React.useEffect(() => {
4164
4376
  const off = runner.on("registry", () => {
4165
4377
  setRegistryVersion((v) => v + 1);
@@ -4167,14 +4379,32 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4167
4379
  return () => off();
4168
4380
  }, [runner]);
4169
4381
  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]);
4382
+ const defaultContextMenuHandlers = React.useMemo(() => {
4383
+ const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu);
4384
+ if (overrides?.getDefaultContextMenuHandlers) {
4385
+ return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
4386
+ }
4387
+ return baseHandlers;
4388
+ }, [addNodeAt, onCloseMenu, overrides, wb]);
4389
+ const selectionContextMenuHandlers = React.useMemo(() => {
4390
+ const baseHandlers = createSelectionContextMenuHandlers(wb, onCloseSelectionMenu, overrides?.getDefaultNodeSize, undefined, // onCopyResult - will be provided by overrides
4391
+ runner);
4392
+ if (overrides?.getSelectionContextMenuHandlers) {
4393
+ const selection = wb.getSelection();
4394
+ return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
4395
+ getDefaultNodeSize: overrides.getDefaultNodeSize,
4396
+ });
4397
+ }
4398
+ return baseHandlers;
4399
+ }, [wb, runner, overrides, onCloseSelectionMenu]);
4174
4400
  const nodeContextMenuHandlers = React.useMemo(() => {
4175
4401
  if (!nodeAtMenu)
4176
4402
  return null;
4177
- return createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu);
4403
+ const baseHandlers = createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu, overrides?.getDefaultNodeSize);
4404
+ if (overrides?.getNodeContextMenuHandlers) {
4405
+ return overrides.getNodeContextMenuHandlers(wb, nodeAtMenu, baseHandlers);
4406
+ }
4407
+ return baseHandlers;
4178
4408
  }, [
4179
4409
  nodeAtMenu,
4180
4410
  wb,
@@ -4183,6 +4413,8 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4183
4413
  outputsMap,
4184
4414
  outputTypesMap,
4185
4415
  onCloseNodeMenu,
4416
+ overrides?.getDefaultNodeSize,
4417
+ overrides?.getNodeContextMenuHandlers,
4186
4418
  ]);
4187
4419
  const canRunPull = React.useMemo(() => engineKind()?.toString() === "pull", [engineKind]);
4188
4420
  const bakeableOutputs = React.useMemo(() => {
@@ -4238,7 +4470,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4238
4470
  }
4239
4471
  }, 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
4472
  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 })))] }) }) }));
4473
+ (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
4474
  });
4243
4475
 
4244
4476
  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 +5024,18 @@ exports.WorkbenchProvider = WorkbenchProvider;
4792
5024
  exports.WorkbenchStudio = WorkbenchStudio;
4793
5025
  exports.computeEffectiveHandles = computeEffectiveHandles;
4794
5026
  exports.countVisibleHandles = countVisibleHandles;
5027
+ exports.createCopyHandler = createCopyHandler;
5028
+ exports.createDefaultContextMenuHandlers = createDefaultContextMenuHandlers;
4795
5029
  exports.createHandleBounds = createHandleBounds;
4796
5030
  exports.createHandleLayout = createHandleLayout;
5031
+ exports.createNodeContextMenuHandlers = createNodeContextMenuHandlers;
5032
+ exports.createNodeCopyHandler = createNodeCopyHandler;
5033
+ exports.createSelectionContextMenuHandlers = createSelectionContextMenuHandlers;
4797
5034
  exports.download = download;
4798
5035
  exports.estimateNodeSize = estimateNodeSize;
4799
5036
  exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
4800
5037
  exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
5038
+ exports.getBakeableOutputs = getBakeableOutputs;
4801
5039
  exports.getHandleBoundsX = getHandleBoundsX;
4802
5040
  exports.getHandleBoundsY = getHandleBoundsY;
4803
5041
  exports.getHandleClassName = getHandleClassName;