@bian-womp/spark-workbench 0.2.30 → 0.2.32

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.
package/lib/cjs/index.cjs CHANGED
@@ -477,7 +477,7 @@ class LocalGraphRunner extends AbstractGraphRunner {
477
477
  this.engine.on("error", (e) => this.emit("error", e));
478
478
  this.engine.on("invalidate", (e) => this.emit("invalidate", e));
479
479
  this.engine.on("stats", (e) => this.emit("stats", e));
480
- this.engine.launch();
480
+ this.engine.launch(opts.invalidate);
481
481
  this.runningKind = opts.engine;
482
482
  this.emit("status", { running: true, engine: this.runningKind });
483
483
  for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
@@ -710,7 +710,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
710
710
  this.listenersBound = true;
711
711
  }
712
712
  this.engine = eng;
713
- this.engine.launch();
713
+ this.engine.launch(opts.invalidate);
714
714
  this.runningKind = "push";
715
715
  this.emit("status", { running: true, engine: this.runningKind });
716
716
  for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
@@ -754,9 +754,39 @@ class RemoteGraphRunner extends AbstractGraphRunner {
754
754
  }
755
755
  }
756
756
  async applySnapshotFull(payload) {
757
+ // Hydrate local cache first so UI can display values immediately
758
+ this.hydrateCacheFromSnapshot(payload);
759
+ // Then sync with backend
757
760
  const runner = await this.ensureRemoteRunner();
758
761
  await runner.applySnapshotFull(payload);
759
762
  }
763
+ /**
764
+ * Hydrates the local valueCache from a snapshot and emits value events.
765
+ * This ensures the UI can display inputs/outputs immediately without waiting
766
+ * for value events from the remote backend.
767
+ */
768
+ hydrateCacheFromSnapshot(snapshot) {
769
+ // Hydrate inputs
770
+ for (const [nodeId, map] of Object.entries(snapshot.inputs || {})) {
771
+ for (const [handle, value] of Object.entries(map || {})) {
772
+ this.valueCache.set(`${nodeId}.${handle}`, {
773
+ io: "input",
774
+ value,
775
+ });
776
+ this.emit("value", { nodeId, handle, value, io: "input" });
777
+ }
778
+ }
779
+ // Hydrate outputs
780
+ for (const [nodeId, map] of Object.entries(snapshot.outputs || {})) {
781
+ for (const [handle, value] of Object.entries(map || {})) {
782
+ this.valueCache.set(`${nodeId}.${handle}`, {
783
+ io: "output",
784
+ value,
785
+ });
786
+ this.emit("value", { nodeId, handle, value, io: "output" });
787
+ }
788
+ }
789
+ }
760
790
  setEnvironment(env, opts) {
761
791
  const t = this.transport;
762
792
  if (!t)
@@ -2331,9 +2361,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2331
2361
  const globalValidationIssues = validationGlobal;
2332
2362
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
2333
2363
  const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
2334
- selectedNode
2335
- ? registry.nodes.get(selectedNode.typeId)
2336
- : undefined;
2337
2364
  // Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
2338
2365
  const effectiveHandles = selectedNode
2339
2366
  ? computeEffectiveHandles(selectedNode, registry)
@@ -2413,81 +2440,81 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2413
2440
  }
2414
2441
  catch { }
2415
2442
  };
2416
- return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, 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: "\u00D7" })] }, 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: "\u00D7" })] }, 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 overflow-auto", 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) => {
2417
- e.stopPropagation();
2418
- deleteEdgeById(m.data?.edgeId);
2419
- }, 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] }), jsxRuntime.jsx("div", { className: "mt-1", children: jsxRuntime.jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2420
- e.stopPropagation();
2421
- deleteEdgeById(selectedEdge.id);
2422
- }, title: "Delete this edge", children: "Delete edge" }) }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-1", children: [jsxRuntime.jsxs("label", { className: "w-20 flex flex-col", children: [jsxRuntime.jsx("span", { children: "Type" }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: "DataTypeId" })] }), jsxRuntime.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: selectedEdge.typeId ?? "", onChange: (e) => {
2423
- const v = e.target.value;
2424
- const next = v === "" ? undefined : v;
2425
- updateEdgeType(selectedEdge.id, next);
2426
- }, children: [jsxRuntime.jsx("option", { value: "", children: "(infer from source)" }), Array.from(registry.types.keys()).map((tid) => (jsxRuntime.jsx("option", { value: tid, children: tid }, tid)))] })] })] }), selectedEdgeValidation.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: selectedEdgeValidation.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}` }), 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) => {
2443
+ 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) => {
2444
+ e.stopPropagation();
2445
+ deleteEdgeById(m.data?.edgeId);
2446
+ }, 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] }), jsxRuntime.jsx("div", { className: "mt-1", children: jsxRuntime.jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2427
2447
  e.stopPropagation();
2428
2448
  deleteEdgeById(selectedEdge.id);
2429
- }, title: "Delete this edge", children: "Delete edge" })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
2430
- selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
2431
- const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
2432
- const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
2433
- e.target.handle === h);
2434
- const commonProps = {
2435
- style: { flex: 1 },
2436
- disabled: isLinked,
2437
- };
2438
- const current = nodeInputs[h];
2439
- const value = drafts[h] ?? safeToString(typeId, current);
2440
- const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
2441
- const commit = () => {
2442
- const draft = drafts[h];
2443
- if (draft === undefined)
2444
- return;
2445
- setInput(h, draft);
2446
- setOriginals((o) => ({ ...o, [h]: draft }));
2447
- };
2448
- const revert = () => {
2449
- const orig = originals[h] ?? safeToString(typeId, current);
2450
- setDrafts((d) => ({ ...d, [h]: orig }));
2451
- };
2452
- const isEnum = typeId?.startsWith("enum:");
2453
- const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
2454
- const hasValidation = inIssues.length > 0;
2455
- const hasErr = inIssues.some((m) => m.level === "error");
2456
- const title = inIssues
2457
- .map((v) => `${v.code}: ${v.message}`)
2458
- .join("; ");
2459
- return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("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
2460
- ? String(current)
2461
- : "", onChange: (e) => {
2462
- const val = e.target.value;
2463
- const raw = val === "" ? undefined : Number(val);
2464
- setInput(h, raw);
2465
- // keep drafts/originals in sync with label for display elsewhere
2466
- const display = safeToString(typeId, raw);
2467
- setDrafts((d) => ({ ...d, [h]: display }));
2468
- setOriginals((o) => ({ ...o, [h]: display }));
2469
- }, ...commonProps, children: [jsxRuntime.jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] })) : isLinked ? (toElement(typeId, current)) : (jsxRuntime.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) => {
2470
- if (e.key === "Enter")
2471
- commit();
2472
- if (e.key === "Escape")
2473
- revert();
2474
- }, ...commonProps }))] }, h));
2475
- }))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-20 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: outputTypesMap[selectedNodeId]?.[h] ?? "" })] }), jsxRuntime.jsx("div", { className: "flex-1", children: (() => {
2476
- const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]);
2477
- return toElement(typeId, value);
2478
- })() }), (() => {
2479
- const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
2480
- if (outIssues.length === 0)
2481
- return null;
2482
- const outErr = outIssues.some((m) => m.level === "error");
2483
- const outTitle = outIssues
2449
+ }, title: "Delete this edge", children: "Delete edge" }) }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-1", children: [jsxRuntime.jsxs("label", { className: "w-20 flex flex-col", children: [jsxRuntime.jsx("span", { children: "Type" }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: "DataTypeId" })] }), jsxRuntime.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: selectedEdge.typeId ?? "", onChange: (e) => {
2450
+ const v = e.target.value;
2451
+ const next = v === "" ? undefined : v;
2452
+ updateEdgeType(selectedEdge.id, next);
2453
+ }, children: [jsxRuntime.jsx("option", { value: "", children: "(infer from source)" }), Array.from(registry.types.keys()).map((tid) => (jsxRuntime.jsx("option", { value: tid, children: tid }, tid)))] })] })] }), selectedEdgeValidation.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: selectedEdgeValidation.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}` }), 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) => {
2454
+ e.stopPropagation();
2455
+ deleteEdgeById(selectedEdge.id);
2456
+ }, title: "Delete this edge", children: "Delete edge" })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
2457
+ selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
2458
+ const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
2459
+ const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
2460
+ e.target.handle === h);
2461
+ const commonProps = {
2462
+ style: { flex: 1 },
2463
+ disabled: isLinked,
2464
+ };
2465
+ const current = nodeInputs[h];
2466
+ const value = drafts[h] ?? safeToString(typeId, current);
2467
+ const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
2468
+ const commit = () => {
2469
+ const draft = drafts[h];
2470
+ if (draft === undefined)
2471
+ return;
2472
+ setInput(h, draft);
2473
+ setOriginals((o) => ({ ...o, [h]: draft }));
2474
+ };
2475
+ const revert = () => {
2476
+ const orig = originals[h] ?? safeToString(typeId, current);
2477
+ setDrafts((d) => ({ ...d, [h]: orig }));
2478
+ };
2479
+ const isEnum = typeId?.startsWith("enum:");
2480
+ const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
2481
+ const hasValidation = inIssues.length > 0;
2482
+ const hasErr = inIssues.some((m) => m.level === "error");
2483
+ const title = inIssues
2484
2484
  .map((v) => `${v.code}: ${v.message}`)
2485
2485
  .join("; ");
2486
- return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
2487
- })()] }, h))))] }), selectedNodeValidation.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: selectedNodeValidation.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) => {
2488
- e.stopPropagation();
2489
- deleteEdgeById(m.data?.edgeId);
2490
- }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
2486
+ return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("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
2487
+ ? String(current)
2488
+ : "", onChange: (e) => {
2489
+ const val = e.target.value;
2490
+ const raw = val === "" ? undefined : Number(val);
2491
+ setInput(h, raw);
2492
+ // keep drafts/originals in sync with label for display elsewhere
2493
+ const display = safeToString(typeId, raw);
2494
+ setDrafts((d) => ({ ...d, [h]: display }));
2495
+ setOriginals((o) => ({ ...o, [h]: display }));
2496
+ }, ...commonProps, children: [jsxRuntime.jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] })) : isLinked ? (toElement(typeId, current)) : (jsxRuntime.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) => {
2497
+ if (e.key === "Enter")
2498
+ commit();
2499
+ if (e.key === "Escape")
2500
+ revert();
2501
+ }, ...commonProps }))] }, h));
2502
+ }))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-20 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: outputTypesMap[selectedNodeId]?.[h] ?? "" })] }), jsxRuntime.jsx("div", { className: "flex-1", children: (() => {
2503
+ const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]);
2504
+ return toElement(typeId, value);
2505
+ })() }), (() => {
2506
+ const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
2507
+ if (outIssues.length === 0)
2508
+ return null;
2509
+ const outErr = outIssues.some((m) => m.level === "error");
2510
+ const outTitle = outIssues
2511
+ .map((v) => `${v.code}: ${v.message}`)
2512
+ .join("; ");
2513
+ return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
2514
+ })()] }, h))))] }), selectedNodeValidation.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: selectedNodeValidation.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) => {
2515
+ e.stopPropagation();
2516
+ deleteEdgeById(m.data?.edgeId);
2517
+ }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) })] }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
2491
2518
  }
2492
2519
 
2493
2520
  function NodeHandleItem({ kind, id, type, position, y, isConnectable, className, labelClassName, renderLabel, }) {
@@ -3655,25 +3682,30 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3655
3682
  return overrides.toElement(baseToElement, { registry });
3656
3683
  return baseToElement;
3657
3684
  }, [overrides, baseToElement, registry]);
3658
- return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsxRuntime.jsx(react$1.PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsxRuntime.jsx(react$1.WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsxRuntime.jsx(react$1.WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
3685
+ return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsxRuntime.jsx(react$1.PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsxRuntime.jsx(react$1.WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsxRuntime.jsx(react$1.WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
3659
3686
  ? "Stop engine before switching example"
3660
- : undefined, children: [jsxRuntime.jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
3687
+ : undefined, children: [jsxRuntime.jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
3661
3688
  ? "Stop engine before switching backend"
3662
3689
  : undefined, children: [jsxRuntime.jsx("option", { value: "local", children: "Local" }), jsxRuntime.jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsxRuntime.jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && !!onHttpBaseUrlChange && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && !!onWsUrlChange && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
3663
3690
  const kind = e.target.value || undefined;
3664
3691
  onEngineChange?.(kind);
3665
- }, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-red-700 border-red-600 flex items-center gap-1", onClick: () => runner.dispose(), title: "Stop engine", children: [jsxRuntime.jsx(react$1.StopIcon, { size: 14, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Stop" })] })) : (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-green-700 border-green-600 flex items-center gap-1 disabled:text-gray-400 disabled:border-gray-300", onClick: () => {
3692
+ }, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: () => runner.step(), disabled: !runner.isRunning(), title: "Step", children: jsxRuntime.jsx(react$1.PlayPauseIcon, { size: 24 }) })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: () => runner.flush(), disabled: !runner.isRunning(), title: "Flush", children: jsxRuntime.jsx(react$1.LightningIcon, { size: 24 }) })), runner.isRunning() ? (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-red-700 border-red-600 flex items-center gap-1", onClick: () => runner.dispose(), title: "Stop engine", children: [jsxRuntime.jsx(react$1.StopIcon, { size: 16, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Stop" })] })) : (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-green-700 border-green-600 flex items-center gap-1 disabled:text-gray-400 disabled:border-gray-300", onClick: (evt) => {
3666
3693
  const kind = engine;
3667
3694
  if (!kind)
3668
3695
  return alert("Select an engine first.");
3696
+ if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
3697
+ return;
3669
3698
  try {
3670
- runner.launch(wb.export(), { engine: kind });
3699
+ runner.launch(wb.export(), {
3700
+ engine: kind,
3701
+ invalidate: evt.shiftKey,
3702
+ });
3671
3703
  }
3672
3704
  catch (err) {
3673
3705
  const message = err instanceof Error ? err.message : String(err);
3674
3706
  alert(message);
3675
3707
  }
3676
- }, disabled: !engine, title: engine ? "Start engine" : "Select an engine first", children: [jsxRuntime.jsx(react$1.PlayIcon, { size: 14, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Start" })] })), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: async () => {
3708
+ }, disabled: !engine, title: engine ? "Start engine" : "Select an engine first", children: [jsxRuntime.jsx(react$1.PlayIcon, { size: 16, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Start" })] })), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: runAutoLayout, children: jsxRuntime.jsx(react$1.TreeStructureIcon, { size: 24 }) }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: jsxRuntime.jsx(react$1.CornersOutIcon, { size: 24 }) }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: downloadGraph, children: jsxRuntime.jsx(react$1.DownloadSimpleIcon, { size: 24 }) }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: async () => {
3677
3709
  try {
3678
3710
  const def = wb.export();
3679
3711
  const positions = wb.getPositions();
@@ -3702,7 +3734,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3702
3734
  const message = err instanceof Error ? err.message : String(err);
3703
3735
  alert(message);
3704
3736
  }
3705
- }, children: "Download Snapshot" }), jsxRuntime.jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: "Upload Graph/Snapshot" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement, contextPanel: overrides?.contextPanel })] })] }));
3737
+ }, children: jsxRuntime.jsx(react$1.DownloadIcon, { size: 24 }) }), jsxRuntime.jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: jsxRuntime.jsx(react$1.UploadIcon, { size: 24 }) }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx(react$1.BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx(react$1.ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement, contextPanel: overrides?.contextPanel })] })] }));
3706
3738
  }
3707
3739
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
3708
3740
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());