@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/esm/index.js CHANGED
@@ -4,7 +4,7 @@ import { Position, Handle, useUpdateNodeInternals, useReactFlow, ReactFlowProvid
4
4
  import React, { useCallback, useState, useRef, useEffect, useMemo, createContext, useContext, useImperativeHandle } from 'react';
5
5
  import cx from 'classnames';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
- import { XCircleIcon, WarningCircleIcon, CopyIcon, TrashIcon, XIcon, ArrowClockwiseIcon, PlugsConnectedIcon, ClockClockwiseIcon, WifiHighIcon, WifiSlashIcon, PlayPauseIcon, LightningIcon, StopIcon, PlayIcon, TreeStructureIcon, CornersOutIcon, DownloadSimpleIcon, DownloadIcon, UploadIcon, BugBeetleIcon, ListBulletsIcon } from '@phosphor-icons/react';
7
+ import { XCircleIcon, WarningCircleIcon, CopyIcon, TrashIcon, XIcon, ArrowClockwiseIcon, ClockClockwiseIcon, StopIcon, PlayIcon, PlugsConnectedIcon, WifiHighIcon, WifiSlashIcon, PlayPauseIcon, LightningIcon, TreeStructureIcon, CornersOutIcon, DownloadSimpleIcon, DownloadIcon, UploadIcon, BugBeetleIcon, ListBulletsIcon } from '@phosphor-icons/react';
8
8
  import isEqual from 'lodash/isEqual';
9
9
 
10
10
  class DefaultUIExtensionRegistry {
@@ -332,6 +332,7 @@ class AbstractGraphRunner {
332
332
  this.backend = backend;
333
333
  this.listeners = new Map();
334
334
  this.stagedInputs = {};
335
+ this.runnerId = "";
335
336
  }
336
337
  launch(def, opts) {
337
338
  // Auto-stop if engine is already running
@@ -452,6 +453,8 @@ class AbstractGraphRunner {
452
453
  }
453
454
  }
454
455
 
456
+ // Counter for generating readable runner IDs
457
+ let localRunnerCounter = 0;
455
458
  class LocalGraphRunner extends AbstractGraphRunner {
456
459
  constructor(registry) {
457
460
  super(registry, { kind: "local" });
@@ -470,7 +473,11 @@ class LocalGraphRunner extends AbstractGraphRunner {
470
473
  this.getEnvironment = () => {
471
474
  return this.runtime?.getEnvironment?.();
472
475
  };
473
- this.emit("transport", { state: "local" });
476
+ // Generate readable ID for this runner instance (e.g., local-001, local-002)
477
+ localRunnerCounter++;
478
+ this.runnerId = `local-${String(localRunnerCounter).padStart(3, "0")}`;
479
+ console.info(`[LocalGraphRunner] Created runner with ID: ${this.runnerId}`);
480
+ this.emit("transport", { runnerId: this.runnerId, state: "local" });
474
481
  }
475
482
  build(def) {
476
483
  const builder = new GraphBuilder(this.registry);
@@ -622,10 +629,12 @@ class LocalGraphRunner extends AbstractGraphRunner {
622
629
  dispose() {
623
630
  super.dispose();
624
631
  this.runtime = undefined;
625
- this.emit("transport", { state: "local" });
632
+ this.emit("transport", { runnerId: this.runnerId, state: "local" });
626
633
  }
627
634
  }
628
635
 
636
+ // Counter for generating readable runner IDs
637
+ let remoteRunnerCounter = 0;
629
638
  class RemoteGraphRunner extends AbstractGraphRunner {
630
639
  /**
631
640
  * Fetch full registry description from remote and register it locally.
@@ -764,13 +773,20 @@ class RemoteGraphRunner extends AbstractGraphRunner {
764
773
  setupClientSubscriptions(client) {
765
774
  // Subscribe to transport status changes
766
775
  // Convert RuntimeApiClient.TransportStatus to IGraphRunner.TransportStatus
776
+ // Only emit status if it matches this runner's ID
767
777
  this.transportStatusUnsubscribe = client.onTransportStatus((status) => {
778
+ if (status.runnerId && status.runnerId !== this.runnerId)
779
+ return;
768
780
  // Map remote-unix to undefined since RemoteGraphRunner doesn't support it
769
781
  const mappedKind = status.kind === "remote-unix" ? undefined : status.kind;
770
- this.emit("transport", {
782
+ const transportStatus = {
771
783
  state: status.state,
772
784
  kind: mappedKind,
773
- });
785
+ runnerId: this.runnerId,
786
+ };
787
+ // Track current status
788
+ this.currentTransportStatus = transportStatus;
789
+ this.emit("transport", transportStatus);
774
790
  });
775
791
  }
776
792
  // Ensure remote client
@@ -791,6 +807,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
791
807
  // Create client with custom event handler if provided
792
808
  const client = new RuntimeApiClient(clientConfig, {
793
809
  onCustomEvent: backend.onCustomEvent,
810
+ runnerId: this.runnerId,
794
811
  });
795
812
  // Setup event subscriptions
796
813
  this.setupClientSubscriptions(client);
@@ -823,6 +840,15 @@ class RemoteGraphRunner extends AbstractGraphRunner {
823
840
  this.registryFetching = false;
824
841
  this.MAX_REGISTRY_FETCH_ATTEMPTS = 3;
825
842
  this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
843
+ // Generate readable ID for this runner instance (e.g., remote-001, remote-002)
844
+ remoteRunnerCounter++;
845
+ this.runnerId = `remote-${String(remoteRunnerCounter).padStart(3, "0")}`;
846
+ console.info(`[RemoteGraphRunner] Created runner with ID: ${this.runnerId}`);
847
+ // Initialize transport status as "connecting" - will be updated when connection completes
848
+ this.currentTransportStatus = {
849
+ state: "connecting",
850
+ kind: backend.kind,
851
+ };
826
852
  // Auto-handle registry-changed invalidations from remote
827
853
  // We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
828
854
  this.ensureClient().then(async (client) => {
@@ -986,13 +1012,16 @@ class RemoteGraphRunner extends AbstractGraphRunner {
986
1012
  await this.whenIdle();
987
1013
  // Capture current state
988
1014
  const currentInputs = { ...this.stagedInputs };
989
- // Stop current engine
990
- this.stop();
991
- // Create and launch new engine with the specified kind
1015
+ // For remote runners, we cannot call this.stop() because it sends a Dispose
1016
+ // command that destroys the graphRuntime on the backend. Instead, we rely on
1017
+ // the backend's launch() method to dispose the old engine and create a new one.
1018
+ // Reconfigure engine on the backend (this will dispose old engine and create new one)
992
1019
  const client = await this.ensureClient();
993
1020
  await client.launch(opts);
994
- // Get the remote engine proxy
1021
+ // Get the remote engine proxy (should be the same RemoteEngine instance)
995
1022
  const eng = client.getEngine();
1023
+ // Update local state to reflect new engine kind
1024
+ // Note: The RemoteEngine instance itself doesn't change, but the backend engine does
996
1025
  this.engine = eng;
997
1026
  this.runningKind = opts?.engine ?? "push";
998
1027
  this.emit("status", { running: true, engine: this.runningKind });
@@ -1160,6 +1189,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1160
1189
  if (this.disposed)
1161
1190
  return;
1162
1191
  this.disposed = true;
1192
+ console.info(`[RemoteGraphRunner] Disposing runner with ID: ${this.runnerId}`);
1163
1193
  super.dispose();
1164
1194
  // Clear client promise if any
1165
1195
  this.clientPromise = undefined;
@@ -1179,10 +1209,30 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1179
1209
  console.warn("[RemoteGraphRunner] Error disposing client:", err);
1180
1210
  });
1181
1211
  }
1182
- this.emit("transport", {
1212
+ const disconnectedStatus = {
1183
1213
  state: "disconnected",
1184
1214
  kind: this.backend.kind,
1185
- });
1215
+ runnerId: this.runnerId,
1216
+ };
1217
+ this.currentTransportStatus = disconnectedStatus;
1218
+ this.emit("transport", disconnectedStatus);
1219
+ }
1220
+ /**
1221
+ * Override on() to emit current transport status immediately when a new listener subscribes.
1222
+ * This ensures listeners don't miss the current status when they attach after connection.
1223
+ */
1224
+ on(event, handler) {
1225
+ const unsubscribe = super.on(event, handler);
1226
+ // If subscribing to transport events and we have a current status, emit it immediately
1227
+ if (event === "transport" && this.currentTransportStatus) {
1228
+ // Use setTimeout to ensure this happens after the listener is registered
1229
+ // This prevents issues if the handler modifies state synchronously
1230
+ setTimeout(() => {
1231
+ // Type assertion is safe here because we checked event === "transport"
1232
+ handler(this.currentTransportStatus);
1233
+ }, 0);
1234
+ }
1235
+ return unsubscribe;
1186
1236
  }
1187
1237
  }
1188
1238
 
@@ -2369,15 +2419,27 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2369
2419
  offRunnerTransport();
2370
2420
  };
2371
2421
  }, [runner, wb, setRegistry]);
2422
+ const isRunning = useCallback(() => runner.isRunning(), [runner]);
2423
+ const engineKind = useCallback(() => runner.getRunningEngine(), [runner]);
2424
+ const start = useCallback((engine) => {
2425
+ try {
2426
+ runner.launch(wb.export(), { engine });
2427
+ }
2428
+ catch { }
2429
+ }, [runner, wb]);
2430
+ const stop = useCallback(() => runner.stop(), [runner]);
2431
+ const step = useCallback(() => runner.step(), [runner]);
2432
+ const flush = useCallback(() => runner.flush(), [runner]);
2372
2433
  // Push incremental updates into running engine without full reload
2434
+ const isGraphRunning = isRunning();
2373
2435
  useEffect(() => {
2374
- if (runner.isRunning()) {
2436
+ if (isGraphRunning) {
2375
2437
  try {
2376
2438
  runner.update(def);
2377
2439
  }
2378
2440
  catch { }
2379
2441
  }
2380
- }, [runner, def, graphTick]);
2442
+ }, [runner, isGraphRunning, def, graphTick]);
2381
2443
  const validationByNode = useMemo(() => {
2382
2444
  const inputs = {};
2383
2445
  const outputs = {};
@@ -2441,17 +2503,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2441
2503
  }
2442
2504
  return { errors, issues };
2443
2505
  }, [validation]);
2444
- const isRunning = useCallback(() => runner.isRunning(), [runner]);
2445
- const engineKind = useCallback(() => runner.getRunningEngine(), [runner]);
2446
- const start = useCallback((engine) => {
2447
- try {
2448
- runner.launch(wb.export(), { engine });
2449
- }
2450
- catch { }
2451
- }, [runner, wb]);
2452
- const stop = useCallback(() => runner.stop(), [runner]);
2453
- const step = useCallback(() => runner.step(), [runner]);
2454
- const flush = useCallback(() => runner.flush(), [runner]);
2455
2506
  const value = useMemo(() => ({
2456
2507
  wb,
2457
2508
  runner,
@@ -2657,6 +2708,42 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2657
2708
  outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
2658
2709
  }
2659
2710
  : { inputs: [], outputs: [] };
2711
+ // Render edge status indicator
2712
+ const renderEdgeStatus = useCallback(() => {
2713
+ if (!selectedEdge)
2714
+ return null;
2715
+ const status = edgeStatus?.[selectedEdge.id];
2716
+ if (status?.activeRuns > 0) {
2717
+ return (jsxs("div", { className: "mt-1 text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1", children: [jsxs("div", { className: "font-semibold", children: ["Running (", status.activeRuns, ")"] }), jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "Note: Edge runIds are not available in stats events" })] }));
2718
+ }
2719
+ return null;
2720
+ }, [selectedEdge, edgeStatus]);
2721
+ // Render linked input display value
2722
+ const renderLinkedInputDisplay = useCallback((typeId, current) => {
2723
+ const displayStr = safeToString(typeId, current);
2724
+ const isLong = displayStr.length > 50;
2725
+ const truncated = isLong ? truncateValue(displayStr) : displayStr;
2726
+ return (jsxs("div", { className: "flex items-center gap-1", children: [jsx("span", { className: "truncate", children: truncated }), isLong && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsx(CopyIcon, { size: 14 }) }))] }));
2727
+ }, [safeToString, truncateValue, copyToClipboard]);
2728
+ // Render output validation issues badge
2729
+ const renderOutputValidationBadge = useCallback((handle) => {
2730
+ const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === handle);
2731
+ if (outIssues.length === 0)
2732
+ return null;
2733
+ const outErr = outIssues.some((m) => m.level === "error");
2734
+ const outTitle = outIssues
2735
+ .map((v) => `${v.code}: ${v.message}`)
2736
+ .join("; ");
2737
+ return (jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
2738
+ }, [selectedNodeHandleValidation]);
2739
+ // Render output display value
2740
+ const renderOutputDisplay = useCallback((outputValue, effectiveHandle) => {
2741
+ const { typeId, value } = resolveOutputDisplay(outputValue, effectiveHandle);
2742
+ const displayStr = safeToString(typeId, value);
2743
+ const isLong = displayStr.length > 50;
2744
+ const truncated = isLong ? truncateValue(displayStr) : displayStr;
2745
+ return (jsxs("div", { className: "flex items-center gap-1", children: [jsx("span", { className: "truncate", children: truncated }), isLong && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsx(CopyIcon, { size: 14 }) }))] }));
2746
+ }, [safeToString, truncateValue, copyToClipboard]);
2660
2747
  // Local drafts and originals for commit-on-blur/enter behavior
2661
2748
  const [drafts, setDrafts] = useState({});
2662
2749
  const [originals, setOriginals] = useState({});
@@ -2714,10 +2801,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2714
2801
  return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsx("div", { className: "break-words", children: err.message }), jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsx("div", { className: "break-words", children: err.message })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Registry Error" }), jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2715
2802
  e.stopPropagation();
2716
2803
  deleteEdgeById(m.data?.edgeId);
2717
- }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxs("div", { children: [jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), (() => {
2718
- const status = edgeStatus?.[selectedEdge.id];
2719
- return status?.activeRuns > 0 ? (jsxs("div", { className: "mt-1 text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1", children: [jsxs("div", { className: "font-semibold", children: ["Running (", status.activeRuns, ")"] }), jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "Note: Edge runIds are not available in stats events" })] })) : null;
2720
- })(), jsx("div", { className: "mt-1", children: jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2804
+ }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxs("div", { children: [jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), renderEdgeStatus(), jsx("div", { className: "mt-1", children: jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2721
2805
  e.stopPropagation();
2722
2806
  deleteEdgeById(selectedEdge.id);
2723
2807
  }, title: "Delete this edge", children: "Delete edge" }) }), jsxs("div", { className: "flex items-center gap-2 mt-1", children: [jsxs("label", { className: "w-20 flex flex-col", children: [jsx("span", { children: "Type" }), jsx("span", { className: "text-gray-500 text-[11px]", children: "DataTypeId" })] }), 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) => {
@@ -2796,14 +2880,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2796
2880
  ? `Default: ${placeholder}`
2797
2881
  : "(select)" }), registry.enums
2798
2882
  .get(typeId)
2799
- ?.options.map((opt) => (jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (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: jsx(XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsx("div", { className: "flex items-center gap-1 flex-1", children: jsx("div", { className: "flex-1 min-w-0", children: (() => {
2800
- const displayStr = safeToString(typeId, current);
2801
- const isLong = displayStr.length > 50;
2802
- const truncated = isLong
2803
- ? truncateValue(displayStr)
2804
- : displayStr;
2805
- return (jsxs("div", { className: "flex items-center gap-1", children: [jsx("span", { className: "truncate", children: truncated }), isLong && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsx(CopyIcon, { size: 14 }) }))] }));
2806
- })() }) })) : (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [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
2883
+ ?.options.map((opt) => (jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (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: jsx(XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsx("div", { className: "flex items-center gap-1 flex-1", children: jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [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
2807
2884
  ? `Default: ${placeholder}`
2808
2885
  : undefined, value: displayValue, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
2809
2886
  if (e.key === "Enter")
@@ -2811,24 +2888,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2811
2888
  if (e.key === "Escape")
2812
2889
  revert();
2813
2890
  }, ...commonProps }), hasValue && !isLinked && (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: jsx(XCircleIcon, { size: 16 }) }))] }))] }, h));
2814
- }))] }), jsxs("div", { children: [jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-20 flex flex-col", children: [jsx("span", { children: prettyHandle(h) }), jsx("span", { className: "text-gray-500 text-[11px]", children: outputTypesMap[selectedNodeId]?.[h] ?? "" })] }), jsx("div", { className: "flex-1 min-w-0", children: (() => {
2815
- const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]);
2816
- const displayStr = safeToString(typeId, value);
2817
- const isLong = displayStr.length > 50;
2818
- const truncated = isLong
2819
- ? truncateValue(displayStr)
2820
- : displayStr;
2821
- return (jsxs("div", { className: "flex items-center gap-1", children: [jsx("span", { className: "truncate", children: truncated }), isLong && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsx(CopyIcon, { size: 14 }) }))] }));
2822
- })() }), (() => {
2823
- const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
2824
- if (outIssues.length === 0)
2825
- return null;
2826
- const outErr = outIssues.some((m) => m.level === "error");
2827
- const outTitle = outIssues
2828
- .map((v) => `${v.code}: ${v.message}`)
2829
- .join("; ");
2830
- return (jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
2831
- })()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2891
+ }))] }), jsxs("div", { children: [jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-20 flex flex-col", children: [jsx("span", { children: prettyHandle(h) }), jsx("span", { className: "text-gray-500 text-[11px]", children: outputTypesMap[selectedNodeId]?.[h] ?? "" })] }), jsx("div", { className: "flex-1 min-w-0", children: renderOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]) }), renderOutputValidationBadge(h)] }, h))))] }), selectedNodeValidation.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2832
2892
  e.stopPropagation();
2833
2893
  deleteEdgeById(m.data?.edgeId);
2834
2894
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) })] }), debug && (jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
@@ -3583,6 +3643,44 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3583
3643
  ? computeEffectiveHandles(selectedNode, registry)
3584
3644
  : { inputs: {}, outputs: {}, inputDefaults: {} };
3585
3645
  const [exampleState, setExampleState] = useState(example ?? "");
3646
+ const isGraphRunning = runner.isRunning();
3647
+ const engineKind = runner.getRunningEngine();
3648
+ // Render Start/Stop button based on transport and runner state
3649
+ const renderStartStopButton = useCallback(() => {
3650
+ // Check if transport is connecting/retrying
3651
+ const isConnecting = transportStatus.state === "connecting" ||
3652
+ transportStatus.state === "retrying";
3653
+ // Only allow Start/Stop when transport is connected or local
3654
+ const canControl = transportStatus.state === "connected" ||
3655
+ transportStatus.state === "local";
3656
+ if (isConnecting) {
3657
+ return (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: [jsx(ClockClockwiseIcon, { size: 16, className: "animate-spin" }), jsx("span", { className: "font-medium ml-1", children: "Connecting..." })] }));
3658
+ }
3659
+ if (isGraphRunning) {
3660
+ return (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: [jsx(StopIcon, { size: 16, weight: "fill" }), jsx("span", { className: "font-medium ml-1", children: "Stop" })] }));
3661
+ }
3662
+ return (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) => {
3663
+ const kind = engine;
3664
+ if (!kind)
3665
+ return alert("Select an engine first.");
3666
+ if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
3667
+ return;
3668
+ try {
3669
+ runner.launch(wb.export(), {
3670
+ engine: kind,
3671
+ invalidate: evt.shiftKey,
3672
+ });
3673
+ }
3674
+ catch (err) {
3675
+ const message = err instanceof Error ? err.message : String(err);
3676
+ alert(message);
3677
+ }
3678
+ }, disabled: !engine || !canControl, title: !engine
3679
+ ? "Select an engine first"
3680
+ : !canControl
3681
+ ? "Waiting for connection"
3682
+ : "Start engine", children: [jsx(PlayIcon, { size: 16, weight: "fill" }), jsx("span", { className: "font-medium ml-1", children: "Start" })] }));
3683
+ }, [transportStatus, isGraphRunning, runner, engine, wb]);
3586
3684
  const defaultExamples = useMemo(() => [
3587
3685
  {
3588
3686
  id: "simple",
@@ -3631,7 +3729,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3631
3729
  return backendKind === "local";
3632
3730
  });
3633
3731
  // Expose init callback with setInitialGraph helper
3634
- // Note: This runs whenever runner changes (e.g., when Flow is enabled and backendOptions changes)
3732
+ // Note: This runs whenever runner changes
3635
3733
  useEffect(() => {
3636
3734
  if (!onInit)
3637
3735
  return;
@@ -3807,7 +3905,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3807
3905
  applyExample(example);
3808
3906
  }, [example, wb]);
3809
3907
  useEffect(() => {
3810
- const off = runner.on("transport", (s) => setTransportStatus(s));
3908
+ const off = runner.on("transport", setTransportStatus);
3811
3909
  return () => off();
3812
3910
  }, [runner]);
3813
3911
  // Track registry readiness for remote backends
@@ -3828,7 +3926,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3828
3926
  useEffect(() => {
3829
3927
  if (!engine)
3830
3928
  return;
3831
- if (runner.isRunning())
3929
+ if (isGraphRunning)
3832
3930
  return;
3833
3931
  // Only auto-launch for local backend; require explicit Start for remote
3834
3932
  if (backendKind !== "local")
@@ -3845,7 +3943,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3845
3943
  catch {
3846
3944
  // ignore
3847
3945
  }
3848
- }, [engine, runner, wb, backendKind]);
3946
+ }, [engine, runner, isGraphRunning, wb, backendKind]);
3849
3947
  // Registry is automatically fetched by RemoteGraphRunner when it connects
3850
3948
  // Run auto layout after registry is hydrated (for remote backends)
3851
3949
  useEffect(() => {
@@ -4031,11 +4129,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
4031
4129
  return overrides.toElement(baseToElement, { registry });
4032
4130
  return baseToElement;
4033
4131
  }, [overrides, baseToElement, registry]);
4034
- return (jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsx(PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsx(ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsx(WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsx(WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsx(ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), 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()
4035
- ? "Stop engine before switching example"
4036
- : undefined, children: [jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), 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()
4037
- ? "Stop engine before switching backend"
4038
- : undefined, children: [jsx("option", { value: "local", children: "Local" }), jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && !!onHttpBaseUrlChange && (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 && (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) })), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: async (e) => {
4132
+ return (jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [isGraphRunning ? (jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", engineKind] })) : (jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsx(PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsx(ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsx(WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsx(WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsx(ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), 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: [jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), 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: [jsx("option", { value: "local", children: "Local" }), jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && !!onHttpBaseUrlChange && (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 && (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) })), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: engineKind ?? engine ?? "", onChange: async (e) => {
4039
4133
  const kind = e.target.value || undefined;
4040
4134
  const currentEngine = runner.getRunningEngine();
4041
4135
  // If engine is running and user selected a different engine, switch it
@@ -4062,23 +4156,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
4062
4156
  // Normal change when not running
4063
4157
  onEngineChange?.(kind);
4064
4158
  }
4065
- }, children: [jsx("option", { value: "", children: "Select Engine\u2026" }), jsx("option", { value: "push", children: "Push" }), jsx("option", { value: "batched", children: "Batched" }), jsx("option", { value: "pull", children: "Pull" }), jsx("option", { value: "hybrid", children: "Hybrid" }), jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: () => runner.step(), disabled: !runner.isRunning(), title: "Step", children: jsx(PlayPauseIcon, { size: 24 }) })), runner.getRunningEngine() === "batched" && (jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: () => runner.flush(), disabled: !runner.isRunning(), title: "Flush", children: jsx(LightningIcon, { size: 24 }) })), runner.isRunning() ? (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: [jsx(StopIcon, { size: 16, weight: "fill" }), jsx("span", { className: "font-medium ml-1", children: "Stop" })] })) : (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) => {
4066
- const kind = engine;
4067
- if (!kind)
4068
- return alert("Select an engine first.");
4069
- if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
4070
- return;
4071
- try {
4072
- runner.launch(wb.export(), {
4073
- engine: kind,
4074
- invalidate: evt.shiftKey,
4075
- });
4076
- }
4077
- catch (err) {
4078
- const message = err instanceof Error ? err.message : String(err);
4079
- alert(message);
4080
- }
4081
- }, disabled: !engine, title: engine ? "Start engine" : "Select an engine first", children: [jsx(PlayIcon, { size: 16, weight: "fill" }), jsx("span", { className: "font-medium ml-1", children: "Start" })] })), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: runAutoLayout, children: jsx(TreeStructureIcon, { size: 24 }) }), jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: jsx(CornersOutIcon, { size: 24 }) }), jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: downloadGraph, children: jsx(DownloadSimpleIcon, { size: 24 }) }), jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: async () => {
4159
+ }, children: [jsx("option", { value: "", children: "Select Engine\u2026" }), jsx("option", { value: "push", children: "Push" }), jsx("option", { value: "batched", children: "Batched" }), jsx("option", { value: "pull", children: "Pull" }), jsx("option", { value: "hybrid", children: "Hybrid" }), jsx("option", { value: "step", children: "Step" })] }), engineKind === "step" && (jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.step(), disabled: !isGraphRunning, title: "Step", children: jsx(PlayPauseIcon, { size: 24 }) })), engineKind === "batched" && (jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.flush(), disabled: !isGraphRunning, title: "Flush", children: jsx(LightningIcon, { size: 24 }) })), renderStartStopButton(), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: runAutoLayout, children: jsx(TreeStructureIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: jsx(CornersOutIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: downloadGraph, children: jsx(DownloadSimpleIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: async () => {
4082
4160
  try {
4083
4161
  const def = wb.export();
4084
4162
  const positions = wb.getPositions();
@@ -4107,7 +4185,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
4107
4185
  const message = err instanceof Error ? err.message : String(err);
4108
4186
  alert(message);
4109
4187
  }
4110
- }, children: jsx(DownloadIcon, { size: 24 }) }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: jsx(UploadIcon, { size: 24 }) }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx(BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx(ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, contextPanel: overrides?.contextPanel })] })] }));
4188
+ }, children: jsx(DownloadIcon, { size: 24 }) }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: triggerUpload, children: jsx(UploadIcon, { size: 24 }) }), jsxs("label", { className: "flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx(BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxs("label", { className: "flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx(ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, contextPanel: overrides?.contextPanel })] })] }));
4111
4189
  }
4112
4190
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
4113
4191
  const [registry, setRegistry] = useState(createSimpleGraphRegistry());
@@ -4171,6 +4249,7 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
4171
4249
  }
4172
4250
  };
4173
4251
  }, [runner]);
4252
+ const isGraphRunning = runner.isRunning();
4174
4253
  // Track UI registration version to trigger nodeTypes recomputation
4175
4254
  const [uiVersion, setUiVersion] = useState(0);
4176
4255
  // Allow external UI registration (e.g., node renderers) with access to wb
@@ -4181,11 +4260,12 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
4181
4260
  setUiVersion((v) => v + 1);
4182
4261
  // eslint-disable-next-line react-hooks/exhaustive-deps
4183
4262
  }, [wb, runner, overrides]);
4184
- return (jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
4185
- if (runner.isRunning())
4186
- runner.dispose();
4187
- onBackendKindChange(v);
4188
- }, 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 }) }));
4263
+ const onBackendKindChangeWithDispose = useCallback(() => (v) => {
4264
+ if (isGraphRunning)
4265
+ runner.dispose();
4266
+ onBackendKindChange(v);
4267
+ }, [isGraphRunning]);
4268
+ return (jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: 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 }) }));
4189
4269
  }
4190
4270
 
4191
4271
  export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, computeEffectiveHandles, countVisibleHandles, estimateNodeSize, formatDataUrlAsLabel, formatDeclaredTypeSignature, getHandleClassName, getNodeBorderClassNames, layoutNode, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };