@bian-womp/spark-workbench 0.2.64 → 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 +589 -205
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +54 -1
  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 +584 -206
  20. package/lib/esm/index.js.map +1 -1
  21. package/lib/esm/src/core/InMemoryWorkbench.d.ts +54 -1
  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/esm/index.js CHANGED
@@ -305,6 +305,22 @@ class InMemoryWorkbench extends AbstractWorkbench {
305
305
  edges: [...this.selection.edges],
306
306
  };
307
307
  }
308
+ /**
309
+ * Delete all selected nodes and edges.
310
+ */
311
+ deleteSelection() {
312
+ const selection = this.getSelection();
313
+ // Delete all selected nodes (this will also remove connected edges)
314
+ for (const nodeId of selection.nodes) {
315
+ this.removeNode(nodeId);
316
+ }
317
+ // Delete remaining selected edges (edges not connected to deleted nodes)
318
+ for (const edgeId of selection.edges) {
319
+ this.disconnect(edgeId);
320
+ }
321
+ // Clear selection
322
+ this.setSelection({ nodes: [], edges: [] });
323
+ }
308
324
  setViewport(viewport, opts) {
309
325
  this.viewport = { ...viewport };
310
326
  this.emit("graphUiChanged", {
@@ -365,6 +381,142 @@ class InMemoryWorkbench extends AbstractWorkbench {
365
381
  for (const h of Array.from(set))
366
382
  h(payload);
367
383
  }
384
+ /**
385
+ * Copy selected nodes and their internal edges.
386
+ * Returns data in a format suitable for pasting.
387
+ * Positions are normalized relative to the selection bounds center.
388
+ * Uses the same logic as duplicate: copies inputs without bindings and supports copyOutputsFrom.
389
+ */
390
+ copySelection(runner, getNodeSize) {
391
+ const selection = this.getSelection();
392
+ if (selection.nodes.length === 0)
393
+ return null;
394
+ const def = this.export();
395
+ const positions = this.getPositions();
396
+ const selectedNodeSet = new Set(selection.nodes);
397
+ // Collect nodes to copy
398
+ const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
399
+ if (nodesToCopy.length === 0)
400
+ return null;
401
+ // Calculate bounds
402
+ let minX = Infinity;
403
+ let minY = Infinity;
404
+ let maxX = -Infinity;
405
+ let maxY = -Infinity;
406
+ nodesToCopy.forEach((node) => {
407
+ const pos = positions[node.nodeId] || { x: 0, y: 0 };
408
+ const size = getNodeSize?.(node.nodeId, node.typeId) || {
409
+ width: 200,
410
+ height: 100,
411
+ };
412
+ minX = Math.min(minX, pos.x);
413
+ minY = Math.min(minY, pos.y);
414
+ maxX = Math.max(maxX, pos.x + size.width);
415
+ maxY = Math.max(maxY, pos.y + size.height);
416
+ });
417
+ const bounds = { minX, minY, maxX, maxY };
418
+ const centerX = (bounds.minX + bounds.maxX) / 2;
419
+ const centerY = (bounds.minY + bounds.maxY) / 2;
420
+ // Get inputs for each node
421
+ // Include values from inbound edges if those edges are selected
422
+ const allInputs = runner.getInputs(def);
423
+ const selectedEdgeSet = new Set(selection.edges);
424
+ const copiedNodes = nodesToCopy.map((node) => {
425
+ const pos = positions[node.nodeId] || { x: 0, y: 0 };
426
+ // Get all inbound edges for this node
427
+ const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
428
+ // Build set of handles that have inbound edges
429
+ // But only exclude handles whose edges are NOT selected
430
+ const inboundHandlesToExclude = new Set(inboundEdges
431
+ .filter((e) => !selectedEdgeSet.has(e.id)) // Only exclude if edge is not selected
432
+ .map((e) => e.target.handle));
433
+ const allNodeInputs = allInputs[node.nodeId] || {};
434
+ // Include inputs that either:
435
+ // 1. Don't have inbound edges (literal values)
436
+ // 2. Have inbound edges that ARE selected (preserve the value from the edge)
437
+ const inputsToCopy = Object.fromEntries(Object.entries(allNodeInputs).filter(([handle]) => !inboundHandlesToExclude.has(handle)));
438
+ return {
439
+ typeId: node.typeId,
440
+ params: node.params,
441
+ resolvedHandles: node.resolvedHandles,
442
+ position: {
443
+ x: pos.x - centerX,
444
+ y: pos.y - centerY,
445
+ },
446
+ inputs: inputsToCopy,
447
+ originalNodeId: node.nodeId,
448
+ };
449
+ });
450
+ // Collect edges between copied nodes
451
+ const copiedEdges = def.edges
452
+ .filter((edge) => {
453
+ return (selectedNodeSet.has(edge.source.nodeId) &&
454
+ selectedNodeSet.has(edge.target.nodeId));
455
+ })
456
+ .map((edge) => ({
457
+ sourceNodeId: edge.source.nodeId,
458
+ sourceHandle: edge.source.handle,
459
+ targetNodeId: edge.target.nodeId,
460
+ targetHandle: edge.target.handle,
461
+ typeId: edge.typeId,
462
+ }));
463
+ return {
464
+ nodes: copiedNodes,
465
+ edges: copiedEdges,
466
+ bounds,
467
+ };
468
+ }
469
+ /**
470
+ * Paste copied graph data at the specified center position.
471
+ * Returns the mapping from original node IDs to new node IDs.
472
+ * Uses copyOutputsFrom to copy outputs from original nodes (like duplicate does).
473
+ */
474
+ pasteCopiedData(data, center) {
475
+ const nodeIdMap = new Map();
476
+ const edgeIds = [];
477
+ // Add nodes
478
+ for (const nodeData of data.nodes) {
479
+ const newNodeId = this.addNode({
480
+ typeId: nodeData.typeId,
481
+ params: nodeData.params,
482
+ resolvedHandles: nodeData.resolvedHandles,
483
+ position: {
484
+ x: nodeData.position.x + center.x,
485
+ y: nodeData.position.y + center.y,
486
+ },
487
+ }, {
488
+ inputs: nodeData.inputs,
489
+ copyOutputsFrom: nodeData.originalNodeId,
490
+ dry: true,
491
+ });
492
+ nodeIdMap.set(nodeData.originalNodeId, newNodeId);
493
+ }
494
+ // Add edges
495
+ for (const edgeData of data.edges) {
496
+ const newSourceNodeId = nodeIdMap.get(edgeData.sourceNodeId);
497
+ const newTargetNodeId = nodeIdMap.get(edgeData.targetNodeId);
498
+ if (newSourceNodeId && newTargetNodeId) {
499
+ const edgeId = this.connect({
500
+ source: {
501
+ nodeId: newSourceNodeId,
502
+ handle: edgeData.sourceHandle,
503
+ },
504
+ target: {
505
+ nodeId: newTargetNodeId,
506
+ handle: edgeData.targetHandle,
507
+ },
508
+ typeId: edgeData.typeId,
509
+ }, { dry: true });
510
+ edgeIds.push(edgeId);
511
+ }
512
+ }
513
+ // Select the newly pasted nodes
514
+ this.setSelection({
515
+ nodes: Array.from(nodeIdMap.values()),
516
+ edges: edgeIds,
517
+ });
518
+ return { nodeIdMap, edgeIds };
519
+ }
368
520
  }
369
521
 
370
522
  class CLIWorkbench {
@@ -2911,6 +3063,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2911
3063
  updateEdgeType,
2912
3064
  triggerExternal,
2913
3065
  uiVersion,
3066
+ overrides,
2914
3067
  }), [
2915
3068
  wb,
2916
3069
  runner,
@@ -2950,10 +3103,272 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2950
3103
  wb,
2951
3104
  runner,
2952
3105
  uiVersion,
3106
+ overrides,
2953
3107
  ]);
2954
3108
  return (jsx(WorkbenchContext.Provider, { value: value, children: children }));
2955
3109
  }
2956
3110
 
3111
+ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
3112
+ const doBake = async (handleId) => {
3113
+ try {
3114
+ const typeId = outputTypesMap?.[nodeId]?.[handleId];
3115
+ const raw = outputsMap?.[nodeId]?.[handleId];
3116
+ if (!typeId || raw === undefined)
3117
+ return;
3118
+ const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
3119
+ const coerceIfNeeded = async (fromType, toType, value) => {
3120
+ if (!toType || toType === fromType || !runner?.coerce)
3121
+ return value;
3122
+ try {
3123
+ return await runner.coerce(fromType, toType, value);
3124
+ }
3125
+ catch {
3126
+ return value;
3127
+ }
3128
+ };
3129
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3130
+ const isArray = typeId.endsWith("[]");
3131
+ const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3132
+ const tArr = isArray ? registry.types.get(typeId) : undefined;
3133
+ const tElem = registry.types.get(baseTypeId);
3134
+ const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3135
+ const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3136
+ const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3137
+ if (singleTarget) {
3138
+ const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3139
+ const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3140
+ const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3141
+ wb.addNode({
3142
+ typeId: singleTarget.nodeTypeId,
3143
+ position: { x: pos.x + 180, y: pos.y },
3144
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
3145
+ return;
3146
+ }
3147
+ if (isArray && arrTarget) {
3148
+ const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3149
+ const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3150
+ const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3151
+ wb.addNode({
3152
+ typeId: arrTarget.nodeTypeId,
3153
+ position: { x: pos.x + 180, y: pos.y },
3154
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
3155
+ return;
3156
+ }
3157
+ if (isArray && elemTarget) {
3158
+ const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3159
+ const inType = getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3160
+ const src = unwrap(raw);
3161
+ const items = Array.isArray(src) ? src : [src];
3162
+ const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3163
+ const COLS = 4;
3164
+ const DX = 180;
3165
+ const DY = 160;
3166
+ for (let idx = 0; idx < coercedItems.length; idx++) {
3167
+ const col = idx % COLS;
3168
+ const row = Math.floor(idx / COLS);
3169
+ wb.addNode({
3170
+ typeId: elemTarget.nodeTypeId,
3171
+ position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3172
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3173
+ }
3174
+ return;
3175
+ }
3176
+ }
3177
+ catch { }
3178
+ };
3179
+ return {
3180
+ onDelete: () => {
3181
+ wb.removeNode(nodeId);
3182
+ onClose();
3183
+ },
3184
+ onDuplicate: async () => {
3185
+ const def = wb.export();
3186
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3187
+ if (!n)
3188
+ return onClose();
3189
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3190
+ const inboundHandles = new Set(def.edges
3191
+ .filter((e) => e.target.nodeId === nodeId)
3192
+ .map((e) => e.target.handle));
3193
+ const allInputs = runner.getInputs(def)[nodeId] || {};
3194
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3195
+ const newNodeId = wb.addNode({
3196
+ typeId: n.typeId,
3197
+ params: n.params,
3198
+ position: { x: pos.x + 24, y: pos.y + 24 },
3199
+ resolvedHandles: n.resolvedHandles,
3200
+ }, {
3201
+ inputs: inputsWithoutBindings,
3202
+ copyOutputsFrom: nodeId,
3203
+ dry: true,
3204
+ });
3205
+ // Select the newly duplicated node
3206
+ wb.setSelection({
3207
+ nodes: [newNodeId],
3208
+ edges: [],
3209
+ });
3210
+ onClose();
3211
+ },
3212
+ onDuplicateWithEdges: async () => {
3213
+ const def = wb.export();
3214
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3215
+ if (!n)
3216
+ return onClose();
3217
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3218
+ // Get inputs without bindings (literal values only)
3219
+ const inputs = runner.getInputs(def)[nodeId] || {};
3220
+ // Add the duplicated node
3221
+ const newNodeId = wb.addNode({
3222
+ typeId: n.typeId,
3223
+ params: n.params,
3224
+ position: { x: pos.x + 24, y: pos.y + 24 },
3225
+ resolvedHandles: n.resolvedHandles,
3226
+ }, {
3227
+ inputs,
3228
+ copyOutputsFrom: nodeId,
3229
+ dry: true,
3230
+ });
3231
+ // Find all incoming edges (edges where target is the original node)
3232
+ const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3233
+ // Duplicate each incoming edge to point to the new node
3234
+ for (const edge of incomingEdges) {
3235
+ wb.connect({
3236
+ source: edge.source, // Keep the same source
3237
+ target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3238
+ typeId: edge.typeId,
3239
+ }, { dry: true });
3240
+ }
3241
+ // Select the newly duplicated node and edges
3242
+ wb.setSelection({
3243
+ nodes: [newNodeId],
3244
+ edges: [],
3245
+ });
3246
+ onClose();
3247
+ },
3248
+ onRunPull: async () => {
3249
+ try {
3250
+ await runner.computeNode(nodeId);
3251
+ }
3252
+ catch { }
3253
+ onClose();
3254
+ },
3255
+ onBake: async (handleId) => {
3256
+ await doBake(handleId);
3257
+ onClose();
3258
+ },
3259
+ onCopy: () => {
3260
+ const copyHandler = createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult);
3261
+ copyHandler();
3262
+ onClose();
3263
+ },
3264
+ onCopyId: async () => {
3265
+ try {
3266
+ await navigator.clipboard.writeText(nodeId);
3267
+ }
3268
+ catch { }
3269
+ onClose();
3270
+ },
3271
+ onClose,
3272
+ };
3273
+ }
3274
+ /**
3275
+ * Creates a copy handler that copies the current selection and optionally stores the result.
3276
+ */
3277
+ function createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult) {
3278
+ return () => {
3279
+ const getNodeSize = getDefaultNodeSize
3280
+ ? (nodeId, typeId) => getDefaultNodeSize(typeId)
3281
+ : undefined;
3282
+ const data = wb.copySelection(runner, getNodeSize);
3283
+ if (onCopyResult) {
3284
+ onCopyResult(data);
3285
+ }
3286
+ };
3287
+ }
3288
+ /**
3289
+ * Creates a copy handler for a single node (temporarily selects it, copies, then restores selection).
3290
+ */
3291
+ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult) {
3292
+ return () => {
3293
+ // Select the node first, then copy
3294
+ const currentSelection = wb.getSelection();
3295
+ wb.setSelection({ nodes: [nodeId], edges: [] });
3296
+ const getNodeSize = getDefaultNodeSize
3297
+ ? (nodeId, typeId) => getDefaultNodeSize(typeId)
3298
+ : undefined;
3299
+ const data = wb.copySelection(runner, getNodeSize);
3300
+ // Restore original selection
3301
+ wb.setSelection(currentSelection);
3302
+ if (onCopyResult) {
3303
+ onCopyResult(data);
3304
+ }
3305
+ };
3306
+ }
3307
+ /**
3308
+ * Creates base selection context menu handlers.
3309
+ */
3310
+ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
3311
+ return {
3312
+ onCopy: runner
3313
+ ? createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult)
3314
+ : () => {
3315
+ // No-op if runner not available
3316
+ onClose();
3317
+ },
3318
+ onDelete: () => {
3319
+ wb.deleteSelection();
3320
+ onClose();
3321
+ },
3322
+ onClose,
3323
+ };
3324
+ }
3325
+ /**
3326
+ * Creates base default context menu handlers.
3327
+ */
3328
+ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste) {
3329
+ return {
3330
+ onAddNode,
3331
+ onPaste,
3332
+ onClose,
3333
+ };
3334
+ }
3335
+ function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3336
+ try {
3337
+ const def = wb.export();
3338
+ const node = def.nodes.find((n) => n.nodeId === nodeId);
3339
+ if (!node)
3340
+ return [];
3341
+ const desc = registry.nodes.get(node.typeId);
3342
+ const handles = Object.keys(desc?.outputs || {});
3343
+ const out = [];
3344
+ for (const h of handles) {
3345
+ const tId = outputTypesMap?.[nodeId]?.[h];
3346
+ if (!tId)
3347
+ continue;
3348
+ if (tId.endsWith("[]")) {
3349
+ const base = tId.slice(0, -2);
3350
+ const tArr = registry.types.get(tId);
3351
+ const tElem = registry.types.get(base);
3352
+ const arrT = tArr?.bakeTarget;
3353
+ const elemT = tElem?.bakeTarget;
3354
+ if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3355
+ (elemT && registry.nodes.has(elemT.nodeTypeId)))
3356
+ out.push(h);
3357
+ }
3358
+ else {
3359
+ const t = registry.types.get(tId);
3360
+ const bt = t?.bakeTarget;
3361
+ if (bt && registry.nodes.has(bt.nodeTypeId))
3362
+ out.push(h);
3363
+ }
3364
+ }
3365
+ return out;
3366
+ }
3367
+ catch {
3368
+ return [];
3369
+ }
3370
+ }
3371
+
2957
3372
  function IssueBadge({ level, title, size = 12, className, }) {
2958
3373
  const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
2959
3374
  return (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" ? (jsx(XCircleIcon, { size: size, weight: "fill" })) : (jsx(WarningCircleIcon, { size: size, weight: "fill" })) }));
@@ -3166,7 +3581,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3166
3581
  }
3167
3582
  catch { }
3168
3583
  };
3169
- return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsx("div", { className: "break-words", children: err.message }), jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsx("div", { className: "break-words", children: err.message })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Registry Error" }), jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (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) => {
3584
+ return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto select-none`, children: [jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsx("div", { className: "break-words", children: err.message }), jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsx("div", { className: "break-words", children: err.message })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Registry Error" }), jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (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) => {
3170
3585
  e.stopPropagation();
3171
3586
  deleteEdgeById(m.data?.edgeId);
3172
3587
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxs("div", { children: [jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), renderEdgeStatus(), jsx("div", { className: "mt-1", children: jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
@@ -3234,7 +3649,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3234
3649
  const title = inIssues
3235
3650
  .map((v) => `${v.code}: ${v.message}`)
3236
3651
  .join("; ");
3237
- return (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-32 flex flex-col", children: [jsx("span", { children: prettyHandle(h) }), jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [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
3652
+ return (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-32 flex flex-col", children: [jsx("span", { children: prettyHandle(h) }), jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [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
3238
3653
  ? String(current)
3239
3654
  : "", onChange: (e) => {
3240
3655
  const val = e.target.value;
@@ -3248,7 +3663,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3248
3663
  ? `Default: ${placeholder}`
3249
3664
  : "(select)" }), registry.enums
3250
3665
  .get(typeId)
3251
- ?.options.map((opt) => (jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (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: jsx(XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsx("div", { className: "flex items-center gap-1 flex-1", children: jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [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
3666
+ ?.options.map((opt) => (jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (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: jsx(XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsx("div", { className: "flex items-center gap-1 flex-1", children: jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [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
3252
3667
  ? `Default: ${placeholder}`
3253
3668
  : undefined, value: displayValue, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
3254
3669
  if (e.key === "Enter")
@@ -3501,6 +3916,13 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3501
3916
  handlers.onAddNode(typeId, { position: p });
3502
3917
  handlers.onClose();
3503
3918
  };
3919
+ const handlePaste = () => {
3920
+ if (!handlers.onPaste)
3921
+ return;
3922
+ const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
3923
+ handlers.onPaste(p);
3924
+ handlers.onClose();
3925
+ };
3504
3926
  const renderTree = (tree, path = []) => {
3505
3927
  const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
3506
3928
  return (jsx("div", { children: entries.map(([key, child]) => {
@@ -3515,10 +3937,10 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3515
3937
  return (jsxs("div", { children: [jsx("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wide text-gray-400", children: label }), child.__self && (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 })), jsx("div", { className: "pl-2 border-l border-gray-200 ml-2", children: renderTree(child, [...path, key]) })] }, idKey));
3516
3938
  }) }));
3517
3939
  };
3518
- return (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) => {
3940
+ return (jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3519
3941
  e.preventDefault();
3520
3942
  e.stopPropagation();
3521
- }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsx("div", { className: "px-2 pb-1", children: jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 px-2 py-1 text-sm outline-none focus:border-gray-400", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
3943
+ }, children: [handlers.onPaste && (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 && jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsx("div", { className: "px-2 pb-1", children: jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 px-2 py-1 text-sm outline-none focus:border-gray-400 select-text", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
3522
3944
  }
3523
3945
 
3524
3946
  function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
@@ -3556,199 +3978,55 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
3556
3978
  const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
3557
3979
  (MENU_MIN_WIDTH + PADDING));
3558
3980
  const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
3559
- return (jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3981
+ return (jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3560
3982
  e.preventDefault();
3561
3983
  e.stopPropagation();
3562
- }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), bakeableOutputs.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h))), jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" })] }));
3984
+ }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), 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 && (jsxs(Fragment, { children: [jsx("div", { className: "h-px bg-gray-200 my-1" }), jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h)))] }))] }));
3563
3985
  }
3564
3986
 
3565
- function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose) {
3566
- const doBake = async (handleId) => {
3567
- try {
3568
- const typeId = outputTypesMap?.[nodeId]?.[handleId];
3569
- const raw = outputsMap?.[nodeId]?.[handleId];
3570
- if (!typeId || raw === undefined)
3571
- return;
3572
- const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
3573
- const coerceIfNeeded = async (fromType, toType, value) => {
3574
- if (!toType || toType === fromType || !runner?.coerce)
3575
- return value;
3576
- try {
3577
- return await runner.coerce(fromType, toType, value);
3578
- }
3579
- catch {
3580
- return value;
3581
- }
3582
- };
3583
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3584
- const isArray = typeId.endsWith("[]");
3585
- const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3586
- const tArr = isArray ? registry.types.get(typeId) : undefined;
3587
- const tElem = registry.types.get(baseTypeId);
3588
- const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3589
- const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3590
- const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3591
- if (singleTarget) {
3592
- const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3593
- const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3594
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3595
- wb.addNode({
3596
- typeId: singleTarget.nodeTypeId,
3597
- position: { x: pos.x + 180, y: pos.y },
3598
- }, { inputs: { [singleTarget.inputHandle]: coerced } });
3599
- return;
3600
- }
3601
- if (isArray && arrTarget) {
3602
- const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3603
- const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3604
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3605
- wb.addNode({
3606
- typeId: arrTarget.nodeTypeId,
3607
- position: { x: pos.x + 180, y: pos.y },
3608
- }, { inputs: { [arrTarget.inputHandle]: coerced } });
3609
- return;
3610
- }
3611
- if (isArray && elemTarget) {
3612
- const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3613
- const inType = getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3614
- const src = unwrap(raw);
3615
- const items = Array.isArray(src) ? src : [src];
3616
- const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3617
- const COLS = 4;
3618
- const DX = 180;
3619
- const DY = 160;
3620
- for (let idx = 0; idx < coercedItems.length; idx++) {
3621
- const col = idx % COLS;
3622
- const row = Math.floor(idx / COLS);
3623
- wb.addNode({
3624
- typeId: elemTarget.nodeTypeId,
3625
- position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3626
- }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3627
- }
3987
+ function SelectionContextMenu({ open, clientPos, handlers, }) {
3988
+ const ref = useRef(null);
3989
+ // Close on outside click and on ESC
3990
+ useEffect(() => {
3991
+ if (!open)
3992
+ return;
3993
+ const onDown = (e) => {
3994
+ if (!ref.current)
3628
3995
  return;
3629
- }
3630
- }
3631
- catch { }
3632
- };
3633
- return {
3634
- onDelete: () => {
3635
- wb.removeNode(nodeId);
3636
- onClose();
3637
- },
3638
- onDuplicate: async () => {
3639
- const def = wb.export();
3640
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3641
- if (!n)
3642
- return onClose();
3643
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3644
- const inboundHandles = new Set(def.edges
3645
- .filter((e) => e.target.nodeId === nodeId)
3646
- .map((e) => e.target.handle));
3647
- const allInputs = runner.getInputs(def)[nodeId] || {};
3648
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3649
- wb.addNode({
3650
- typeId: n.typeId,
3651
- params: n.params,
3652
- position: { x: pos.x + 24, y: pos.y + 24 },
3653
- resolvedHandles: n.resolvedHandles,
3654
- }, {
3655
- inputs: inputsWithoutBindings,
3656
- copyOutputsFrom: nodeId,
3657
- dry: true,
3658
- });
3659
- onClose();
3660
- },
3661
- onDuplicateWithEdges: async () => {
3662
- const def = wb.export();
3663
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3664
- if (!n)
3665
- return onClose();
3666
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3667
- // Get inputs without bindings (literal values only)
3668
- const inputs = runner.getInputs(def)[nodeId] || {};
3669
- // Add the duplicated node
3670
- const newNodeId = wb.addNode({
3671
- typeId: n.typeId,
3672
- params: n.params,
3673
- position: { x: pos.x + 24, y: pos.y + 24 },
3674
- resolvedHandles: n.resolvedHandles,
3675
- }, {
3676
- inputs,
3677
- copyOutputsFrom: nodeId,
3678
- dry: true,
3679
- });
3680
- // Find all incoming edges (edges where target is the original node)
3681
- const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3682
- // Duplicate each incoming edge to point to the new node
3683
- for (const edge of incomingEdges) {
3684
- wb.connect({
3685
- source: edge.source, // Keep the same source
3686
- target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3687
- typeId: edge.typeId,
3688
- }, { dry: true });
3689
- }
3690
- onClose();
3691
- },
3692
- onRunPull: async () => {
3693
- try {
3694
- await runner.computeNode(nodeId);
3695
- }
3696
- catch { }
3697
- onClose();
3698
- },
3699
- onBake: async (handleId) => {
3700
- await doBake(handleId);
3701
- onClose();
3702
- },
3703
- onCopyId: async () => {
3704
- try {
3705
- await navigator.clipboard.writeText(nodeId);
3706
- }
3707
- catch { }
3708
- onClose();
3709
- },
3710
- onClose,
3711
- };
3712
- }
3713
- function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3714
- try {
3715
- const def = wb.export();
3716
- const node = def.nodes.find((n) => n.nodeId === nodeId);
3717
- if (!node)
3718
- return [];
3719
- const desc = registry.nodes.get(node.typeId);
3720
- const handles = Object.keys(desc?.outputs || {});
3721
- const out = [];
3722
- for (const h of handles) {
3723
- const tId = outputTypesMap?.[nodeId]?.[h];
3724
- if (!tId)
3725
- continue;
3726
- if (tId.endsWith("[]")) {
3727
- const base = tId.slice(0, -2);
3728
- const tArr = registry.types.get(tId);
3729
- const tElem = registry.types.get(base);
3730
- const arrT = tArr?.bakeTarget;
3731
- const elemT = tElem?.bakeTarget;
3732
- if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3733
- (elemT && registry.nodes.has(elemT.nodeTypeId)))
3734
- out.push(h);
3735
- }
3736
- else {
3737
- const t = registry.types.get(tId);
3738
- const bt = t?.bakeTarget;
3739
- if (bt && registry.nodes.has(bt.nodeTypeId))
3740
- out.push(h);
3741
- }
3742
- }
3743
- return out;
3744
- }
3745
- catch {
3746
- return [];
3747
- }
3996
+ if (!ref.current.contains(e.target))
3997
+ handlers.onClose();
3998
+ };
3999
+ const onKey = (e) => {
4000
+ if (e.key === "Escape")
4001
+ handlers.onClose();
4002
+ };
4003
+ window.addEventListener("mousedown", onDown, true);
4004
+ window.addEventListener("keydown", onKey);
4005
+ return () => {
4006
+ window.removeEventListener("mousedown", onDown, true);
4007
+ window.removeEventListener("keydown", onKey);
4008
+ };
4009
+ }, [open, handlers]);
4010
+ useEffect(() => {
4011
+ if (open)
4012
+ ref.current?.focus();
4013
+ }, [open]);
4014
+ if (!open || !clientPos)
4015
+ return null;
4016
+ // Clamp menu position to viewport
4017
+ const MENU_MIN_WIDTH = 180;
4018
+ const PADDING = 16;
4019
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
4020
+ (MENU_MIN_WIDTH + PADDING));
4021
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 100);
4022
+ return (jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
4023
+ e.preventDefault();
4024
+ e.stopPropagation();
4025
+ }, children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" })] }));
3748
4026
  }
3749
4027
 
3750
4028
  const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
3751
- const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, } = useWorkbenchContext();
4029
+ const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, overrides, } = useWorkbenchContext();
3752
4030
  const nodeValidation = validationByNode;
3753
4031
  const edgeValidation = validationByEdge.errors;
3754
4032
  const [registryVersion, setRegistryVersion] = useState(0);
@@ -3987,23 +4265,100 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3987
4265
  const [nodeMenuOpen, setNodeMenuOpen] = useState(false);
3988
4266
  const [nodeMenuPos, setNodeMenuPos] = useState(null);
3989
4267
  const [nodeAtMenu, setNodeAtMenu] = useState(null);
4268
+ const [selectionMenuPos, setSelectionMenuPos] = useState(null);
4269
+ const [selectionMenuOpen, setSelectionMenuOpen] = useState(false);
4270
+ // Compute the rectangular screen-space bounds of the current selection
4271
+ const getSelectionScreenBounds = () => {
4272
+ if (typeof document === "undefined")
4273
+ return null;
4274
+ const selection = wb.getSelection();
4275
+ if (!selection.nodes.length)
4276
+ return null;
4277
+ let bounds = null;
4278
+ for (const nodeId of selection.nodes) {
4279
+ const el = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`);
4280
+ if (!el)
4281
+ continue;
4282
+ const rect = el.getBoundingClientRect();
4283
+ if (!bounds) {
4284
+ bounds = {
4285
+ left: rect.left,
4286
+ top: rect.top,
4287
+ right: rect.right,
4288
+ bottom: rect.bottom,
4289
+ };
4290
+ }
4291
+ else {
4292
+ bounds.left = Math.min(bounds.left, rect.left);
4293
+ bounds.top = Math.min(bounds.top, rect.top);
4294
+ bounds.right = Math.max(bounds.right, rect.right);
4295
+ bounds.bottom = Math.max(bounds.bottom, rect.bottom);
4296
+ }
4297
+ }
4298
+ return bounds;
4299
+ };
3990
4300
  const onContextMenu = (e) => {
3991
4301
  e.preventDefault();
3992
- // Determine if right-clicked over a node by hit-testing selection
4302
+ // First, check if the cursor is inside the rectangular bounds of the current selection
4303
+ const selectionBounds = getSelectionScreenBounds();
4304
+ if (selectionBounds) {
4305
+ const { left, top, right, bottom } = selectionBounds;
4306
+ if (e.clientX >= left &&
4307
+ e.clientX <= right &&
4308
+ e.clientY >= top &&
4309
+ e.clientY <= bottom) {
4310
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4311
+ setSelectionMenuOpen(true);
4312
+ setMenuOpen(false);
4313
+ setNodeMenuOpen(false);
4314
+ return;
4315
+ }
4316
+ }
4317
+ // Determine if right-clicked over a node by hit-testing
3993
4318
  const target = e.target?.closest(".react-flow__node");
3994
4319
  if (target) {
3995
4320
  // Resolve node id from data-id attribute React Flow sets
3996
4321
  const nodeId = target.getAttribute("data-id");
3997
- setNodeAtMenu(nodeId);
3998
- setNodeMenuPos({ x: e.clientX, y: e.clientY });
3999
- setNodeMenuOpen(true);
4000
- setMenuOpen(false);
4322
+ const selection = wb.getSelection();
4323
+ const isSelected = nodeId && selection.nodes.includes(nodeId);
4324
+ if (isSelected) {
4325
+ // Right-clicked on a selected node - show selection menu
4326
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4327
+ setSelectionMenuOpen(true);
4328
+ setMenuOpen(false);
4329
+ setNodeMenuOpen(false);
4330
+ return;
4331
+ }
4332
+ else {
4333
+ // Right-clicked on a non-selected node - show node menu
4334
+ setNodeAtMenu(nodeId);
4335
+ setNodeMenuPos({ x: e.clientX, y: e.clientY });
4336
+ setNodeMenuOpen(true);
4337
+ setMenuOpen(false);
4338
+ setSelectionMenuOpen(false);
4339
+ return;
4340
+ }
4001
4341
  }
4002
- else {
4003
- setMenuPos({ x: e.clientX, y: e.clientY });
4004
- setMenuOpen(true);
4005
- setNodeMenuOpen(false);
4342
+ // Check if right-clicked on a selected edge
4343
+ const edgeTarget = e.target?.closest(".react-flow__edge");
4344
+ if (edgeTarget) {
4345
+ const edgeId = edgeTarget.getAttribute("data-id");
4346
+ const selection = wb.getSelection();
4347
+ const isSelected = edgeId && selection.edges.includes(edgeId);
4348
+ if (isSelected) {
4349
+ // Right-clicked on a selected edge - show selection menu
4350
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4351
+ setSelectionMenuOpen(true);
4352
+ setMenuOpen(false);
4353
+ setNodeMenuOpen(false);
4354
+ return;
4355
+ }
4006
4356
  }
4357
+ // Right-clicked on empty space - show default menu
4358
+ setMenuPos({ x: e.clientX, y: e.clientY });
4359
+ setMenuOpen(true);
4360
+ setNodeMenuOpen(false);
4361
+ setSelectionMenuOpen(false);
4007
4362
  };
4008
4363
  const addNodeAt = useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
4009
4364
  const onCloseMenu = useCallback(() => {
@@ -4012,6 +4367,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4012
4367
  const onCloseNodeMenu = useCallback(() => {
4013
4368
  setNodeMenuOpen(false);
4014
4369
  }, []);
4370
+ const onCloseSelectionMenu = useCallback(() => {
4371
+ setSelectionMenuOpen(false);
4372
+ }, []);
4015
4373
  useEffect(() => {
4016
4374
  const off = runner.on("registry", () => {
4017
4375
  setRegistryVersion((v) => v + 1);
@@ -4019,14 +4377,32 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4019
4377
  return () => off();
4020
4378
  }, [runner]);
4021
4379
  const nodeIds = useMemo(() => Array.from(registry.nodes.keys()), [registry, registryVersion]);
4022
- const defaultContextMenuHandlers = useMemo(() => ({
4023
- onAddNode: addNodeAt,
4024
- onClose: onCloseMenu,
4025
- }), [addNodeAt, onCloseMenu]);
4380
+ const defaultContextMenuHandlers = useMemo(() => {
4381
+ const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu);
4382
+ if (overrides?.getDefaultContextMenuHandlers) {
4383
+ return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
4384
+ }
4385
+ return baseHandlers;
4386
+ }, [addNodeAt, onCloseMenu, overrides, wb]);
4387
+ const selectionContextMenuHandlers = useMemo(() => {
4388
+ const baseHandlers = createSelectionContextMenuHandlers(wb, onCloseSelectionMenu, overrides?.getDefaultNodeSize, undefined, // onCopyResult - will be provided by overrides
4389
+ runner);
4390
+ if (overrides?.getSelectionContextMenuHandlers) {
4391
+ const selection = wb.getSelection();
4392
+ return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
4393
+ getDefaultNodeSize: overrides.getDefaultNodeSize,
4394
+ });
4395
+ }
4396
+ return baseHandlers;
4397
+ }, [wb, runner, overrides, onCloseSelectionMenu]);
4026
4398
  const nodeContextMenuHandlers = useMemo(() => {
4027
4399
  if (!nodeAtMenu)
4028
4400
  return null;
4029
- return createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu);
4401
+ const baseHandlers = createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu, overrides?.getDefaultNodeSize);
4402
+ if (overrides?.getNodeContextMenuHandlers) {
4403
+ return overrides.getNodeContextMenuHandlers(wb, nodeAtMenu, baseHandlers);
4404
+ }
4405
+ return baseHandlers;
4030
4406
  }, [
4031
4407
  nodeAtMenu,
4032
4408
  wb,
@@ -4035,6 +4411,8 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4035
4411
  outputsMap,
4036
4412
  outputTypesMap,
4037
4413
  onCloseNodeMenu,
4414
+ overrides?.getDefaultNodeSize,
4415
+ overrides?.getNodeContextMenuHandlers,
4038
4416
  ]);
4039
4417
  const canRunPull = useMemo(() => engineKind()?.toString() === "pull", [engineKind]);
4040
4418
  const bakeableOutputs = useMemo(() => {
@@ -4090,7 +4468,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4090
4468
  }
4091
4469
  }, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [BackgroundRenderer ? (jsx(BackgroundRenderer, {})) : (jsx(Background, { id: "workbench-canvas-background", variant: BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsx(MinimapRenderer, {}) : jsx(MiniMap, {}), ControlsRenderer ? jsx(ControlsRenderer, {}) : jsx(Controls, {}), DefaultContextMenuRenderer ? (jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })) : (jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })), !!nodeAtMenu &&
4092
4470
  nodeContextMenuHandlers &&
4093
- (NodeContextMenuRenderer ? (jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })))] }) }) }));
4471
+ (NodeContextMenuRenderer ? (jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs }))), selectionMenuOpen && selectionMenuPos && (jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers }))] }) }) }));
4094
4472
  });
4095
4473
 
4096
4474
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
@@ -4627,5 +5005,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
4627
5005
  return (jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: onBackendKindChangeWithDispose, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides, onInit: onInit, onChange: onChange }) }));
4628
5006
  }
4629
5007
 
4630
- export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, computeEffectiveHandles, countVisibleHandles, createHandleBounds, createHandleLayout, download, estimateNodeSize, formatDataUrlAsLabel, formatDeclaredTypeSignature, getHandleBoundsX, getHandleBoundsY, getHandleClassName, getHandleLayoutY, getNodeBorderClassNames, layoutNode, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, upload, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
5008
+ export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, computeEffectiveHandles, countVisibleHandles, createCopyHandler, createDefaultContextMenuHandlers, createHandleBounds, createHandleLayout, createNodeContextMenuHandlers, createNodeCopyHandler, createSelectionContextMenuHandlers, download, estimateNodeSize, formatDataUrlAsLabel, formatDeclaredTypeSignature, getBakeableOutputs, getHandleBoundsX, getHandleBoundsY, getHandleClassName, getHandleLayoutY, getNodeBorderClassNames, layoutNode, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, upload, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
4631
5009
  //# sourceMappingURL=index.js.map