@bian-womp/spark-workbench 0.2.35 → 0.2.37

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 (31) hide show
  1. package/lib/cjs/index.cjs +117 -54
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
  4. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  5. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +4 -1
  6. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  7. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  8. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +3 -4
  9. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  10. package/lib/cjs/src/runtime/IGraphRunner.d.ts +3 -3
  11. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
  12. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +3 -3
  13. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  14. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +4 -3
  15. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  16. package/lib/esm/index.js +117 -54
  17. package/lib/esm/index.js.map +1 -1
  18. package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
  19. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  20. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +4 -1
  21. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  22. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  23. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +3 -4
  24. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  25. package/lib/esm/src/runtime/IGraphRunner.d.ts +3 -3
  26. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
  27. package/lib/esm/src/runtime/LocalGraphRunner.d.ts +3 -3
  28. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  29. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +4 -3
  30. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  31. package/package.json +4 -4
package/lib/cjs/index.cjs CHANGED
@@ -339,12 +339,16 @@ class AbstractGraphRunner {
339
339
  if (this.engine) {
340
340
  throw new Error("Engine already running. Stop the current engine first.");
341
341
  }
342
- this.currentDef = def;
343
342
  }
344
343
  setInput(nodeId, handle, value) {
345
344
  if (!this.stagedInputs[nodeId])
346
345
  this.stagedInputs[nodeId] = {};
347
- this.stagedInputs[nodeId][handle] = value;
346
+ if (value === undefined) {
347
+ delete this.stagedInputs[nodeId][handle];
348
+ }
349
+ else {
350
+ this.stagedInputs[nodeId][handle] = value;
351
+ }
348
352
  if (this.engine) {
349
353
  this.engine.setInput(nodeId, handle, value);
350
354
  }
@@ -362,7 +366,14 @@ class AbstractGraphRunner {
362
366
  return;
363
367
  if (!this.stagedInputs[nodeId])
364
368
  this.stagedInputs[nodeId] = {};
365
- Object.assign(this.stagedInputs[nodeId], inputs);
369
+ for (const [handle, value] of Object.entries(inputs)) {
370
+ if (value === undefined) {
371
+ delete this.stagedInputs[nodeId][handle];
372
+ }
373
+ else {
374
+ this.stagedInputs[nodeId][handle] = value;
375
+ }
376
+ }
366
377
  if (this.engine) {
367
378
  // Running: set all inputs
368
379
  this.engine.setInputs(nodeId, inputs);
@@ -396,7 +407,6 @@ class AbstractGraphRunner {
396
407
  this.engine = undefined;
397
408
  this.runtime?.dispose();
398
409
  this.runtime = undefined;
399
- this.currentDef = undefined;
400
410
  if (this.runningKind) {
401
411
  this.runningKind = undefined;
402
412
  this.emit("status", { running: false, engine: undefined });
@@ -431,14 +441,12 @@ class LocalGraphRunner extends AbstractGraphRunner {
431
441
  this.emit("transport", { state: "local" });
432
442
  }
433
443
  build(def) {
434
- this.currentDef = def;
435
444
  const builder = new sparkGraph.GraphBuilder(this.registry);
436
445
  this.runtime = builder.build(def);
437
446
  // Signal UI that freshly built graph should be considered invalidated
438
447
  this.emit("invalidate", { reason: "graph-built" });
439
448
  }
440
449
  update(def) {
441
- this.currentDef = def;
442
450
  if (!this.runtime)
443
451
  return;
444
452
  // Prevent mid-run churn while wiring changes are applied
@@ -503,11 +511,11 @@ class LocalGraphRunner extends AbstractGraphRunner {
503
511
  if (eng instanceof sparkGraph.BatchedEngine)
504
512
  eng.flush();
505
513
  }
506
- getOutputs() {
514
+ getOutputs(def) {
507
515
  const out = {};
508
- if (!this.runtime || !this.currentDef)
516
+ if (!this.runtime)
509
517
  return out;
510
- for (const n of this.currentDef.nodes) {
518
+ for (const n of def.nodes) {
511
519
  const desc = this.registry.nodes.get(n.typeId);
512
520
  const handles = Object.keys(desc?.outputs ?? {});
513
521
  for (const h of handles) {
@@ -521,17 +529,15 @@ class LocalGraphRunner extends AbstractGraphRunner {
521
529
  }
522
530
  return out;
523
531
  }
524
- getInputs() {
532
+ getInputs(def) {
525
533
  const out = {};
526
- if (!this.currentDef)
527
- return out;
528
- for (const n of this.currentDef.nodes) {
534
+ for (const n of def.nodes) {
529
535
  const staged = this.stagedInputs[n.nodeId] ?? {};
530
536
  const runtimeInputs = this.runtime
531
537
  ? this.runtime.getNodeData?.(n.nodeId)?.inputs ?? {}
532
538
  : {};
533
539
  // Build inbound handle set for this node from current def
534
- const inbound = new Set(this.currentDef.edges
540
+ const inbound = new Set(def.edges
535
541
  .filter((e) => e.target.nodeId === n.nodeId)
536
542
  .map((e) => e.target.handle));
537
543
  // Merge staged only for non-inbound handles so UI reflects runtime values for wired inputs
@@ -545,11 +551,9 @@ class LocalGraphRunner extends AbstractGraphRunner {
545
551
  }
546
552
  return out;
547
553
  }
548
- getInputDefaults() {
554
+ getInputDefaults(def) {
549
555
  const out = {};
550
- if (!this.currentDef)
551
- return out;
552
- for (const n of this.currentDef.nodes) {
556
+ for (const n of def.nodes) {
553
557
  const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
554
558
  if (Object.keys(dynDefaults).length > 0) {
555
559
  out[n.nodeId] = dynDefaults;
@@ -559,8 +563,24 @@ class LocalGraphRunner extends AbstractGraphRunner {
559
563
  }
560
564
  async snapshotFull() {
561
565
  const def = undefined; // UI will supply def/positions on download for local
562
- const inputs = this.getInputs();
563
- const outputs = this.getOutputs();
566
+ const inputs = this.getInputs(this.runtime
567
+ ? {
568
+ nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({
569
+ nodeId: id,
570
+ typeId: "",
571
+ })),
572
+ edges: [],
573
+ }
574
+ : { nodes: [], edges: [] });
575
+ const outputs = this.getOutputs(this.runtime
576
+ ? {
577
+ nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({
578
+ nodeId: id,
579
+ typeId: "",
580
+ })),
581
+ edges: [],
582
+ }
583
+ : { nodes: [], edges: [] });
564
584
  const environment = this.getEnvironment() || {};
565
585
  return { def, environment, inputs, outputs };
566
586
  }
@@ -637,8 +657,8 @@ class RemoteGraphRunner extends AbstractGraphRunner {
637
657
  this.emit("registry", this.registry);
638
658
  // Trigger update so validation/UI refreshes using last known graph
639
659
  try {
640
- if (this.currentDef)
641
- this.update(this.currentDef);
660
+ if (this.lastDef)
661
+ this.update(this.lastDef);
642
662
  }
643
663
  catch {
644
664
  console.error("Failed to update graph definition after registry changed");
@@ -656,12 +676,12 @@ class RemoteGraphRunner extends AbstractGraphRunner {
656
676
  console.warn("Unsupported operation for remote runner");
657
677
  }
658
678
  update(def) {
659
- this.currentDef = def;
660
679
  // Remote: forward update; ignore errors (fire-and-forget)
661
680
  this.ensureRemoteRunner().then(async (runner) => {
662
681
  try {
663
682
  await runner.update(def);
664
683
  this.emit("invalidate", { reason: "graph-updated" });
684
+ this.lastDef = def;
665
685
  }
666
686
  catch { }
667
687
  });
@@ -673,6 +693,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
673
693
  await runner.build(def);
674
694
  // Signal UI after remote build as well
675
695
  this.emit("invalidate", { reason: "graph-built" });
696
+ this.lastDef = def;
676
697
  // Hydrate current remote inputs/outputs (including defaults) into cache
677
698
  try {
678
699
  const snap = await runner.snapshot();
@@ -808,12 +829,12 @@ class RemoteGraphRunner extends AbstractGraphRunner {
808
829
  // For now, we expose an async helper on RemoteRunner. Keep sync signature per interface.
809
830
  return undefined;
810
831
  }
811
- getOutputs() {
832
+ getOutputs(def) {
812
833
  const out = {};
813
834
  const cache = this.valueCache;
814
- if (!cache || !this.currentDef)
835
+ if (!cache)
815
836
  return out;
816
- for (const n of this.currentDef.nodes) {
837
+ for (const n of def.nodes) {
817
838
  const resolved = n.resolvedHandles?.outputs;
818
839
  const desc = this.registry.nodes.get(n.typeId);
819
840
  const handles = Object.keys(resolved ?? desc?.outputs ?? {});
@@ -829,19 +850,17 @@ class RemoteGraphRunner extends AbstractGraphRunner {
829
850
  }
830
851
  return out;
831
852
  }
832
- getInputs() {
853
+ getInputs(def) {
833
854
  const out = {};
834
855
  const cache = this.valueCache;
835
- if (!this.currentDef)
836
- return out;
837
- for (const n of this.currentDef.nodes) {
856
+ for (const n of def.nodes) {
838
857
  const staged = this.stagedInputs[n.nodeId] ?? {};
839
858
  const resolved = n.resolvedHandles?.inputs;
840
859
  const desc = this.registry.nodes.get(n.typeId);
841
860
  const handles = Object.keys(resolved ?? desc?.inputs ?? {});
842
861
  const cur = {};
843
862
  // Build inbound handle set for this node to honor wiring precedence
844
- const inbound = new Set(this.currentDef.edges
863
+ const inbound = new Set(def.edges
845
864
  .filter((e) => e.target.nodeId === n.nodeId)
846
865
  .map((e) => e.target.handle));
847
866
  for (const h of handles) {
@@ -860,11 +879,9 @@ class RemoteGraphRunner extends AbstractGraphRunner {
860
879
  }
861
880
  return out;
862
881
  }
863
- getInputDefaults() {
882
+ getInputDefaults(def) {
864
883
  const out = {};
865
- if (!this.currentDef)
866
- return out;
867
- for (const n of this.currentDef.nodes) {
884
+ for (const n of def.nodes) {
868
885
  const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
869
886
  if (Object.keys(dynDefaults).length > 0) {
870
887
  out[n.nodeId] = dynDefaults;
@@ -1733,14 +1750,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1733
1750
  const clearEvents = React.useCallback(() => setEvents([]), []);
1734
1751
  const [systemErrors, setSystemErrors] = React.useState([]);
1735
1752
  const [registryErrors, setRegistryErrors] = React.useState([]);
1753
+ const [inputValidationErrors, setInputValidationErrors] = React.useState([]);
1736
1754
  const clearSystemErrors = React.useCallback(() => setSystemErrors([]), []);
1737
1755
  const clearRegistryErrors = React.useCallback(() => setRegistryErrors([]), []);
1756
+ const clearInputValidationErrors = React.useCallback(() => setInputValidationErrors([]), []);
1738
1757
  const removeSystemError = React.useCallback((index) => {
1739
1758
  setSystemErrors((prev) => prev.filter((_, idx) => idx !== index));
1740
1759
  }, []);
1741
1760
  const removeRegistryError = React.useCallback((index) => {
1742
1761
  setRegistryErrors((prev) => prev.filter((_, idx) => idx !== index));
1743
1762
  }, []);
1763
+ const removeInputValidationError = React.useCallback((index) => {
1764
+ setInputValidationErrors((prev) => prev.filter((_, idx) => idx !== index));
1765
+ }, []);
1744
1766
  // Fallback progress animation: drive progress to 100% over ~2 minutes
1745
1767
  const FALLBACK_TOTAL_MS = 2 * 60 * 1000;
1746
1768
  const [fallbackStarts, setFallbackStarts] = React.useState({});
@@ -1789,14 +1811,15 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1789
1811
  const valuesTick = versionTick + graphTick + graphUiTick;
1790
1812
  // Def and IO values
1791
1813
  const def = wb.export();
1792
- const inputsMap = React.useMemo(() => runner.getInputs(), [runner, valuesTick]);
1793
- const inputDefaultsMap = React.useMemo(() => runner.getInputDefaults(), [runner, valuesTick]);
1794
- const outputsMap = React.useMemo(() => runner.getOutputs(), [runner, valuesTick]);
1814
+ const inputsMap = React.useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
1815
+ const inputDefaultsMap = React.useMemo(() => runner.getInputDefaults(def), [runner, def, valuesTick]);
1816
+ const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
1795
1817
  const outputTypesMap = React.useMemo(() => {
1796
1818
  const out = {};
1797
1819
  // Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
1798
1820
  for (const n of def.nodes) {
1799
- const outputsDecl = registry.nodes.get(n.typeId)?.outputs ?? {};
1821
+ const effectiveHandles = computeEffectiveHandles(n, registry);
1822
+ const outputsDecl = effectiveHandles.outputs;
1800
1823
  const handles = Object.keys(outputsDecl);
1801
1824
  const cur = {};
1802
1825
  for (const h of handles) {
@@ -1957,10 +1980,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1957
1980
  const offRunnerValue = runner.on("value", (e) => {
1958
1981
  if (e?.io === "input") {
1959
1982
  const nodeId = e?.nodeId;
1983
+ const handle = e?.handle;
1960
1984
  setNodeStatus((s) => ({
1961
1985
  ...s,
1962
1986
  [nodeId]: { ...s[nodeId], invalidated: true },
1963
1987
  }));
1988
+ // Clear validation errors for this input when a valid value is set
1989
+ setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
1964
1990
  }
1965
1991
  return add("runner", "value")(e);
1966
1992
  });
@@ -1969,6 +1995,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1969
1995
  const nodeError = e;
1970
1996
  const registryError = e;
1971
1997
  const systemError = e;
1998
+ const inputValidationError = e;
1972
1999
  if (edgeError.kind === "edge-convert") {
1973
2000
  const edgeId = edgeError.edgeId;
1974
2001
  setEdgeStatus((s) => ({
@@ -2004,6 +2031,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2004
2031
  return [...prev, registryError];
2005
2032
  });
2006
2033
  }
2034
+ else if (inputValidationError.kind === "input-validation") {
2035
+ // Track input validation errors for UI display
2036
+ setInputValidationErrors((prev) => {
2037
+ // Avoid duplicates by checking nodeId, handle, and typeId
2038
+ if (prev.some((err) => err.nodeId === inputValidationError.nodeId &&
2039
+ err.handle === inputValidationError.handle &&
2040
+ err.typeId === inputValidationError.typeId)) {
2041
+ return prev;
2042
+ }
2043
+ return [...prev, inputValidationError];
2044
+ });
2045
+ }
2007
2046
  else if (systemError.kind === "system") {
2008
2047
  // Track custom errors for UI display
2009
2048
  setSystemErrors((prev) => {
@@ -2127,7 +2166,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2127
2166
  }
2128
2167
  return add("runner", "stats")(s);
2129
2168
  });
2130
- const offWbGraphChanged = wb.on("graphChanged", add("workbench", "graphChanged"));
2169
+ const offWbGraphChanged = wb.on("graphChanged", (event) => {
2170
+ // Clear validation errors for removed nodes
2171
+ if (event.change?.type === "removeNode") {
2172
+ const removedNodeId = event.change.nodeId;
2173
+ setInputValidationErrors((prev) => prev.filter((err) => err.nodeId !== removedNodeId));
2174
+ }
2175
+ return add("workbench", "graphChanged")(event);
2176
+ });
2131
2177
  const offWbGraphUiChanged = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
2132
2178
  const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
2133
2179
  // Ensure newly added nodes start as invalidated until first evaluation
@@ -2191,13 +2237,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2191
2237
  offRunnerTransport();
2192
2238
  };
2193
2239
  }, [runner, wb]);
2194
- // Keep runner.currentDef in sync with the workbench graph at all times.
2195
- // When an engine/runtime exists, this also pushes incremental wiring changes into it.
2240
+ // Push incremental updates into running engine without full reload
2196
2241
  React.useEffect(() => {
2197
- try {
2198
- runner.update(def);
2242
+ if (runner.isRunning()) {
2243
+ try {
2244
+ runner.update(def);
2245
+ }
2246
+ catch { }
2199
2247
  }
2200
- catch { }
2201
2248
  }, [runner, def, graphTick]);
2202
2249
  const validationByNode = React.useMemo(() => {
2203
2250
  const inputs = {};
@@ -2296,10 +2343,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2296
2343
  clearEvents,
2297
2344
  systemErrors,
2298
2345
  registryErrors,
2346
+ inputValidationErrors,
2299
2347
  clearSystemErrors,
2300
2348
  clearRegistryErrors,
2349
+ clearInputValidationErrors,
2301
2350
  removeSystemError,
2302
2351
  removeRegistryError,
2352
+ removeInputValidationError,
2303
2353
  isRunning,
2304
2354
  engineKind,
2305
2355
  start,
@@ -2324,10 +2374,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2324
2374
  valuesTick,
2325
2375
  systemErrors,
2326
2376
  registryErrors,
2377
+ inputValidationErrors,
2327
2378
  clearSystemErrors,
2328
2379
  clearRegistryErrors,
2380
+ clearInputValidationErrors,
2329
2381
  removeSystemError,
2330
2382
  removeRegistryError,
2383
+ removeInputValidationError,
2331
2384
  inputsMap,
2332
2385
  inputDefaultsMap,
2333
2386
  outputsMap,
@@ -2415,7 +2468,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2415
2468
  return String(value ?? "");
2416
2469
  }
2417
2470
  };
2418
- const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, clearSystemErrors, clearRegistryErrors, removeSystemError, removeRegistryError, } = useWorkbenchContext();
2471
+ const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, inputValidationErrors, clearSystemErrors, clearRegistryErrors, clearInputValidationErrors, removeSystemError, removeRegistryError, removeInputValidationError, } = useWorkbenchContext();
2419
2472
  const nodeValidationIssues = validationByNode.issues;
2420
2473
  const edgeValidationIssues = validationByEdge.issues;
2421
2474
  const nodeValidationHandles = validationByNode;
@@ -2526,7 +2579,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2526
2579
  }
2527
2580
  catch { }
2528
2581
  };
2529
- return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2582
+ return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), jsxRuntime.jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2530
2583
  e.stopPropagation();
2531
2584
  deleteEdgeById(m.data?.edgeId);
2532
2585
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), (() => {
@@ -2564,13 +2617,20 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2564
2617
  const current = nodeInputs[h];
2565
2618
  const hasValue = current !== undefined && current !== null;
2566
2619
  const value = drafts[h] ?? safeToString(typeId, current);
2567
- const displayValue = hasValue ? value : "";
2620
+ const displayValue = value;
2568
2621
  const placeholder = hasDefault ? defaultStr : undefined;
2569
2622
  const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
2570
2623
  const commit = () => {
2571
2624
  const draft = drafts[h];
2572
2625
  if (draft === undefined)
2573
2626
  return;
2627
+ // Only commit if draft differs from current value
2628
+ const currentDisplay = safeToString(typeId, current);
2629
+ if (draft === currentDisplay) {
2630
+ // No change, just sync originals without calling setInput
2631
+ setOriginals((o) => ({ ...o, [h]: draft }));
2632
+ return;
2633
+ }
2574
2634
  setInput(h, draft);
2575
2635
  setOriginals((o) => ({ ...o, [h]: draft }));
2576
2636
  };
@@ -3388,9 +3448,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3388
3448
  state: "local",
3389
3449
  });
3390
3450
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
3391
- const selectedDesc = selectedNode
3451
+ selectedNode
3392
3452
  ? registry.nodes.get(selectedNode.typeId)
3393
3453
  : undefined;
3454
+ const effectiveHandles = selectedNode
3455
+ ? computeEffectiveHandles(selectedNode, registry)
3456
+ : { inputs: {}, outputs: {}, inputDefaults: {} };
3394
3457
  const [exampleState, setExampleState] = React.useState(example ?? "");
3395
3458
  const defaultExamples = React.useMemo(() => [
3396
3459
  {
@@ -3465,7 +3528,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3465
3528
  const off1 = wb.on("graphChanged", () => {
3466
3529
  try {
3467
3530
  const cur = wb.export();
3468
- const inputs = runner.getInputs();
3531
+ const inputs = runner.getInputs(cur);
3469
3532
  onChange({ def: cur, inputs });
3470
3533
  }
3471
3534
  catch { }
@@ -3473,7 +3536,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3473
3536
  const off2 = runner.on("value", () => {
3474
3537
  try {
3475
3538
  const cur = wb.export();
3476
- const inputs = runner.getInputs();
3539
+ const inputs = runner.getInputs(cur);
3477
3540
  onChange({ def: cur, inputs });
3478
3541
  }
3479
3542
  catch { }
@@ -3519,7 +3582,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3519
3582
  const downloadGraph = React.useCallback(() => {
3520
3583
  try {
3521
3584
  const def = wb.export();
3522
- const inputs = runner.getInputs();
3585
+ const inputs = runner.getInputs(def);
3523
3586
  const payload = { def, inputs };
3524
3587
  const pretty = JSON.stringify(payload, null, 2);
3525
3588
  const blob = new Blob([pretty], { type: "application/json" });
@@ -3678,7 +3741,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3678
3741
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
3679
3742
  if (isLinked)
3680
3743
  return;
3681
- const typeId = selectedDesc?.inputs?.[handle];
3744
+ const typeId = effectiveHandles.inputs[handle];
3682
3745
  let value = raw;
3683
3746
  const parseArray = (s, map) => {
3684
3747
  const str = String(s).trim();
@@ -3755,7 +3818,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3755
3818
  }
3756
3819
  }
3757
3820
  runner.setInput(selectedNodeId, handle, value);
3758
- }, [selectedNodeId, def.edges, selectedDesc, runner]);
3821
+ }, [selectedNodeId, def.edges, effectiveHandles, runner]);
3759
3822
  const setInput = React.useMemo(() => {
3760
3823
  if (overrides?.setInput) {
3761
3824
  return overrides.setInput(baseSetInput, {