@bian-womp/spark-workbench 0.2.48 → 0.2.49

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
@@ -334,6 +334,7 @@ class AbstractGraphRunner {
334
334
  this.backend = backend;
335
335
  this.listeners = new Map();
336
336
  this.stagedInputs = {};
337
+ this.runnerId = "";
337
338
  }
338
339
  launch(def, opts) {
339
340
  // Auto-stop if engine is already running
@@ -454,6 +455,8 @@ class AbstractGraphRunner {
454
455
  }
455
456
  }
456
457
 
458
+ // Counter for generating readable runner IDs
459
+ let localRunnerCounter = 0;
457
460
  class LocalGraphRunner extends AbstractGraphRunner {
458
461
  constructor(registry) {
459
462
  super(registry, { kind: "local" });
@@ -472,7 +475,11 @@ class LocalGraphRunner extends AbstractGraphRunner {
472
475
  this.getEnvironment = () => {
473
476
  return this.runtime?.getEnvironment?.();
474
477
  };
475
- this.emit("transport", { state: "local" });
478
+ // Generate readable ID for this runner instance (e.g., local-001, local-002)
479
+ localRunnerCounter++;
480
+ this.runnerId = `local-${String(localRunnerCounter).padStart(3, "0")}`;
481
+ console.info(`[LocalGraphRunner] Created runner with ID: ${this.runnerId}`);
482
+ this.emit("transport", { runnerId: this.runnerId, state: "local" });
476
483
  }
477
484
  build(def) {
478
485
  const builder = new sparkGraph.GraphBuilder(this.registry);
@@ -624,10 +631,12 @@ class LocalGraphRunner extends AbstractGraphRunner {
624
631
  dispose() {
625
632
  super.dispose();
626
633
  this.runtime = undefined;
627
- this.emit("transport", { state: "local" });
634
+ this.emit("transport", { runnerId: this.runnerId, state: "local" });
628
635
  }
629
636
  }
630
637
 
638
+ // Counter for generating readable runner IDs
639
+ let remoteRunnerCounter = 0;
631
640
  class RemoteGraphRunner extends AbstractGraphRunner {
632
641
  /**
633
642
  * Fetch full registry description from remote and register it locally.
@@ -766,13 +775,20 @@ class RemoteGraphRunner extends AbstractGraphRunner {
766
775
  setupClientSubscriptions(client) {
767
776
  // Subscribe to transport status changes
768
777
  // Convert RuntimeApiClient.TransportStatus to IGraphRunner.TransportStatus
778
+ // Only emit status if it matches this runner's ID
769
779
  this.transportStatusUnsubscribe = client.onTransportStatus((status) => {
780
+ if (status.runnerId && status.runnerId !== this.runnerId)
781
+ return;
770
782
  // Map remote-unix to undefined since RemoteGraphRunner doesn't support it
771
783
  const mappedKind = status.kind === "remote-unix" ? undefined : status.kind;
772
- this.emit("transport", {
784
+ const transportStatus = {
773
785
  state: status.state,
774
786
  kind: mappedKind,
775
- });
787
+ runnerId: this.runnerId,
788
+ };
789
+ // Track current status
790
+ this.currentTransportStatus = transportStatus;
791
+ this.emit("transport", transportStatus);
776
792
  });
777
793
  }
778
794
  // Ensure remote client
@@ -793,6 +809,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
793
809
  // Create client with custom event handler if provided
794
810
  const client = new sparkRemote.RuntimeApiClient(clientConfig, {
795
811
  onCustomEvent: backend.onCustomEvent,
812
+ runnerId: this.runnerId,
796
813
  });
797
814
  // Setup event subscriptions
798
815
  this.setupClientSubscriptions(client);
@@ -825,6 +842,15 @@ class RemoteGraphRunner extends AbstractGraphRunner {
825
842
  this.registryFetching = false;
826
843
  this.MAX_REGISTRY_FETCH_ATTEMPTS = 3;
827
844
  this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
845
+ // Generate readable ID for this runner instance (e.g., remote-001, remote-002)
846
+ remoteRunnerCounter++;
847
+ this.runnerId = `remote-${String(remoteRunnerCounter).padStart(3, "0")}`;
848
+ console.info(`[RemoteGraphRunner] Created runner with ID: ${this.runnerId}`);
849
+ // Initialize transport status as "connecting" - will be updated when connection completes
850
+ this.currentTransportStatus = {
851
+ state: "connecting",
852
+ kind: backend.kind,
853
+ };
828
854
  // Auto-handle registry-changed invalidations from remote
829
855
  // We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
830
856
  this.ensureClient().then(async (client) => {
@@ -988,13 +1014,16 @@ class RemoteGraphRunner extends AbstractGraphRunner {
988
1014
  await this.whenIdle();
989
1015
  // Capture current state
990
1016
  const currentInputs = { ...this.stagedInputs };
991
- // Stop current engine
992
- this.stop();
993
- // Create and launch new engine with the specified kind
1017
+ // For remote runners, we cannot call this.stop() because it sends a Dispose
1018
+ // command that destroys the graphRuntime on the backend. Instead, we rely on
1019
+ // the backend's launch() method to dispose the old engine and create a new one.
1020
+ // Reconfigure engine on the backend (this will dispose old engine and create new one)
994
1021
  const client = await this.ensureClient();
995
1022
  await client.launch(opts);
996
- // Get the remote engine proxy
1023
+ // Get the remote engine proxy (should be the same RemoteEngine instance)
997
1024
  const eng = client.getEngine();
1025
+ // Update local state to reflect new engine kind
1026
+ // Note: The RemoteEngine instance itself doesn't change, but the backend engine does
998
1027
  this.engine = eng;
999
1028
  this.runningKind = opts?.engine ?? "push";
1000
1029
  this.emit("status", { running: true, engine: this.runningKind });
@@ -1162,6 +1191,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1162
1191
  if (this.disposed)
1163
1192
  return;
1164
1193
  this.disposed = true;
1194
+ console.info(`[RemoteGraphRunner] Disposing runner with ID: ${this.runnerId}`);
1165
1195
  super.dispose();
1166
1196
  // Clear client promise if any
1167
1197
  this.clientPromise = undefined;
@@ -1181,10 +1211,30 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1181
1211
  console.warn("[RemoteGraphRunner] Error disposing client:", err);
1182
1212
  });
1183
1213
  }
1184
- this.emit("transport", {
1214
+ const disconnectedStatus = {
1185
1215
  state: "disconnected",
1186
1216
  kind: this.backend.kind,
1187
- });
1217
+ runnerId: this.runnerId,
1218
+ };
1219
+ this.currentTransportStatus = disconnectedStatus;
1220
+ this.emit("transport", disconnectedStatus);
1221
+ }
1222
+ /**
1223
+ * Override on() to emit current transport status immediately when a new listener subscribes.
1224
+ * This ensures listeners don't miss the current status when they attach after connection.
1225
+ */
1226
+ on(event, handler) {
1227
+ const unsubscribe = super.on(event, handler);
1228
+ // If subscribing to transport events and we have a current status, emit it immediately
1229
+ if (event === "transport" && this.currentTransportStatus) {
1230
+ // Use setTimeout to ensure this happens after the listener is registered
1231
+ // This prevents issues if the handler modifies state synchronously
1232
+ setTimeout(() => {
1233
+ // Type assertion is safe here because we checked event === "transport"
1234
+ handler(this.currentTransportStatus);
1235
+ }, 0);
1236
+ }
1237
+ return unsubscribe;
1188
1238
  }
1189
1239
  }
1190
1240
 
@@ -2371,15 +2421,27 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2371
2421
  offRunnerTransport();
2372
2422
  };
2373
2423
  }, [runner, wb, setRegistry]);
2424
+ const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
2425
+ const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
2426
+ const start = React.useCallback((engine) => {
2427
+ try {
2428
+ runner.launch(wb.export(), { engine });
2429
+ }
2430
+ catch { }
2431
+ }, [runner, wb]);
2432
+ const stop = React.useCallback(() => runner.stop(), [runner]);
2433
+ const step = React.useCallback(() => runner.step(), [runner]);
2434
+ const flush = React.useCallback(() => runner.flush(), [runner]);
2374
2435
  // Push incremental updates into running engine without full reload
2436
+ const isGraphRunning = isRunning();
2375
2437
  React.useEffect(() => {
2376
- if (runner.isRunning()) {
2438
+ if (isGraphRunning) {
2377
2439
  try {
2378
2440
  runner.update(def);
2379
2441
  }
2380
2442
  catch { }
2381
2443
  }
2382
- }, [runner, def, graphTick]);
2444
+ }, [runner, isGraphRunning, def, graphTick]);
2383
2445
  const validationByNode = React.useMemo(() => {
2384
2446
  const inputs = {};
2385
2447
  const outputs = {};
@@ -2443,17 +2505,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2443
2505
  }
2444
2506
  return { errors, issues };
2445
2507
  }, [validation]);
2446
- const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
2447
- const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
2448
- const start = React.useCallback((engine) => {
2449
- try {
2450
- runner.launch(wb.export(), { engine });
2451
- }
2452
- catch { }
2453
- }, [runner, wb]);
2454
- const stop = React.useCallback(() => runner.stop(), [runner]);
2455
- const step = React.useCallback(() => runner.step(), [runner]);
2456
- const flush = React.useCallback(() => runner.flush(), [runner]);
2457
2508
  const value = React.useMemo(() => ({
2458
2509
  wb,
2459
2510
  runner,
@@ -2659,6 +2710,42 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2659
2710
  outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
2660
2711
  }
2661
2712
  : { inputs: [], outputs: [] };
2713
+ // Render edge status indicator
2714
+ const renderEdgeStatus = React.useCallback(() => {
2715
+ if (!selectedEdge)
2716
+ return null;
2717
+ const status = edgeStatus?.[selectedEdge.id];
2718
+ if (status?.activeRuns > 0) {
2719
+ return (jsxRuntime.jsxs("div", { className: "mt-1 text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "font-semibold", children: ["Running (", status.activeRuns, ")"] }), jsxRuntime.jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "Note: Edge runIds are not available in stats events" })] }));
2720
+ }
2721
+ return null;
2722
+ }, [selectedEdge, edgeStatus]);
2723
+ // Render linked input display value
2724
+ const renderLinkedInputDisplay = React.useCallback((typeId, current) => {
2725
+ const displayStr = safeToString(typeId, current);
2726
+ const isLong = displayStr.length > 50;
2727
+ const truncated = isLong ? truncateValue(displayStr) : displayStr;
2728
+ return (jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("span", { className: "truncate", children: truncated }), isLong && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }))] }));
2729
+ }, [safeToString, truncateValue, copyToClipboard]);
2730
+ // Render output validation issues badge
2731
+ const renderOutputValidationBadge = React.useCallback((handle) => {
2732
+ const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === handle);
2733
+ if (outIssues.length === 0)
2734
+ return null;
2735
+ const outErr = outIssues.some((m) => m.level === "error");
2736
+ const outTitle = outIssues
2737
+ .map((v) => `${v.code}: ${v.message}`)
2738
+ .join("; ");
2739
+ return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
2740
+ }, [selectedNodeHandleValidation]);
2741
+ // Render output display value
2742
+ const renderOutputDisplay = React.useCallback((outputValue, effectiveHandle) => {
2743
+ const { typeId, value } = resolveOutputDisplay(outputValue, effectiveHandle);
2744
+ const displayStr = safeToString(typeId, value);
2745
+ const isLong = displayStr.length > 50;
2746
+ const truncated = isLong ? truncateValue(displayStr) : displayStr;
2747
+ return (jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("span", { className: "truncate", children: truncated }), isLong && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }))] }));
2748
+ }, [safeToString, truncateValue, copyToClipboard]);
2662
2749
  // Local drafts and originals for commit-on-blur/enter behavior
2663
2750
  const [drafts, setDrafts] = React.useState({});
2664
2751
  const [originals, setOriginals] = React.useState({});
@@ -2716,10 +2803,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2716
2803
  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) => {
2717
2804
  e.stopPropagation();
2718
2805
  deleteEdgeById(m.data?.edgeId);
2719
- }, 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] }), (() => {
2720
- const status = edgeStatus?.[selectedEdge.id];
2721
- return status?.activeRuns > 0 ? (jsxRuntime.jsxs("div", { className: "mt-1 text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "font-semibold", children: ["Running (", status.activeRuns, ")"] }), jsxRuntime.jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "Note: Edge runIds are not available in stats events" })] })) : null;
2722
- })(), 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) => {
2806
+ }, 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] }), renderEdgeStatus(), 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) => {
2723
2807
  e.stopPropagation();
2724
2808
  deleteEdgeById(selectedEdge.id);
2725
2809
  }, 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) => {
@@ -2798,14 +2882,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2798
2882
  ? `Default: ${placeholder}`
2799
2883
  : "(select)" }), registry.enums
2800
2884
  .get(typeId)
2801
- ?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: (() => {
2802
- const displayStr = safeToString(typeId, current);
2803
- const isLong = displayStr.length > 50;
2804
- const truncated = isLong
2805
- ? truncateValue(displayStr)
2806
- : displayStr;
2807
- return (jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("span", { className: "truncate", children: truncated }), isLong && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }))] }));
2808
- })() }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", placeholder: placeholder
2885
+ ?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", placeholder: placeholder
2809
2886
  ? `Default: ${placeholder}`
2810
2887
  : undefined, value: displayValue, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
2811
2888
  if (e.key === "Enter")
@@ -2813,24 +2890,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2813
2890
  if (e.key === "Escape")
2814
2891
  revert();
2815
2892
  }, ...commonProps }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] }))] }, h));
2816
- }))] }), 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 min-w-0", children: (() => {
2817
- const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]);
2818
- const displayStr = safeToString(typeId, value);
2819
- const isLong = displayStr.length > 50;
2820
- const truncated = isLong
2821
- ? truncateValue(displayStr)
2822
- : displayStr;
2823
- return (jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("span", { className: "truncate", children: truncated }), isLong && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }))] }));
2824
- })() }), (() => {
2825
- const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
2826
- if (outIssues.length === 0)
2827
- return null;
2828
- const outErr = outIssues.some((m) => m.level === "error");
2829
- const outTitle = outIssues
2830
- .map((v) => `${v.code}: ${v.message}`)
2831
- .join("; ");
2832
- return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
2833
- })()] }, 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) => {
2893
+ }))] }), 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 min-w-0", children: renderOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]) }), renderOutputValidationBadge(h)] }, 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) => {
2834
2894
  e.stopPropagation();
2835
2895
  deleteEdgeById(m.data?.edgeId);
2836
2896
  }, 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 }) }))] }));
@@ -3585,6 +3645,44 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3585
3645
  ? computeEffectiveHandles(selectedNode, registry)
3586
3646
  : { inputs: {}, outputs: {}, inputDefaults: {} };
3587
3647
  const [exampleState, setExampleState] = React.useState(example ?? "");
3648
+ const isGraphRunning = runner.isRunning();
3649
+ const engineKind = runner.getRunningEngine();
3650
+ // Render Start/Stop button based on transport and runner state
3651
+ const renderStartStopButton = React.useCallback(() => {
3652
+ // Check if transport is connecting/retrying
3653
+ const isConnecting = transportStatus.state === "connecting" ||
3654
+ transportStatus.state === "retrying";
3655
+ // Only allow Start/Stop when transport is connected or local
3656
+ const canControl = transportStatus.state === "connected" ||
3657
+ transportStatus.state === "local";
3658
+ if (isConnecting) {
3659
+ return (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-gray-500 border-gray-400 flex items-center gap-1 disabled:opacity-50", disabled: true, title: "Connecting to backend...", children: [jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 16, className: "animate-spin" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Connecting..." })] }));
3660
+ }
3661
+ if (isGraphRunning) {
3662
+ return (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-red-700 border-red-600 flex items-center gap-1 disabled:opacity-50 disabled:text-gray-400 disabled:border-gray-300", onClick: () => runner.stop(), disabled: !canControl, title: canControl ? "Stop engine" : "Waiting for connection", children: [jsxRuntime.jsx(react$1.StopIcon, { size: 16, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Stop" })] }));
3663
+ }
3664
+ return (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 disabled:opacity-50", onClick: (evt) => {
3665
+ const kind = engine;
3666
+ if (!kind)
3667
+ return alert("Select an engine first.");
3668
+ if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
3669
+ return;
3670
+ try {
3671
+ runner.launch(wb.export(), {
3672
+ engine: kind,
3673
+ invalidate: evt.shiftKey,
3674
+ });
3675
+ }
3676
+ catch (err) {
3677
+ const message = err instanceof Error ? err.message : String(err);
3678
+ alert(message);
3679
+ }
3680
+ }, disabled: !engine || !canControl, title: !engine
3681
+ ? "Select an engine first"
3682
+ : !canControl
3683
+ ? "Waiting for connection"
3684
+ : "Start engine", children: [jsxRuntime.jsx(react$1.PlayIcon, { size: 16, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Start" })] }));
3685
+ }, [transportStatus, isGraphRunning, runner, engine, wb]);
3588
3686
  const defaultExamples = React.useMemo(() => [
3589
3687
  {
3590
3688
  id: "simple",
@@ -3633,7 +3731,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3633
3731
  return backendKind === "local";
3634
3732
  });
3635
3733
  // Expose init callback with setInitialGraph helper
3636
- // Note: This runs whenever runner changes (e.g., when Flow is enabled and backendOptions changes)
3734
+ // Note: This runs whenever runner changes
3637
3735
  React.useEffect(() => {
3638
3736
  if (!onInit)
3639
3737
  return;
@@ -3809,7 +3907,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3809
3907
  applyExample(example);
3810
3908
  }, [example, wb]);
3811
3909
  React.useEffect(() => {
3812
- const off = runner.on("transport", (s) => setTransportStatus(s));
3910
+ const off = runner.on("transport", setTransportStatus);
3813
3911
  return () => off();
3814
3912
  }, [runner]);
3815
3913
  // Track registry readiness for remote backends
@@ -3830,7 +3928,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3830
3928
  React.useEffect(() => {
3831
3929
  if (!engine)
3832
3930
  return;
3833
- if (runner.isRunning())
3931
+ if (isGraphRunning)
3834
3932
  return;
3835
3933
  // Only auto-launch for local backend; require explicit Start for remote
3836
3934
  if (backendKind !== "local")
@@ -3847,7 +3945,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3847
3945
  catch {
3848
3946
  // ignore
3849
3947
  }
3850
- }, [engine, runner, wb, backendKind]);
3948
+ }, [engine, runner, isGraphRunning, wb, backendKind]);
3851
3949
  // Registry is automatically fetched by RemoteGraphRunner when it connects
3852
3950
  // Run auto layout after registry is hydrated (for remote backends)
3853
3951
  React.useEffect(() => {
@@ -4033,11 +4131,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
4033
4131
  return overrides.toElement(baseToElement, { registry });
4034
4132
  return baseToElement;
4035
4133
  }, [overrides, baseToElement, registry]);
4036
- 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()
4037
- ? "Stop engine before switching example"
4038
- : 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()
4039
- ? "Stop engine before switching backend"
4040
- : 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: async (e) => {
4134
+ 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: [isGraphRunning ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", engineKind] })) : (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: isGraphRunning, title: isGraphRunning ? "Stop engine before switching example" : 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: isGraphRunning, title: isGraphRunning ? "Stop engine before switching backend" : 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: "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: "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: engineKind ?? engine ?? "", onChange: async (e) => {
4041
4135
  const kind = e.target.value || undefined;
4042
4136
  const currentEngine = runner.getRunningEngine();
4043
4137
  // If engine is running and user selected a different engine, switch it
@@ -4064,23 +4158,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
4064
4158
  // Normal change when not running
4065
4159
  onEngineChange?.(kind);
4066
4160
  }
4067
- }, 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.stop(), 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) => {
4068
- const kind = engine;
4069
- if (!kind)
4070
- return alert("Select an engine first.");
4071
- if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
4072
- return;
4073
- try {
4074
- runner.launch(wb.export(), {
4075
- engine: kind,
4076
- invalidate: evt.shiftKey,
4077
- });
4078
- }
4079
- catch (err) {
4080
- const message = err instanceof Error ? err.message : String(err);
4081
- alert(message);
4082
- }
4083
- }, 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 () => {
4161
+ }, 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" })] }), engineKind === "step" && (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.step(), disabled: !isGraphRunning, title: "Step", children: jsxRuntime.jsx(react$1.PlayPauseIcon, { size: 24 }) })), engineKind === "batched" && (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.flush(), disabled: !isGraphRunning, title: "Flush", children: jsxRuntime.jsx(react$1.LightningIcon, { size: 24 }) })), renderStartStopButton(), 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: "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: "border border-gray-300 rounded p-1", onClick: downloadGraph, children: jsxRuntime.jsx(react$1.DownloadSimpleIcon, { size: 24 }) }), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: async () => {
4084
4162
  try {
4085
4163
  const def = wb.export();
4086
4164
  const positions = wb.getPositions();
@@ -4109,7 +4187,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
4109
4187
  const message = err instanceof Error ? err.message : String(err);
4110
4188
  alert(message);
4111
4189
  }
4112
- }, 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, contextPanel: overrides?.contextPanel })] })] }));
4190
+ }, 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: "border border-gray-300 rounded p-1", onClick: triggerUpload, children: jsxRuntime.jsx(react$1.UploadIcon, { size: 24 }) }), jsxRuntime.jsxs("label", { className: "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: "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, contextPanel: overrides?.contextPanel })] })] }));
4113
4191
  }
4114
4192
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
4115
4193
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
@@ -4173,6 +4251,7 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
4173
4251
  }
4174
4252
  };
4175
4253
  }, [runner]);
4254
+ const isGraphRunning = runner.isRunning();
4176
4255
  // Track UI registration version to trigger nodeTypes recomputation
4177
4256
  const [uiVersion, setUiVersion] = React.useState(0);
4178
4257
  // Allow external UI registration (e.g., node renderers) with access to wb
@@ -4183,11 +4262,12 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
4183
4262
  setUiVersion((v) => v + 1);
4184
4263
  // eslint-disable-next-line react-hooks/exhaustive-deps
4185
4264
  }, [wb, runner, overrides]);
4186
- return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
4187
- if (runner.isRunning())
4188
- runner.dispose();
4189
- onBackendKindChange(v);
4190
- }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, backendOptions: backendOptions, overrides: overrides, onInit: onInit, onChange: onChange }) }));
4265
+ const onBackendKindChangeWithDispose = React.useCallback(() => (v) => {
4266
+ if (isGraphRunning)
4267
+ runner.dispose();
4268
+ onBackendKindChange(v);
4269
+ }, [isGraphRunning]);
4270
+ return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: onBackendKindChangeWithDispose, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, backendOptions: backendOptions, overrides: overrides, onInit: onInit, onChange: onChange }) }));
4191
4271
  }
4192
4272
 
4193
4273
  exports.AbstractWorkbench = AbstractWorkbench;