@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/esm/index.js CHANGED
@@ -337,12 +337,16 @@ class AbstractGraphRunner {
337
337
  if (this.engine) {
338
338
  throw new Error("Engine already running. Stop the current engine first.");
339
339
  }
340
- this.currentDef = def;
341
340
  }
342
341
  setInput(nodeId, handle, value) {
343
342
  if (!this.stagedInputs[nodeId])
344
343
  this.stagedInputs[nodeId] = {};
345
- this.stagedInputs[nodeId][handle] = value;
344
+ if (value === undefined) {
345
+ delete this.stagedInputs[nodeId][handle];
346
+ }
347
+ else {
348
+ this.stagedInputs[nodeId][handle] = value;
349
+ }
346
350
  if (this.engine) {
347
351
  this.engine.setInput(nodeId, handle, value);
348
352
  }
@@ -360,7 +364,14 @@ class AbstractGraphRunner {
360
364
  return;
361
365
  if (!this.stagedInputs[nodeId])
362
366
  this.stagedInputs[nodeId] = {};
363
- Object.assign(this.stagedInputs[nodeId], inputs);
367
+ for (const [handle, value] of Object.entries(inputs)) {
368
+ if (value === undefined) {
369
+ delete this.stagedInputs[nodeId][handle];
370
+ }
371
+ else {
372
+ this.stagedInputs[nodeId][handle] = value;
373
+ }
374
+ }
364
375
  if (this.engine) {
365
376
  // Running: set all inputs
366
377
  this.engine.setInputs(nodeId, inputs);
@@ -394,7 +405,6 @@ class AbstractGraphRunner {
394
405
  this.engine = undefined;
395
406
  this.runtime?.dispose();
396
407
  this.runtime = undefined;
397
- this.currentDef = undefined;
398
408
  if (this.runningKind) {
399
409
  this.runningKind = undefined;
400
410
  this.emit("status", { running: false, engine: undefined });
@@ -429,14 +439,12 @@ class LocalGraphRunner extends AbstractGraphRunner {
429
439
  this.emit("transport", { state: "local" });
430
440
  }
431
441
  build(def) {
432
- this.currentDef = def;
433
442
  const builder = new GraphBuilder(this.registry);
434
443
  this.runtime = builder.build(def);
435
444
  // Signal UI that freshly built graph should be considered invalidated
436
445
  this.emit("invalidate", { reason: "graph-built" });
437
446
  }
438
447
  update(def) {
439
- this.currentDef = def;
440
448
  if (!this.runtime)
441
449
  return;
442
450
  // Prevent mid-run churn while wiring changes are applied
@@ -501,11 +509,11 @@ class LocalGraphRunner extends AbstractGraphRunner {
501
509
  if (eng instanceof BatchedEngine)
502
510
  eng.flush();
503
511
  }
504
- getOutputs() {
512
+ getOutputs(def) {
505
513
  const out = {};
506
- if (!this.runtime || !this.currentDef)
514
+ if (!this.runtime)
507
515
  return out;
508
- for (const n of this.currentDef.nodes) {
516
+ for (const n of def.nodes) {
509
517
  const desc = this.registry.nodes.get(n.typeId);
510
518
  const handles = Object.keys(desc?.outputs ?? {});
511
519
  for (const h of handles) {
@@ -519,17 +527,15 @@ class LocalGraphRunner extends AbstractGraphRunner {
519
527
  }
520
528
  return out;
521
529
  }
522
- getInputs() {
530
+ getInputs(def) {
523
531
  const out = {};
524
- if (!this.currentDef)
525
- return out;
526
- for (const n of this.currentDef.nodes) {
532
+ for (const n of def.nodes) {
527
533
  const staged = this.stagedInputs[n.nodeId] ?? {};
528
534
  const runtimeInputs = this.runtime
529
535
  ? this.runtime.getNodeData?.(n.nodeId)?.inputs ?? {}
530
536
  : {};
531
537
  // Build inbound handle set for this node from current def
532
- const inbound = new Set(this.currentDef.edges
538
+ const inbound = new Set(def.edges
533
539
  .filter((e) => e.target.nodeId === n.nodeId)
534
540
  .map((e) => e.target.handle));
535
541
  // Merge staged only for non-inbound handles so UI reflects runtime values for wired inputs
@@ -543,11 +549,9 @@ class LocalGraphRunner extends AbstractGraphRunner {
543
549
  }
544
550
  return out;
545
551
  }
546
- getInputDefaults() {
552
+ getInputDefaults(def) {
547
553
  const out = {};
548
- if (!this.currentDef)
549
- return out;
550
- for (const n of this.currentDef.nodes) {
554
+ for (const n of def.nodes) {
551
555
  const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
552
556
  if (Object.keys(dynDefaults).length > 0) {
553
557
  out[n.nodeId] = dynDefaults;
@@ -557,8 +561,24 @@ class LocalGraphRunner extends AbstractGraphRunner {
557
561
  }
558
562
  async snapshotFull() {
559
563
  const def = undefined; // UI will supply def/positions on download for local
560
- const inputs = this.getInputs();
561
- const outputs = this.getOutputs();
564
+ const inputs = this.getInputs(this.runtime
565
+ ? {
566
+ nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({
567
+ nodeId: id,
568
+ typeId: "",
569
+ })),
570
+ edges: [],
571
+ }
572
+ : { nodes: [], edges: [] });
573
+ const outputs = this.getOutputs(this.runtime
574
+ ? {
575
+ nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({
576
+ nodeId: id,
577
+ typeId: "",
578
+ })),
579
+ edges: [],
580
+ }
581
+ : { nodes: [], edges: [] });
562
582
  const environment = this.getEnvironment() || {};
563
583
  return { def, environment, inputs, outputs };
564
584
  }
@@ -635,8 +655,8 @@ class RemoteGraphRunner extends AbstractGraphRunner {
635
655
  this.emit("registry", this.registry);
636
656
  // Trigger update so validation/UI refreshes using last known graph
637
657
  try {
638
- if (this.currentDef)
639
- this.update(this.currentDef);
658
+ if (this.lastDef)
659
+ this.update(this.lastDef);
640
660
  }
641
661
  catch {
642
662
  console.error("Failed to update graph definition after registry changed");
@@ -654,12 +674,12 @@ class RemoteGraphRunner extends AbstractGraphRunner {
654
674
  console.warn("Unsupported operation for remote runner");
655
675
  }
656
676
  update(def) {
657
- this.currentDef = def;
658
677
  // Remote: forward update; ignore errors (fire-and-forget)
659
678
  this.ensureRemoteRunner().then(async (runner) => {
660
679
  try {
661
680
  await runner.update(def);
662
681
  this.emit("invalidate", { reason: "graph-updated" });
682
+ this.lastDef = def;
663
683
  }
664
684
  catch { }
665
685
  });
@@ -671,6 +691,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
671
691
  await runner.build(def);
672
692
  // Signal UI after remote build as well
673
693
  this.emit("invalidate", { reason: "graph-built" });
694
+ this.lastDef = def;
674
695
  // Hydrate current remote inputs/outputs (including defaults) into cache
675
696
  try {
676
697
  const snap = await runner.snapshot();
@@ -806,12 +827,12 @@ class RemoteGraphRunner extends AbstractGraphRunner {
806
827
  // For now, we expose an async helper on RemoteRunner. Keep sync signature per interface.
807
828
  return undefined;
808
829
  }
809
- getOutputs() {
830
+ getOutputs(def) {
810
831
  const out = {};
811
832
  const cache = this.valueCache;
812
- if (!cache || !this.currentDef)
833
+ if (!cache)
813
834
  return out;
814
- for (const n of this.currentDef.nodes) {
835
+ for (const n of def.nodes) {
815
836
  const resolved = n.resolvedHandles?.outputs;
816
837
  const desc = this.registry.nodes.get(n.typeId);
817
838
  const handles = Object.keys(resolved ?? desc?.outputs ?? {});
@@ -827,19 +848,17 @@ class RemoteGraphRunner extends AbstractGraphRunner {
827
848
  }
828
849
  return out;
829
850
  }
830
- getInputs() {
851
+ getInputs(def) {
831
852
  const out = {};
832
853
  const cache = this.valueCache;
833
- if (!this.currentDef)
834
- return out;
835
- for (const n of this.currentDef.nodes) {
854
+ for (const n of def.nodes) {
836
855
  const staged = this.stagedInputs[n.nodeId] ?? {};
837
856
  const resolved = n.resolvedHandles?.inputs;
838
857
  const desc = this.registry.nodes.get(n.typeId);
839
858
  const handles = Object.keys(resolved ?? desc?.inputs ?? {});
840
859
  const cur = {};
841
860
  // Build inbound handle set for this node to honor wiring precedence
842
- const inbound = new Set(this.currentDef.edges
861
+ const inbound = new Set(def.edges
843
862
  .filter((e) => e.target.nodeId === n.nodeId)
844
863
  .map((e) => e.target.handle));
845
864
  for (const h of handles) {
@@ -858,11 +877,9 @@ class RemoteGraphRunner extends AbstractGraphRunner {
858
877
  }
859
878
  return out;
860
879
  }
861
- getInputDefaults() {
880
+ getInputDefaults(def) {
862
881
  const out = {};
863
- if (!this.currentDef)
864
- return out;
865
- for (const n of this.currentDef.nodes) {
882
+ for (const n of def.nodes) {
866
883
  const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
867
884
  if (Object.keys(dynDefaults).length > 0) {
868
885
  out[n.nodeId] = dynDefaults;
@@ -1731,14 +1748,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1731
1748
  const clearEvents = useCallback(() => setEvents([]), []);
1732
1749
  const [systemErrors, setSystemErrors] = useState([]);
1733
1750
  const [registryErrors, setRegistryErrors] = useState([]);
1751
+ const [inputValidationErrors, setInputValidationErrors] = useState([]);
1734
1752
  const clearSystemErrors = useCallback(() => setSystemErrors([]), []);
1735
1753
  const clearRegistryErrors = useCallback(() => setRegistryErrors([]), []);
1754
+ const clearInputValidationErrors = useCallback(() => setInputValidationErrors([]), []);
1736
1755
  const removeSystemError = useCallback((index) => {
1737
1756
  setSystemErrors((prev) => prev.filter((_, idx) => idx !== index));
1738
1757
  }, []);
1739
1758
  const removeRegistryError = useCallback((index) => {
1740
1759
  setRegistryErrors((prev) => prev.filter((_, idx) => idx !== index));
1741
1760
  }, []);
1761
+ const removeInputValidationError = useCallback((index) => {
1762
+ setInputValidationErrors((prev) => prev.filter((_, idx) => idx !== index));
1763
+ }, []);
1742
1764
  // Fallback progress animation: drive progress to 100% over ~2 minutes
1743
1765
  const FALLBACK_TOTAL_MS = 2 * 60 * 1000;
1744
1766
  const [fallbackStarts, setFallbackStarts] = useState({});
@@ -1787,14 +1809,15 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1787
1809
  const valuesTick = versionTick + graphTick + graphUiTick;
1788
1810
  // Def and IO values
1789
1811
  const def = wb.export();
1790
- const inputsMap = useMemo(() => runner.getInputs(), [runner, valuesTick]);
1791
- const inputDefaultsMap = useMemo(() => runner.getInputDefaults(), [runner, valuesTick]);
1792
- const outputsMap = useMemo(() => runner.getOutputs(), [runner, valuesTick]);
1812
+ const inputsMap = useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
1813
+ const inputDefaultsMap = useMemo(() => runner.getInputDefaults(def), [runner, def, valuesTick]);
1814
+ const outputsMap = useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
1793
1815
  const outputTypesMap = useMemo(() => {
1794
1816
  const out = {};
1795
1817
  // Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
1796
1818
  for (const n of def.nodes) {
1797
- const outputsDecl = registry.nodes.get(n.typeId)?.outputs ?? {};
1819
+ const effectiveHandles = computeEffectiveHandles(n, registry);
1820
+ const outputsDecl = effectiveHandles.outputs;
1798
1821
  const handles = Object.keys(outputsDecl);
1799
1822
  const cur = {};
1800
1823
  for (const h of handles) {
@@ -1955,10 +1978,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1955
1978
  const offRunnerValue = runner.on("value", (e) => {
1956
1979
  if (e?.io === "input") {
1957
1980
  const nodeId = e?.nodeId;
1981
+ const handle = e?.handle;
1958
1982
  setNodeStatus((s) => ({
1959
1983
  ...s,
1960
1984
  [nodeId]: { ...s[nodeId], invalidated: true },
1961
1985
  }));
1986
+ // Clear validation errors for this input when a valid value is set
1987
+ setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
1962
1988
  }
1963
1989
  return add("runner", "value")(e);
1964
1990
  });
@@ -1967,6 +1993,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1967
1993
  const nodeError = e;
1968
1994
  const registryError = e;
1969
1995
  const systemError = e;
1996
+ const inputValidationError = e;
1970
1997
  if (edgeError.kind === "edge-convert") {
1971
1998
  const edgeId = edgeError.edgeId;
1972
1999
  setEdgeStatus((s) => ({
@@ -2002,6 +2029,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2002
2029
  return [...prev, registryError];
2003
2030
  });
2004
2031
  }
2032
+ else if (inputValidationError.kind === "input-validation") {
2033
+ // Track input validation errors for UI display
2034
+ setInputValidationErrors((prev) => {
2035
+ // Avoid duplicates by checking nodeId, handle, and typeId
2036
+ if (prev.some((err) => err.nodeId === inputValidationError.nodeId &&
2037
+ err.handle === inputValidationError.handle &&
2038
+ err.typeId === inputValidationError.typeId)) {
2039
+ return prev;
2040
+ }
2041
+ return [...prev, inputValidationError];
2042
+ });
2043
+ }
2005
2044
  else if (systemError.kind === "system") {
2006
2045
  // Track custom errors for UI display
2007
2046
  setSystemErrors((prev) => {
@@ -2125,7 +2164,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2125
2164
  }
2126
2165
  return add("runner", "stats")(s);
2127
2166
  });
2128
- const offWbGraphChanged = wb.on("graphChanged", add("workbench", "graphChanged"));
2167
+ const offWbGraphChanged = wb.on("graphChanged", (event) => {
2168
+ // Clear validation errors for removed nodes
2169
+ if (event.change?.type === "removeNode") {
2170
+ const removedNodeId = event.change.nodeId;
2171
+ setInputValidationErrors((prev) => prev.filter((err) => err.nodeId !== removedNodeId));
2172
+ }
2173
+ return add("workbench", "graphChanged")(event);
2174
+ });
2129
2175
  const offWbGraphUiChanged = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
2130
2176
  const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
2131
2177
  // Ensure newly added nodes start as invalidated until first evaluation
@@ -2189,13 +2235,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2189
2235
  offRunnerTransport();
2190
2236
  };
2191
2237
  }, [runner, wb]);
2192
- // Keep runner.currentDef in sync with the workbench graph at all times.
2193
- // When an engine/runtime exists, this also pushes incremental wiring changes into it.
2238
+ // Push incremental updates into running engine without full reload
2194
2239
  useEffect(() => {
2195
- try {
2196
- runner.update(def);
2240
+ if (runner.isRunning()) {
2241
+ try {
2242
+ runner.update(def);
2243
+ }
2244
+ catch { }
2197
2245
  }
2198
- catch { }
2199
2246
  }, [runner, def, graphTick]);
2200
2247
  const validationByNode = useMemo(() => {
2201
2248
  const inputs = {};
@@ -2294,10 +2341,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2294
2341
  clearEvents,
2295
2342
  systemErrors,
2296
2343
  registryErrors,
2344
+ inputValidationErrors,
2297
2345
  clearSystemErrors,
2298
2346
  clearRegistryErrors,
2347
+ clearInputValidationErrors,
2299
2348
  removeSystemError,
2300
2349
  removeRegistryError,
2350
+ removeInputValidationError,
2301
2351
  isRunning,
2302
2352
  engineKind,
2303
2353
  start,
@@ -2322,10 +2372,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2322
2372
  valuesTick,
2323
2373
  systemErrors,
2324
2374
  registryErrors,
2375
+ inputValidationErrors,
2325
2376
  clearSystemErrors,
2326
2377
  clearRegistryErrors,
2378
+ clearInputValidationErrors,
2327
2379
  removeSystemError,
2328
2380
  removeRegistryError,
2381
+ removeInputValidationError,
2329
2382
  inputsMap,
2330
2383
  inputDefaultsMap,
2331
2384
  outputsMap,
@@ -2413,7 +2466,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2413
2466
  return String(value ?? "");
2414
2467
  }
2415
2468
  };
2416
- const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, clearSystemErrors, clearRegistryErrors, removeSystemError, removeRegistryError, } = useWorkbenchContext();
2469
+ 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();
2417
2470
  const nodeValidationIssues = validationByNode.issues;
2418
2471
  const edgeValidationIssues = validationByEdge.issues;
2419
2472
  const nodeValidationHandles = validationByNode;
@@ -2524,7 +2577,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2524
2577
  }
2525
2578
  catch { }
2526
2579
  };
2527
- 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 }), 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) => {
2580
+ 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) => {
2528
2581
  e.stopPropagation();
2529
2582
  deleteEdgeById(m.data?.edgeId);
2530
2583
  }, 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] }), (() => {
@@ -2562,13 +2615,20 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2562
2615
  const current = nodeInputs[h];
2563
2616
  const hasValue = current !== undefined && current !== null;
2564
2617
  const value = drafts[h] ?? safeToString(typeId, current);
2565
- const displayValue = hasValue ? value : "";
2618
+ const displayValue = value;
2566
2619
  const placeholder = hasDefault ? defaultStr : undefined;
2567
2620
  const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
2568
2621
  const commit = () => {
2569
2622
  const draft = drafts[h];
2570
2623
  if (draft === undefined)
2571
2624
  return;
2625
+ // Only commit if draft differs from current value
2626
+ const currentDisplay = safeToString(typeId, current);
2627
+ if (draft === currentDisplay) {
2628
+ // No change, just sync originals without calling setInput
2629
+ setOriginals((o) => ({ ...o, [h]: draft }));
2630
+ return;
2631
+ }
2572
2632
  setInput(h, draft);
2573
2633
  setOriginals((o) => ({ ...o, [h]: draft }));
2574
2634
  };
@@ -3386,9 +3446,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3386
3446
  state: "local",
3387
3447
  });
3388
3448
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
3389
- const selectedDesc = selectedNode
3449
+ selectedNode
3390
3450
  ? registry.nodes.get(selectedNode.typeId)
3391
3451
  : undefined;
3452
+ const effectiveHandles = selectedNode
3453
+ ? computeEffectiveHandles(selectedNode, registry)
3454
+ : { inputs: {}, outputs: {}, inputDefaults: {} };
3392
3455
  const [exampleState, setExampleState] = useState(example ?? "");
3393
3456
  const defaultExamples = useMemo(() => [
3394
3457
  {
@@ -3463,7 +3526,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3463
3526
  const off1 = wb.on("graphChanged", () => {
3464
3527
  try {
3465
3528
  const cur = wb.export();
3466
- const inputs = runner.getInputs();
3529
+ const inputs = runner.getInputs(cur);
3467
3530
  onChange({ def: cur, inputs });
3468
3531
  }
3469
3532
  catch { }
@@ -3471,7 +3534,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3471
3534
  const off2 = runner.on("value", () => {
3472
3535
  try {
3473
3536
  const cur = wb.export();
3474
- const inputs = runner.getInputs();
3537
+ const inputs = runner.getInputs(cur);
3475
3538
  onChange({ def: cur, inputs });
3476
3539
  }
3477
3540
  catch { }
@@ -3517,7 +3580,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3517
3580
  const downloadGraph = useCallback(() => {
3518
3581
  try {
3519
3582
  const def = wb.export();
3520
- const inputs = runner.getInputs();
3583
+ const inputs = runner.getInputs(def);
3521
3584
  const payload = { def, inputs };
3522
3585
  const pretty = JSON.stringify(payload, null, 2);
3523
3586
  const blob = new Blob([pretty], { type: "application/json" });
@@ -3676,7 +3739,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3676
3739
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
3677
3740
  if (isLinked)
3678
3741
  return;
3679
- const typeId = selectedDesc?.inputs?.[handle];
3742
+ const typeId = effectiveHandles.inputs[handle];
3680
3743
  let value = raw;
3681
3744
  const parseArray = (s, map) => {
3682
3745
  const str = String(s).trim();
@@ -3753,7 +3816,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3753
3816
  }
3754
3817
  }
3755
3818
  runner.setInput(selectedNodeId, handle, value);
3756
- }, [selectedNodeId, def.edges, selectedDesc, runner]);
3819
+ }, [selectedNodeId, def.edges, effectiveHandles, runner]);
3757
3820
  const setInput = useMemo(() => {
3758
3821
  if (overrides?.setInput) {
3759
3822
  return overrides.setInput(baseSetInput, {