@bian-womp/spark-workbench 0.1.10 → 0.1.11

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 (33) hide show
  1. package/lib/cjs/index.cjs +351 -155
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/index.d.ts +1 -1
  4. package/lib/cjs/src/index.d.ts.map +1 -1
  5. package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -1
  6. package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
  7. package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
  8. package/lib/cjs/src/misc/NodeContextMenu.d.ts +10 -0
  9. package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +1 -0
  10. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts +2 -2
  11. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/mapping.d.ts +35 -4
  15. package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
  16. package/lib/cjs/src/runtime/GraphRunner.d.ts.map +1 -1
  17. package/lib/esm/index.js +350 -154
  18. package/lib/esm/index.js.map +1 -1
  19. package/lib/esm/src/index.d.ts +1 -1
  20. package/lib/esm/src/index.d.ts.map +1 -1
  21. package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -1
  22. package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
  23. package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
  24. package/lib/esm/src/misc/NodeContextMenu.d.ts +10 -0
  25. package/lib/esm/src/misc/NodeContextMenu.d.ts.map +1 -0
  26. package/lib/esm/src/misc/WorkbenchCanvas.d.ts +2 -2
  27. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  28. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  29. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  30. package/lib/esm/src/misc/mapping.d.ts +35 -4
  31. package/lib/esm/src/misc/mapping.d.ts.map +1 -1
  32. package/lib/esm/src/runtime/GraphRunner.d.ts.map +1 -1
  33. package/package.json +4 -4
package/lib/esm/index.js CHANGED
@@ -317,37 +317,6 @@ class CLIWorkbench {
317
317
  }
318
318
  }
319
319
 
320
- function toReactFlow$1(def, positions = {}) {
321
- const nodes = def.nodes.map((n) => ({
322
- id: n.nodeId,
323
- data: { typeId: n.typeId, params: n.params },
324
- position: positions[n.nodeId] ?? { x: 0, y: 0 },
325
- }));
326
- const edges = def.edges.map((e) => ({
327
- id: e.id,
328
- source: e.source.nodeId,
329
- target: e.target.nodeId,
330
- sourceHandle: e.source.handle,
331
- targetHandle: e.target.handle,
332
- }));
333
- return { nodes, edges };
334
- }
335
- class ReactFlowWorkbench {
336
- constructor(wb) {
337
- this.wb = wb;
338
- }
339
- get actions() {
340
- return this.wb;
341
- }
342
- async load(def) {
343
- await this.wb.load(def);
344
- }
345
- export(def) {
346
- const d = def ?? this.wb.export();
347
- return toReactFlow$1(d);
348
- }
349
- }
350
-
351
320
  class GraphRunner {
352
321
  constructor(registry, backend) {
353
322
  this.registry = registry;
@@ -361,6 +330,8 @@ class GraphRunner {
361
330
  if (this.backend.kind === "local") {
362
331
  const builder = new GraphBuilder(this.registry);
363
332
  this.runtime = builder.build(def);
333
+ // Signal UI that freshly built graph should be considered invalidated
334
+ this.emit("invalidate", { reason: "graph-built" });
364
335
  return;
365
336
  }
366
337
  // Remote: no-op here; build is performed on remote server during launch
@@ -369,7 +340,20 @@ class GraphRunner {
369
340
  if (this.backend.kind === "local") {
370
341
  if (!this.runtime)
371
342
  return;
343
+ // Prevent mid-run churn while wiring changes are applied
344
+ try {
345
+ this.runtime.pause();
346
+ }
347
+ catch {
348
+ console.error("Failed to pause runtime");
349
+ }
372
350
  this.runtime.update(def, this.registry);
351
+ try {
352
+ this.runtime.resume();
353
+ }
354
+ catch {
355
+ console.error("Failed to resume runtime");
356
+ }
373
357
  this.emit("invalidate", { reason: "graph-updated" });
374
358
  return;
375
359
  }
@@ -432,6 +416,8 @@ class GraphRunner {
432
416
  // Remote: build remotely then launch
433
417
  void this.ensureRemote().then(async (rc) => {
434
418
  await rc.runner.build(def);
419
+ // Signal UI after remote build as well
420
+ this.emit("invalidate", { reason: "graph-built" });
435
421
  const eng = rc.runner.getEngine();
436
422
  if (!rc.listenersBound) {
437
423
  eng.on("value", (e) => {
@@ -461,8 +447,13 @@ class GraphRunner {
461
447
  if (!this.stagedInputs[nodeId])
462
448
  this.stagedInputs[nodeId] = {};
463
449
  this.stagedInputs[nodeId][handle] = value;
464
- if (this.engine)
450
+ if (this.engine) {
465
451
  this.engine.setInput(nodeId, handle, value);
452
+ }
453
+ else {
454
+ // Emit a value event so UI updates even when engine isn't running
455
+ this.emit("value", { nodeId, handle, value, io: "input" });
456
+ }
466
457
  }
467
458
  async step() {
468
459
  if (this.backend.kind !== "local")
@@ -778,6 +769,98 @@ function useQueryParamString(key, defaultValue) {
778
769
  return [val, set];
779
770
  }
780
771
 
772
+ function toReactFlow(def, positions, registry, opts) {
773
+ const nodeHandleMap = {};
774
+ const nodes = def.nodes.map((n) => {
775
+ const desc = registry.nodes.get(n.typeId);
776
+ const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
777
+ const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
778
+ nodeHandleMap[n.nodeId] = {
779
+ inputs: new Set(inputHandles.map((h) => h.id)),
780
+ outputs: new Set(outputHandles.map((h) => h.id)),
781
+ };
782
+ return {
783
+ id: n.nodeId,
784
+ data: {
785
+ typeId: n.typeId,
786
+ params: n.params,
787
+ inputHandles,
788
+ outputHandles,
789
+ showValues: opts.showValues,
790
+ inputValues: opts.inputs?.[n.nodeId],
791
+ outputValues: opts.outputs?.[n.nodeId],
792
+ status: opts.nodeStatus?.[n.nodeId],
793
+ validation: {
794
+ inputs: opts.nodeValidation?.inputs?.[n.nodeId] ?? [],
795
+ outputs: opts.nodeValidation?.outputs?.[n.nodeId] ?? [],
796
+ issues: opts.nodeValidation?.issues?.[n.nodeId] ?? [],
797
+ },
798
+ toString: opts.toString,
799
+ toElement: opts.toElement,
800
+ },
801
+ position: positions[n.nodeId] ?? { x: 0, y: 0 },
802
+ type: opts.resolveNodeType?.(n.typeId) ?? "spark-default",
803
+ selected: opts.selectedNodeIds
804
+ ? opts.selectedNodeIds.has(n.nodeId)
805
+ : undefined,
806
+ };
807
+ });
808
+ const edges = def.edges
809
+ .filter((e) => {
810
+ const src = nodeHandleMap[e.source.nodeId];
811
+ const dst = nodeHandleMap[e.target.nodeId];
812
+ if (!src || !dst)
813
+ return false;
814
+ return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
815
+ })
816
+ .map((e) => {
817
+ const st = opts.edgeStatus?.[e.id];
818
+ const isRunning = !!st?.running;
819
+ const hasError = !!st?.lastError;
820
+ const isInvalidEdge = !!opts.edgeValidation?.[e.id];
821
+ const style = hasError || isInvalidEdge
822
+ ? { stroke: "#ef4444", strokeWidth: 2 }
823
+ : isRunning
824
+ ? { stroke: "#3b82f6" }
825
+ : undefined;
826
+ return {
827
+ id: e.id,
828
+ source: e.source.nodeId,
829
+ target: e.target.nodeId,
830
+ sourceHandle: e.source.handle,
831
+ targetHandle: e.target.handle,
832
+ selected: opts.selectedEdgeIds
833
+ ? opts.selectedEdgeIds.has(e.id)
834
+ : undefined,
835
+ animated: isRunning,
836
+ style,
837
+ };
838
+ });
839
+ return { nodes, edges };
840
+ }
841
+ // Shared node container border class composition for consistent visuals
842
+ function getNodeBorderClassNames(args) {
843
+ const selected = !!args.selected;
844
+ const status = args.status || {};
845
+ const issues = args.validation?.issues ?? [];
846
+ const hasError = !!status.lastError;
847
+ const hasValidationError = issues.some((i) => i?.level === "error");
848
+ const hasValidationWarning = !hasValidationError && issues.length > 0;
849
+ const isRunning = !!status.running;
850
+ const isInvalid = !!status.invalidated && !isRunning && !hasError;
851
+ const borderWidth = selected ? "border-2" : "border";
852
+ const borderStyle = isInvalid ? "border-dashed" : "border-solid";
853
+ const borderColor = hasError || hasValidationError
854
+ ? "border-red-500"
855
+ : hasValidationWarning
856
+ ? "border-amber-500"
857
+ : isRunning
858
+ ? "border-blue-500"
859
+ : "border-gray-500 dark:border-gray-400";
860
+ const ring = isRunning ? " ring-2 ring-blue-200 dark:ring-blue-900" : "";
861
+ return `${borderWidth} ${borderStyle} ${borderColor}${ring}`.trim();
862
+ }
863
+
781
864
  const WorkbenchContext = createContext(null);
782
865
  function useWorkbenchContext() {
783
866
  const ctx = useContext(WorkbenchContext);
@@ -806,6 +889,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
806
889
  const def = wb.export();
807
890
  const inputsMap = useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
808
891
  const outputsMap = useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
892
+ // Initialize nodes as invalidated by default until first successful run
893
+ useEffect(() => {
894
+ setNodeStatus((prev) => {
895
+ const next = { ...prev };
896
+ for (const n of def.nodes) {
897
+ const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
898
+ if (cur.invalidated === undefined) {
899
+ next[n.nodeId] = { ...cur, invalidated: true };
900
+ }
901
+ }
902
+ return next;
903
+ });
904
+ }, [def]);
809
905
  // Auto layout (simple layered layout)
810
906
  const runAutoLayout = useCallback(() => {
811
907
  const cur = wb.export();
@@ -873,20 +969,20 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
873
969
  const off2 = runner.on("error", (e) => {
874
970
  const edgeError = e;
875
971
  const nodeError = e;
876
- if (edgeError?.kind === "edge-convert") {
972
+ if (edgeError.kind === "edge-convert") {
877
973
  const edgeId = edgeError.edgeId;
878
974
  setEdgeStatus((s) => ({
879
975
  ...s,
880
976
  [edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
881
977
  }));
882
978
  }
883
- else if (nodeError?.nodeId) {
884
- const nodeId = nodeError?.nodeId;
979
+ else if (nodeError.nodeId) {
980
+ const nodeId = nodeError.nodeId;
885
981
  setNodeStatus((s) => ({
886
982
  ...s,
887
983
  [nodeId]: {
888
984
  ...(s[nodeId] ?? {}),
889
- lastError: nodeError?.err,
985
+ lastError: nodeError.err,
890
986
  },
891
987
  }));
892
988
  }
@@ -954,6 +1050,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
954
1050
  return add("runner", "stats")(s);
955
1051
  });
956
1052
  const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
1053
+ // Ensure newly added nodes start as invalidated until first evaluation
1054
+ const off4c = wb.on("graphChanged", (e) => {
1055
+ const change = e.change;
1056
+ if (change?.type === "addNode" && typeof change.nodeId === "string") {
1057
+ const id = change.nodeId;
1058
+ setNodeStatus((s) => ({
1059
+ ...s,
1060
+ [id]: { ...(s[id] ?? {}), invalidated: true },
1061
+ }));
1062
+ }
1063
+ });
957
1064
  const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
958
1065
  const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
959
1066
  const off5b = wb.on("validationChanged", (r) => setValidation(r));
@@ -970,6 +1077,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
970
1077
  off3b();
971
1078
  off4();
972
1079
  off4b();
1080
+ off4c();
973
1081
  off5();
974
1082
  off5b();
975
1083
  off6();
@@ -992,10 +1100,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
992
1100
  if (!validation)
993
1101
  return { inputs, outputs, issues };
994
1102
  for (const is of validation.issues ?? []) {
995
- const d = is?.data;
996
- const level = is?.level;
997
- const code = String(is?.code ?? "");
998
- const message = String(is?.message ?? code);
1103
+ const d = is.data;
1104
+ const level = is.level;
1105
+ const code = String(is.code ?? "");
1106
+ const message = String(is.message ?? code);
999
1107
  if (!d)
1000
1108
  continue;
1001
1109
  if (d.nodeId) {
@@ -1024,10 +1132,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1024
1132
  if (!validation)
1025
1133
  return list;
1026
1134
  for (const is of validation.issues ?? []) {
1027
- const d = is?.data;
1028
- const level = is?.level;
1029
- const code = String(is?.code ?? "");
1030
- const message = String(is?.message ?? code);
1135
+ const d = is.data;
1136
+ const level = is.level;
1137
+ const code = String(is.code ?? "");
1138
+ const message = String(is.message ?? code);
1031
1139
  if (!d || (!d.nodeId && !d.edgeId)) {
1032
1140
  list.push({ level, code, message });
1033
1141
  }
@@ -1040,10 +1148,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1040
1148
  if (!validation)
1041
1149
  return { errors, issues };
1042
1150
  for (const is of validation.issues ?? []) {
1043
- const d = is?.data;
1044
- const level = is?.level;
1045
- const code = String(is?.code ?? "");
1046
- const message = String(is?.message ?? code);
1151
+ const d = is.data;
1152
+ const level = is.level;
1153
+ const code = String(is.code ?? "");
1154
+ const message = String(is.message ?? code);
1047
1155
  if (d?.edgeId) {
1048
1156
  if (level === "error")
1049
1157
  errors[d.edgeId] = true;
@@ -1154,6 +1262,16 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
1154
1262
  }
1155
1263
 
1156
1264
  function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, setInput, }) {
1265
+ const safeToString = (typeId, value) => {
1266
+ try {
1267
+ return typeof toString === "function"
1268
+ ? toString(typeId, value)
1269
+ : String(value ?? "");
1270
+ }
1271
+ catch {
1272
+ return String(value ?? "");
1273
+ }
1274
+ };
1157
1275
  const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
1158
1276
  const nodeValidationIssues = validationByNode.issues;
1159
1277
  const edgeValidationIssues = validationByEdge.issues;
@@ -1213,7 +1331,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1213
1331
  for (const h of handles) {
1214
1332
  const typeId = desc?.inputs?.[h];
1215
1333
  const current = nodeInputs[h];
1216
- const display = toString(typeId, current);
1334
+ const display = safeToString(typeId, current);
1217
1335
  const wasOriginal = originals[h];
1218
1336
  const isDirty = drafts[h] !== undefined &&
1219
1337
  wasOriginal !== undefined &&
@@ -1239,7 +1357,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1239
1357
  disabled: isLinked,
1240
1358
  };
1241
1359
  const current = nodeInputs[h];
1242
- const value = drafts[h] ?? toString(typeId, current);
1360
+ const value = drafts[h] ?? safeToString(typeId, current);
1243
1361
  const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
1244
1362
  const commit = () => {
1245
1363
  const draft = drafts[h];
@@ -1249,7 +1367,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1249
1367
  setOriginals((o) => ({ ...o, [h]: draft }));
1250
1368
  };
1251
1369
  const revert = () => {
1252
- const orig = originals[h] ?? toString(typeId, current);
1370
+ const orig = originals[h] ?? safeToString(typeId, current);
1253
1371
  setDrafts((d) => ({ ...d, [h]: orig }));
1254
1372
  };
1255
1373
  const isEnum = typeId?.includes("enum:");
@@ -1259,19 +1377,17 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1259
1377
  const title = inIssues
1260
1378
  .map((v) => `${v.code}: ${v.message}`)
1261
1379
  .join("; ");
1262
- return (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-28", children: [h, jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: drafts[h] ?? toString(typeId, current), onChange: (e) => {
1263
- const label = String(e.target.value);
1264
- const byLabel = registry.enums
1265
- .get(typeId)
1266
- ?.labelToValue.get(label.toLowerCase());
1267
- let raw = (byLabel !== undefined ? byLabel : Number(label));
1268
- if (!Number.isFinite(raw))
1269
- raw = undefined;
1380
+ return (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-28", children: [h, jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: current !== undefined && current !== null
1381
+ ? String(current)
1382
+ : "", onChange: (e) => {
1383
+ const val = e.target.value;
1384
+ const raw = val === "" ? undefined : Number(val);
1270
1385
  setInput(h, raw);
1271
- const display = toString(typeId, raw);
1386
+ // keep drafts/originals in sync with label for display elsewhere
1387
+ const display = safeToString(typeId, raw);
1272
1388
  setDrafts((d) => ({ ...d, [h]: display }));
1273
1389
  setOriginals((o) => ({ ...o, [h]: display }));
1274
- }, ...commonProps, children: [jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsx("option", { value: opt.label, children: opt.label }, opt.value)))] })) : (jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", placeholder: isLinked ? "wired" : undefined, value: value, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
1390
+ }, ...commonProps, children: [jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] })) : isLinked ? (toElement(typeId, current)) : (jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", placeholder: isLinked ? "wired" : undefined, value: value, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
1275
1391
  if (e.key === "Enter")
1276
1392
  commit();
1277
1393
  if (e.key === "Escape")
@@ -1289,74 +1405,8 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1289
1405
  })()] }, h))))] }), selectedNodeValidation.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: selectedNodeValidation.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}` })] }, i))) })] }))] })) }), debug && (jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
1290
1406
  }
1291
1407
 
1292
- function toReactFlow(def, positions, registry, selectedNodeIds, selectedEdgeIds, opts) {
1293
- const nodeHandleMap = {};
1294
- const nodes = def.nodes.map((n) => {
1295
- const desc = registry.nodes.get(n.typeId);
1296
- const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
1297
- const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
1298
- nodeHandleMap[n.nodeId] = {
1299
- inputs: new Set(inputHandles.map((h) => h.id)),
1300
- outputs: new Set(outputHandles.map((h) => h.id)),
1301
- };
1302
- return {
1303
- id: n.nodeId,
1304
- data: {
1305
- typeId: n.typeId,
1306
- params: n.params,
1307
- inputHandles,
1308
- outputHandles,
1309
- showValues: opts?.showValues,
1310
- inputValues: opts?.inputs?.[n.nodeId],
1311
- outputValues: opts?.outputs?.[n.nodeId],
1312
- status: opts?.nodeStatus?.[n.nodeId],
1313
- validation: {
1314
- inputs: opts?.nodeValidation?.inputs?.[n.nodeId] ?? [],
1315
- outputs: opts?.nodeValidation?.outputs?.[n.nodeId] ?? [],
1316
- issues: opts?.nodeValidation?.issues?.[n.nodeId] ?? [],
1317
- },
1318
- toString: opts?.toString,
1319
- toElement: opts?.toElement,
1320
- },
1321
- position: positions[n.nodeId] ?? { x: 0, y: 0 },
1322
- type: opts?.resolveNodeType?.(n.typeId) ?? "spark:default",
1323
- selected: selectedNodeIds ? selectedNodeIds.has(n.nodeId) : undefined,
1324
- };
1325
- });
1326
- const edges = def.edges
1327
- .filter((e) => {
1328
- const src = nodeHandleMap[e.source.nodeId];
1329
- const dst = nodeHandleMap[e.target.nodeId];
1330
- if (!src || !dst)
1331
- return false;
1332
- return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
1333
- })
1334
- .map((e) => {
1335
- const st = opts?.edgeStatus?.[e.id];
1336
- const isRunning = !!st?.running;
1337
- const hasError = !!st?.lastError;
1338
- const isInvalidEdge = !!opts?.edgeValidation?.[e.id];
1339
- const style = hasError || isInvalidEdge
1340
- ? { stroke: "#ef4444", strokeWidth: 2 }
1341
- : isRunning
1342
- ? { stroke: "#3b82f6" }
1343
- : undefined;
1344
- return {
1345
- id: e.id,
1346
- source: e.source.nodeId,
1347
- target: e.target.nodeId,
1348
- sourceHandle: e.source.handle,
1349
- targetHandle: e.target.handle,
1350
- selected: selectedEdgeIds ? selectedEdgeIds.has(e.id) : undefined,
1351
- animated: isRunning,
1352
- style,
1353
- };
1354
- });
1355
- return { nodes, edges };
1356
- }
1357
-
1358
1408
  const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
1359
- const { typeId, showValues, inputValues, outputValues, toString, toElement, } = data;
1409
+ const { typeId, showValues, inputValues, outputValues, toString } = data;
1360
1410
  const inputEntries = data.inputHandles ?? [];
1361
1411
  const outputEntries = data.outputHandles ?? [];
1362
1412
  const status = data.status ?? {};
@@ -1372,19 +1422,26 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1372
1422
  const minWidth = data.showValues ? 320 : 160;
1373
1423
  const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
1374
1424
  const hasError = !!status.lastError;
1425
+ const hasValidationError = validation.issues.some((i) => i.level === "error");
1426
+ const hasValidationWarning = !hasValidationError && validation.issues.length > 0;
1375
1427
  const isRunning = !!status.running;
1376
1428
  const isInvalid = !!status.invalidated && !isRunning && !hasError;
1377
- const borderClasses = selected
1378
- ? "border-2 border-gray-900 dark:border-gray-100"
1379
- : hasError
1380
- ? "border-2 border-red-500"
1429
+ // Border color encodes severity; thickness encodes selection; style (dashed) encodes invalidated
1430
+ const borderWidth = selected ? "border-2" : "border";
1431
+ const borderStyle = isInvalid ? "border-dashed" : "border-solid";
1432
+ const borderColor = hasError || hasValidationError
1433
+ ? "border-red-500"
1434
+ : hasValidationWarning
1435
+ ? "border-amber-500"
1381
1436
  : isRunning
1382
- ? "border-2 border-blue-500 ring-2 ring-blue-200 dark:ring-blue-900"
1383
- : isInvalid
1384
- ? "border-2 border-amber-500 border-dashed"
1385
- : "border border-gray-500 dark:border-gray-400";
1437
+ ? "border-blue-500"
1438
+ : "border-gray-500 dark:border-gray-400";
1439
+ const ringClasses = isRunning
1440
+ ? "ring-2 ring-blue-200 dark:ring-blue-900"
1441
+ : undefined;
1442
+ const borderClasses = cx(borderWidth, borderStyle, borderColor, ringClasses);
1386
1443
  const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
1387
- return (jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900 border-solid", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [jsxs("div", { className: "flex h-6 items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", children: [jsx("strong", { className: "flex-1 h-full leading-6 text-xs", children: typeId }), jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsx(XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
1444
+ return (jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [jsxs("div", { className: "flex h-6 items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", children: [jsx("strong", { className: "flex-1 h-full leading-6 text-xs", children: typeId }), jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsx(XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
1388
1445
  ? "error"
1389
1446
  : "warning", size: 12, className: "w-3 h-3", title: validation.issues
1390
1447
  .map((v) => `${v.code}: ${v.message}`)
@@ -1395,8 +1452,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1395
1452
  const title = vIssues
1396
1453
  .map((v) => `${v.code}: ${v.message}`)
1397
1454
  .join("; ");
1398
- return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "target", position: Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8 }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues &&
1399
- (toElement ? (toElement(entry.typeId, inputValues?.[entry.id])) : toString ? (jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) })) : (jsx("span", { className: "ml-1 opacity-60", children: String(inputValues?.[entry.id]) })))] })] }, `in-${entry.id}`));
1455
+ return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "target", position: Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8 }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
1400
1456
  }), outputEntries.map((entry, i) => {
1401
1457
  const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
1402
1458
  const hasAny = vIssues.length > 0;
@@ -1404,8 +1460,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1404
1460
  const title = vIssues
1405
1461
  .map((v) => `${v.code}: ${v.message}`)
1406
1462
  .join("; ");
1407
- return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "source", position: Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8, textAlign: "right" }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues &&
1408
- (toElement ? (toElement(entry.typeId, outputValues?.[entry.id])) : toString ? (jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, outputValues?.[entry.id]) })) : (jsx("span", { className: "ml-1 opacity-60", children: String(outputValues?.[entry.id]) })))] })] }, `out-${entry.id}`));
1463
+ return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "source", position: Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8, textAlign: "right" }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, outputValues?.[entry.id]) }))] })] }, `out-${entry.id}`));
1409
1464
  })] }));
1410
1465
  });
1411
1466
  DefaultNode.displayName = "DefaultNode";
@@ -1413,15 +1468,135 @@ DefaultNode.displayName = "DefaultNode";
1413
1468
  function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
1414
1469
  const { registry } = useWorkbenchContext();
1415
1470
  const rf = useReactFlow();
1471
+ const ids = Array.from(registry.nodes.keys());
1472
+ // Group node ids by the segment before the first '.'
1473
+ const grouped = {};
1474
+ for (const id of ids) {
1475
+ const parts = id.split(".");
1476
+ const cat = parts.length > 1 ? parts[0] : "other";
1477
+ const label = parts.length > 1 ? parts.slice(1).join(".") : id;
1478
+ (grouped[cat] = grouped[cat] || []).push({ id, label });
1479
+ }
1480
+ const cats = Object.keys(grouped).sort((a, b) => a.localeCompare(b));
1481
+ cats.forEach((c) => grouped[c].sort((a, b) => a.label.localeCompare(b.label)));
1482
+ const totalCount = ids.length;
1483
+ // Ref for focus/outside click handling
1484
+ const ref = useRef(null);
1485
+ // Close on outside click and on ESC
1486
+ useEffect(() => {
1487
+ if (!open)
1488
+ return;
1489
+ const onDown = (e) => {
1490
+ if (!ref.current)
1491
+ return;
1492
+ if (!ref.current.contains(e.target))
1493
+ onClose();
1494
+ };
1495
+ const onKey = (e) => {
1496
+ if (e.key === "Escape")
1497
+ onClose();
1498
+ };
1499
+ window.addEventListener("mousedown", onDown, true);
1500
+ window.addEventListener("keydown", onKey);
1501
+ return () => {
1502
+ window.removeEventListener("mousedown", onDown, true);
1503
+ window.removeEventListener("keydown", onKey);
1504
+ };
1505
+ }, [open, onClose]);
1506
+ // Focus for keyboard accessibility
1507
+ useEffect(() => {
1508
+ if (open)
1509
+ ref.current?.focus();
1510
+ }, [open]);
1416
1511
  if (!open || !clientPos)
1417
1512
  return null;
1418
- const items = Array.from(registry.nodes.keys());
1513
+ // Clamp menu position to viewport
1514
+ const MENU_MIN_WIDTH = 180;
1515
+ const PADDING = 16; // rough padding/shadow
1516
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
1517
+ (MENU_MIN_WIDTH + PADDING));
1518
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
1419
1519
  const handleClick = (typeId) => {
1420
- const p = rf.project({ x: clientPos.x, y: clientPos.y });
1520
+ // project() is deprecated; use screenToFlowPosition for screen coordinates
1521
+ const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
1421
1522
  onAdd(typeId, p);
1422
1523
  onClose();
1423
1524
  };
1424
- return (jsxs("div", { 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: clientPos.x, top: clientPos.y }, onMouseLeave: onClose, children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Add Node" }), jsx("div", { className: "max-h-60 overflow-auto", children: items.map((id) => (jsx("button", { onClick: () => handleClick(id), className: "block w-full text-left px-2 py-1 hover:bg-gray-100 cursor-pointer", children: id }, id))) })] }));
1525
+ 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) => {
1526
+ e.preventDefault();
1527
+ e.stopPropagation();
1528
+ }, 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: "max-h-60 overflow-auto", children: cats.map((cat) => (jsxs("div", { className: "py-1", children: [jsxs("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wide text-gray-400", children: [cat, " ", jsxs("span", { className: "opacity-60 normal-case", children: ["(", grouped[cat].length, ")"] })] }), grouped[cat].map(({ id, label }) => (jsx("button", { onClick: () => handleClick(id), className: "block w-full text-left px-3 py-1 hover:bg-gray-100 cursor-pointer", title: id, children: label }, id)))] }, cat))) })] }));
1529
+ }
1530
+
1531
+ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
1532
+ const { wb, runner, engineKind } = useWorkbenchContext();
1533
+ const ref = useRef(null);
1534
+ // outside click + ESC
1535
+ useEffect(() => {
1536
+ if (!open)
1537
+ return;
1538
+ const onDown = (e) => {
1539
+ if (!ref.current)
1540
+ return;
1541
+ if (!ref.current.contains(e.target))
1542
+ onClose();
1543
+ };
1544
+ const onKey = (e) => {
1545
+ if (e.key === "Escape")
1546
+ onClose();
1547
+ };
1548
+ window.addEventListener("mousedown", onDown, true);
1549
+ window.addEventListener("keydown", onKey);
1550
+ return () => {
1551
+ window.removeEventListener("mousedown", onDown, true);
1552
+ window.removeEventListener("keydown", onKey);
1553
+ };
1554
+ }, [open, onClose]);
1555
+ useEffect(() => {
1556
+ if (open)
1557
+ ref.current?.focus();
1558
+ }, [open]);
1559
+ if (!open || !clientPos || !nodeId)
1560
+ return null;
1561
+ // clamp
1562
+ const MENU_MIN_WIDTH = 180;
1563
+ const PADDING = 16;
1564
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
1565
+ (MENU_MIN_WIDTH + PADDING));
1566
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
1567
+ // actions
1568
+ const handleDelete = () => {
1569
+ wb.removeNode(nodeId);
1570
+ onClose();
1571
+ };
1572
+ const handleDuplicate = () => {
1573
+ const def = wb.export();
1574
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
1575
+ if (!n)
1576
+ return onClose();
1577
+ const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
1578
+ wb.addNode({ typeId: n.typeId, params: n.params, position: { x: pos.x + 24, y: pos.y + 24 } });
1579
+ onClose();
1580
+ };
1581
+ const handleCopyId = async () => {
1582
+ try {
1583
+ await navigator.clipboard.writeText(nodeId);
1584
+ }
1585
+ catch { }
1586
+ onClose();
1587
+ };
1588
+ const canRunPull = engineKind()?.toString() === "pull";
1589
+ const handleRunPull = async () => {
1590
+ try {
1591
+ await runner.computeNode(nodeId);
1592
+ }
1593
+ catch { }
1594
+ onClose();
1595
+ };
1596
+ 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) => {
1597
+ e.preventDefault();
1598
+ e.stopPropagation();
1599
+ }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDuplicate, children: "Duplicate" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleCopyId, children: "Copy Node ID" })] }));
1425
1600
  }
1426
1601
 
1427
1602
  function WorkbenchCanvas({ showValues, toString, toElement, }) {
@@ -1439,18 +1614,21 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
1439
1614
  if (renderer)
1440
1615
  custom.set(typeId, renderer);
1441
1616
  }
1442
- const types = { "spark:default": DefaultNode };
1617
+ const types = {
1618
+ "spark-default": DefaultNode,
1619
+ default: DefaultNode,
1620
+ };
1443
1621
  for (const [typeId, comp] of custom.entries()) {
1444
- types[`spark:${typeId}`] = comp;
1622
+ types[`spark-${typeId}`] = comp;
1445
1623
  }
1446
- const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark:${nodeTypeId}` : "spark:default";
1624
+ const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark-${nodeTypeId}` : "spark-default";
1447
1625
  return { nodeTypes: types, resolveNodeType: resolver };
1448
1626
  // registry is stable; ui renderers expected to be set up before mount
1449
1627
  }, [wb, registry]);
1450
1628
  const { nodes, edges } = useMemo(() => {
1451
1629
  const def = wb.export();
1452
1630
  const sel = wb.getSelection();
1453
- return toReactFlow(def, wb.getPositions(), registry, new Set(sel.nodes), new Set(sel.edges), {
1631
+ return toReactFlow(def, wb.getPositions(), registry, {
1454
1632
  showValues,
1455
1633
  inputs: ioValues.inputs,
1456
1634
  outputs: ioValues.outputs,
@@ -1461,6 +1639,8 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
1461
1639
  edgeStatus,
1462
1640
  nodeValidation,
1463
1641
  edgeValidation,
1642
+ selectedNodeIds: new Set(sel.nodes),
1643
+ selectedEdgeIds: new Set(sel.edges),
1464
1644
  });
1465
1645
  }, [
1466
1646
  showValues,
@@ -1475,15 +1655,31 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
1475
1655
  ]);
1476
1656
  const [menuOpen, setMenuOpen] = useState(false);
1477
1657
  const [menuPos, setMenuPos] = useState(null);
1658
+ const [nodeMenuOpen, setNodeMenuOpen] = useState(false);
1659
+ const [nodeMenuPos, setNodeMenuPos] = useState(null);
1660
+ const [nodeAtMenu, setNodeAtMenu] = useState(null);
1478
1661
  const onContextMenu = (e) => {
1479
1662
  e.preventDefault();
1480
- setMenuPos({ x: e.clientX, y: e.clientY });
1481
- setMenuOpen(true);
1663
+ // Determine if right-clicked over a node by hit-testing selection
1664
+ const target = e.target?.closest(".react-flow__node");
1665
+ if (target) {
1666
+ // Resolve node id from data-id attribute React Flow sets
1667
+ const nodeId = target.getAttribute("data-id");
1668
+ setNodeAtMenu(nodeId);
1669
+ setNodeMenuPos({ x: e.clientX, y: e.clientY });
1670
+ setNodeMenuOpen(true);
1671
+ setMenuOpen(false);
1672
+ }
1673
+ else {
1674
+ setMenuPos({ x: e.clientX, y: e.clientY });
1675
+ setMenuOpen(true);
1676
+ setNodeMenuOpen(false);
1677
+ }
1482
1678
  };
1483
1679
  const addNodeAt = (typeId, pos) => {
1484
1680
  wb.addNode({ typeId, position: pos });
1485
1681
  };
1486
- return (jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onSelectionChange: onSelectionChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, children: [jsx(Background, {}), jsx(MiniMap, {}), jsx(Controls, {}), jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) })] }) }));
1682
+ return (jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onSelectionChange: onSelectionChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, children: [jsx(Background, {}), jsx(MiniMap, {}), jsx(Controls, {}), jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
1487
1683
  }
1488
1684
 
1489
1685
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
@@ -1776,7 +1972,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1776
1972
  return String(value);
1777
1973
  }, [registry]);
1778
1974
  const baseToElement = useCallback((typeId, value) => {
1779
- return jsx("span", { children: baseToString(typeId, value) });
1975
+ return (jsx("span", { className: "ml-1 opacity-60", children: baseToString(typeId, value) }));
1780
1976
  }, [baseToString]);
1781
1977
  const toString = useMemo(() => {
1782
1978
  if (overrides?.toString)
@@ -1807,7 +2003,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1807
2003
  catch (err) {
1808
2004
  alert(String(err?.message ?? err));
1809
2005
  }
1810
- }, disabled: !engine, children: "Start" })), jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx("span", { children: "Debug events" })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx("span", { children: "Show values in nodes" })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { showValues: showValues, toString: toString, toElement: toElement }, exampleState) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
2006
+ }, disabled: !engine, children: "Start" })), jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx("span", { children: "Debug events" })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx("span", { children: "Show values in nodes" })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { showValues: showValues, toString: toString, toElement: toElement }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
1811
2007
  }
1812
2008
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
1813
2009
  const [registry, setRegistry] = useState(createSimpleGraphRegistry());
@@ -1833,5 +2029,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
1833
2029
  }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides }) }));
1834
2030
  }
1835
2031
 
1836
- export { AbstractWorkbench, CLIWorkbench, DefaultUIExtensionRegistry, GraphRunner, InMemoryWorkbench, Inspector, ReactFlowWorkbench, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, toReactFlow$1 as toReactFlow, useQueryParamBoolean, useQueryParamString, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
2032
+ export { AbstractWorkbench, CLIWorkbench, DefaultUIExtensionRegistry, GraphRunner, InMemoryWorkbench, Inspector, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, getNodeBorderClassNames, toReactFlow, useQueryParamBoolean, useQueryParamString, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
1837
2033
  //# sourceMappingURL=index.js.map