@bian-womp/spark-workbench 0.2.21 → 0.2.23

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
@@ -534,13 +534,19 @@ class LocalGraphRunner extends AbstractGraphRunner {
534
534
  const def = undefined; // UI will supply def/positions on download for local
535
535
  const inputs = this.getInputs(this.runtime
536
536
  ? {
537
- nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({ nodeId: id, typeId: "" })),
537
+ nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({
538
+ nodeId: id,
539
+ typeId: "",
540
+ })),
538
541
  edges: [],
539
542
  }
540
543
  : { nodes: [], edges: [] });
541
544
  const outputs = this.getOutputs(this.runtime
542
545
  ? {
543
- nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({ nodeId: id, typeId: "" })),
546
+ nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({
547
+ nodeId: id,
548
+ typeId: "",
549
+ })),
544
550
  edges: [],
545
551
  }
546
552
  : { nodes: [], edges: [] });
@@ -1261,18 +1267,44 @@ function layoutNode(args) {
1261
1267
  }
1262
1268
 
1263
1269
  function toReactFlow(def, positions, registry, opts) {
1270
+ const EDGE_STYLE_MISSING = { stroke: "#f59e0b", strokeWidth: 2 }; // amber-500
1264
1271
  const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
1265
1272
  const EDGE_STYLE_RUNNING = { stroke: "#3b82f6" };
1266
- const nodeHandleMap = {};
1267
- // Precompute which inputs are connected per node
1273
+ // Build a map of valid handles per node up-front
1274
+ const validHandleMap = {};
1275
+ for (const n of def.nodes) {
1276
+ const { inputs, outputs } = computeEffectiveHandles(n, registry);
1277
+ const inputOrder = Object.keys(inputs).filter((k) => !sparkGraph.isInputPrivate(inputs, k));
1278
+ const outputOrder = Object.keys(outputs);
1279
+ validHandleMap[n.nodeId] = {
1280
+ inputs: new Set(inputOrder),
1281
+ outputs: new Set(outputOrder),
1282
+ };
1283
+ }
1284
+ // Track which inputs are connected (for UI) and which handles are missing (for layout)
1268
1285
  const connectedInputs = {};
1286
+ const missingInputsByNode = {};
1287
+ const missingOutputsByNode = {};
1269
1288
  for (const e of def.edges) {
1270
- const nid = e.target.nodeId;
1271
- const hid = e.target.handle;
1272
- if (!connectedInputs[nid])
1273
- connectedInputs[nid] = new Set();
1274
- connectedInputs[nid].add(hid);
1275
- }
1289
+ const tgtId = e.target.nodeId;
1290
+ const tgtHandle = e.target.handle;
1291
+ if (!connectedInputs[tgtId])
1292
+ connectedInputs[tgtId] = new Set();
1293
+ connectedInputs[tgtId].add(tgtHandle);
1294
+ const tgtValid = !!validHandleMap[tgtId]?.inputs.has(tgtHandle);
1295
+ if (!tgtValid) {
1296
+ (missingInputsByNode[tgtId] || (missingInputsByNode[tgtId] = new Set())).add(tgtHandle);
1297
+ }
1298
+ const srcId = e.source.nodeId;
1299
+ const srcHandle = e.source.handle;
1300
+ const srcValid = !!validHandleMap[srcId]?.outputs.has(srcHandle);
1301
+ if (!srcValid) {
1302
+ (missingOutputsByNode[srcId] || (missingOutputsByNode[srcId] = new Set())).add(srcHandle);
1303
+ }
1304
+ }
1305
+ // This map is still used later for certain checks; align with valid handles
1306
+ const nodeHandleMap = {};
1307
+ Object.assign(nodeHandleMap, validHandleMap);
1276
1308
  const nodes = def.nodes.map((n) => {
1277
1309
  const { inputs: inputSource, outputs: outputSource } = computeEffectiveHandles(n, registry);
1278
1310
  const overrideSize = opts.getDefaultNodeSize?.(n.typeId);
@@ -1294,11 +1326,61 @@ function toReactFlow(def, positions, registry, opts) {
1294
1326
  inputs: new Set(inputHandles.map((h) => h.id)),
1295
1327
  outputs: new Set(outputHandles.map((h) => h.id)),
1296
1328
  };
1297
- // Shared sizing
1329
+ // Append placeholder entries for any missing handles (below valid ones)
1330
+ const baseLeftCount = geom.inputOrder.length;
1331
+ const baseRightCount = geom.outputOrder.length;
1332
+ const extraInputs = Array.from(missingInputsByNode[n.nodeId] || []);
1333
+ const extraOutputs = Array.from(missingOutputsByNode[n.nodeId] || []);
1334
+ const HEADER = NODE_HEADER_HEIGHT_PX;
1335
+ const ROW = NODE_ROW_HEIGHT_PX;
1336
+ const extraHandleLayoutLeft = extraInputs.map((id, i) => ({
1337
+ id,
1338
+ type: "target",
1339
+ position: react.Position.Left,
1340
+ y: HEADER + (baseLeftCount + i) * ROW + ROW / 2,
1341
+ missing: true,
1342
+ }));
1343
+ const extraHandleLayoutRight = extraOutputs.map((id, i) => ({
1344
+ id,
1345
+ type: "source",
1346
+ position: react.Position.Right,
1347
+ y: HEADER + (baseRightCount + i) * ROW + ROW / 2,
1348
+ missing: true,
1349
+ }));
1350
+ const handleLayout = [
1351
+ ...geom.handleLayout,
1352
+ ...extraHandleLayoutLeft,
1353
+ ...extraHandleLayoutRight,
1354
+ ];
1355
+ // Precompute handle bounds (including missing) so edges can render immediately
1356
+ const missingBoundsLeft = extraInputs.map((id, i) => ({
1357
+ id,
1358
+ type: "target",
1359
+ position: react.Position.Left,
1360
+ x: 0,
1361
+ y: HEADER + (baseLeftCount + i) * ROW,
1362
+ width: 1,
1363
+ height: ROW + 2,
1364
+ }));
1365
+ const missingBoundsRight = extraOutputs.map((id, i) => ({
1366
+ id,
1367
+ type: "source",
1368
+ position: react.Position.Right,
1369
+ x: geom.width - 1,
1370
+ y: HEADER + (baseRightCount + i) * ROW,
1371
+ width: 1,
1372
+ height: ROW + 2,
1373
+ }));
1374
+ const handles = [
1375
+ ...geom.handles,
1376
+ ...missingBoundsLeft,
1377
+ ...missingBoundsRight,
1378
+ ];
1379
+ // Adjust node height to accommodate missing handle rows
1380
+ const baseRows = Math.max(baseLeftCount, baseRightCount);
1381
+ const newRows = Math.max(baseLeftCount + extraInputs.length, baseRightCount + extraOutputs.length);
1298
1382
  const initialWidth = geom.width;
1299
- const initialHeight = geom.height;
1300
- // Precompute handle bounds so edges can render immediately without waiting for measurement
1301
- const handles = geom.handles;
1383
+ const initialHeight = geom.height + Math.max(0, newRows - baseRows) * ROW;
1302
1384
  return {
1303
1385
  id: n.nodeId,
1304
1386
  data: {
@@ -1310,7 +1392,7 @@ function toReactFlow(def, positions, registry, opts) {
1310
1392
  h.id,
1311
1393
  !!connectedInputs[n.nodeId]?.has(h.id),
1312
1394
  ])),
1313
- handleLayout: geom.handleLayout,
1395
+ handleLayout,
1314
1396
  showValues: opts.showValues,
1315
1397
  renderWidth: initialWidth,
1316
1398
  renderHeight: initialHeight,
@@ -1337,24 +1419,21 @@ function toReactFlow(def, positions, registry, opts) {
1337
1419
  height: initialHeight,
1338
1420
  };
1339
1421
  });
1340
- const edges = def.edges
1341
- .filter((e) => {
1342
- const src = nodeHandleMap[e.source.nodeId];
1343
- const dst = nodeHandleMap[e.target.nodeId];
1344
- if (!src || !dst)
1345
- return false;
1346
- return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
1347
- })
1348
- .map((e) => {
1422
+ const edges = def.edges.map((e) => {
1349
1423
  const st = opts.edgeStatus?.[e.id];
1350
1424
  const isRunning = !!st?.activeRuns;
1351
1425
  const hasError = !!st?.lastError;
1352
1426
  const isInvalidEdge = !!opts.edgeValidation?.[e.id];
1353
- const style = hasError || isInvalidEdge
1354
- ? EDGE_STYLE_ERROR
1355
- : isRunning
1356
- ? EDGE_STYLE_RUNNING
1357
- : undefined;
1427
+ const sourceMissing = !validHandleMap[e.source.nodeId]?.outputs.has(e.source.handle);
1428
+ const targetMissing = !validHandleMap[e.target.nodeId]?.inputs.has(e.target.handle);
1429
+ const isMissing = sourceMissing || targetMissing;
1430
+ const style = isMissing
1431
+ ? EDGE_STYLE_MISSING
1432
+ : hasError || isInvalidEdge
1433
+ ? EDGE_STYLE_ERROR
1434
+ : isRunning
1435
+ ? EDGE_STYLE_RUNNING
1436
+ : undefined;
1358
1437
  return {
1359
1438
  id: e.id,
1360
1439
  source: e.source.nodeId,
@@ -1364,7 +1443,7 @@ function toReactFlow(def, positions, registry, opts) {
1364
1443
  selected: opts.selectedEdgeIds
1365
1444
  ? opts.selectedEdgeIds.has(e.id)
1366
1445
  : undefined,
1367
- animated: isRunning,
1446
+ animated: isRunning && !isMissing,
1368
1447
  style,
1369
1448
  label: e.typeId || undefined,
1370
1449
  };
@@ -1420,7 +1499,7 @@ function useWorkbenchContext() {
1420
1499
  return ctx;
1421
1500
  }
1422
1501
 
1423
- function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1502
+ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, children, }) {
1424
1503
  const [nodeStatus, setNodeStatus] = React.useState({});
1425
1504
  const [edgeStatus, setEdgeStatus] = React.useState({});
1426
1505
  const [events, setEvents] = React.useState([]);
@@ -1551,8 +1630,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1551
1630
  if (!node)
1552
1631
  continue;
1553
1632
  // Prefer showValues sizing similar to node rendering
1554
- // Lazy import to avoid circular deps at module top
1555
- const size = estimateNodeSize({ node, registry, showValues: true });
1633
+ // Consider per-type overrides when available via UI
1634
+ const overrideSize = overrides?.getDefaultNodeSize?.(node.typeId) ?? undefined;
1635
+ const size = estimateNodeSize({
1636
+ node,
1637
+ registry,
1638
+ showValues: true,
1639
+ overrides: overrideSize,
1640
+ });
1556
1641
  heights[id] = size.height;
1557
1642
  if (size.width > maxWidth)
1558
1643
  maxWidth = size.width;
@@ -1567,7 +1652,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1567
1652
  curX += maxWidth + H_GAP;
1568
1653
  }
1569
1654
  wb.setPositions(pos);
1570
- }, [wb]);
1655
+ }, [wb, registry, overrides?.getDefaultNodeSize]);
1571
1656
  const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
1572
1657
  const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
1573
1658
  // Subscribe to runner/workbench events
@@ -2155,6 +2240,17 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2155
2240
  }, 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 }) }))] }));
2156
2241
  }
2157
2242
 
2243
+ function NodeHandleItem({ kind, id, type, position, y, isConnectable, className, labelClassName, renderLabel, }) {
2244
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: id, type: type, position: position, isConnectable: isConnectable, className: className, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + (kind === "input" ? " left-2" : " right-2"), style: {
2245
+ top: (y ?? 0) - 8,
2246
+ ...(kind === "input"
2247
+ ? { right: "50%" }
2248
+ : { left: "50%", textAlign: "right" }),
2249
+ whiteSpace: "nowrap",
2250
+ overflow: "hidden",
2251
+ textOverflow: "ellipsis",
2252
+ }, children: renderLabel({ kind, id }) }))] }));
2253
+ }
2158
2254
  function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray-600", outputClassName = "!w-2 !h-2 !bg-gray-600", getClassName, renderLabel, labelClassName = "absolute text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", }) {
2159
2255
  const layout = data.handleLayout ?? [];
2160
2256
  const byId = React.useMemo(() => {
@@ -2165,40 +2261,49 @@ function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray
2165
2261
  position: h.position,
2166
2262
  y: h.y,
2167
2263
  type: h.type,
2264
+ missing: h.missing,
2168
2265
  });
2169
2266
  // Back-compat: also store by id-only if not already set
2170
2267
  if (!m.has(h.id))
2171
- m.set(h.id, { position: h.position, y: h.y, type: h.type });
2268
+ m.set(h.id, {
2269
+ position: h.position,
2270
+ y: h.y,
2271
+ type: h.type,
2272
+ missing: h.missing,
2273
+ });
2172
2274
  }
2173
2275
  return m;
2174
2276
  }, [layout]);
2277
+ const inputIds = React.useMemo(() => new Set((data.inputHandles ?? []).map((h) => h.id)), [data.inputHandles]);
2278
+ const outputIds = React.useMemo(() => new Set((data.outputHandles ?? []).map((h) => h.id)), [data.outputHandles]);
2279
+ const missingInputs = React.useMemo(() => (layout || []).filter((h) => h.type === "target" && (!inputIds.has(h.id) || h.missing)), [layout, inputIds]);
2280
+ const missingOutputs = React.useMemo(() => (layout || []).filter((h) => h.type === "source" && (!outputIds.has(h.id) || h.missing)), [layout, outputIds]);
2175
2281
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(data.inputHandles ?? []).map((h) => {
2176
2282
  const placed = byId.get(`target:${h.id}`) ?? byId.get(h.id);
2177
2283
  const position = placed?.position ?? react.Position.Left;
2178
2284
  const y = placed?.y;
2179
2285
  const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ??
2180
2286
  inputClassName;
2181
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: h.id, type: "target", position: position, isConnectable: isConnectable, className: cls, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + " left-2", style: {
2182
- top: (y ?? 0) - 8,
2183
- right: "50%",
2184
- whiteSpace: "nowrap",
2185
- overflow: "hidden",
2186
- textOverflow: "ellipsis",
2187
- }, children: renderLabel({ kind: "input", id: h.id }) }))] }, h.id));
2287
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "input", id: h.id, type: "target", position: position, y: y, isConnectable: isConnectable, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, h.id));
2288
+ }), missingInputs.map((h) => {
2289
+ const key = `missing-input:${h.id}`;
2290
+ const position = h.position ?? react.Position.Left;
2291
+ const y = h.y;
2292
+ const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500";
2293
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "input", id: h.id, type: "target", position: position, y: y, isConnectable: false, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, key));
2188
2294
  }), (data.outputHandles ?? []).map((h) => {
2189
2295
  const placed = byId.get(`source:${h.id}`) ?? byId.get(h.id);
2190
2296
  const position = placed?.position ?? react.Position.Right;
2191
2297
  const y = placed?.y;
2192
2298
  const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ??
2193
2299
  outputClassName;
2194
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: h.id, type: "source", position: position, isConnectable: isConnectable, className: `${cls} wb-nodrag wb-nowheel`, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + " right-2", style: {
2195
- top: (y ?? 0) - 8,
2196
- left: "50%",
2197
- textAlign: "right",
2198
- whiteSpace: "nowrap",
2199
- overflow: "hidden",
2200
- textOverflow: "ellipsis",
2201
- }, children: renderLabel({ kind: "output", id: h.id }) }))] }, h.id));
2300
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: isConnectable, className: `${cls} wb-nodrag wb-nowheel`, labelClassName: labelClassName, renderLabel: renderLabel }, h.id));
2301
+ }), missingOutputs.map((h) => {
2302
+ const key = `missing-output:${h.id}`;
2303
+ const position = h.position ?? react.Position.Right;
2304
+ const y = h.y;
2305
+ const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500 !rounded-none wb-nodrag wb-nowheel";
2306
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: false, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, key));
2202
2307
  })] }));
2203
2308
  }
2204
2309
 
@@ -2262,6 +2367,18 @@ function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate,
2262
2367
  }
2263
2368
  function DefaultNodeContent({ data, isConnectable, }) {
2264
2369
  const { showValues, inputValues, outputValues, toString } = data;
2370
+ const prettyHandle = React.useCallback((id) => {
2371
+ try {
2372
+ const parts = String(id).split(":");
2373
+ // If there are exactly 3 colons (4 parts), display only the second part
2374
+ if (parts.length === 4)
2375
+ return parts[1] || id;
2376
+ return id;
2377
+ }
2378
+ catch {
2379
+ return id;
2380
+ }
2381
+ }, []);
2265
2382
  const inputEntries = data.inputHandles ?? [];
2266
2383
  const outputEntries = data.outputHandles ?? [];
2267
2384
  const status = data.status ?? { activeRuns: 0 };
@@ -2279,7 +2396,7 @@ function DefaultNodeContent({ data, isConnectable, }) {
2279
2396
  const entries = kind === "input" ? inputEntries : outputEntries;
2280
2397
  const entry = entries.find((e) => e.id === handleId);
2281
2398
  if (!entry)
2282
- return handleId;
2399
+ return prettyHandle(handleId);
2283
2400
  const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === handleId);
2284
2401
  const hasAny = vIssues.length > 0;
2285
2402
  const hasErr = vIssues.some((v) => v.level === "error");
@@ -2297,7 +2414,7 @@ function DefaultNodeContent({ data, isConnectable, }) {
2297
2414
  const txt = toString(resolved.typeId, resolved.value);
2298
2415
  return typeof txt === "string" ? txt : String(txt);
2299
2416
  })();
2300
- return (jsxRuntime.jsxs("span", { className: "flex items-center gap-1 w-full", children: [kind === "output" ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [valueText !== undefined ? (jsxRuntime.jsx("span", { className: "opacity-60 truncate pl-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText })) : (jsxRuntime.jsx("span", { style: { flex: 1, minWidth: 0, maxWidth: "100%" } })), jsxRuntime.jsx("span", { className: "truncate shrink-0", style: valueText !== undefined ? { maxWidth: "40%" } : {}, children: handleId })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "truncate shrink-0", style: valueText !== undefined ? { maxWidth: "40%" } : {}, children: handleId }), valueText !== undefined && (jsxRuntime.jsx("span", { className: "opacity-60 truncate pr-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText }))] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "shrink-0", title: title }))] }));
2417
+ return (jsxRuntime.jsxs("span", { className: "flex items-center gap-1 w-full", children: [kind === "output" ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [valueText !== undefined ? (jsxRuntime.jsx("span", { className: "opacity-60 truncate pl-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText })) : (jsxRuntime.jsx("span", { style: { flex: 1, minWidth: 0, maxWidth: "100%" } })), jsxRuntime.jsx("span", { className: "truncate shrink-0", style: valueText !== undefined ? { maxWidth: "40%" } : {}, children: prettyHandle(handleId) })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "truncate shrink-0", style: valueText !== undefined ? { maxWidth: "40%" } : {}, children: prettyHandle(handleId) }), valueText !== undefined && (jsxRuntime.jsx("span", { className: "opacity-60 truncate pr-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText }))] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "shrink-0", title: title }))] }));
2301
2418
  } })] }));
2302
2419
  }
2303
2420
 
@@ -2887,6 +3004,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2887
3004
  return overrides.getExamples(defaultExamples);
2888
3005
  return defaultExamples;
2889
3006
  }, [overrides, defaultExamples]);
3007
+ const [hydrated, setHydrated] = React.useState(false);
2890
3008
  const lastAutoLaunched = React.useRef(undefined);
2891
3009
  const autoLayoutRan = React.useRef(false);
2892
3010
  const canvasRef = React.useRef(null);
@@ -2910,7 +3028,6 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2910
3028
  runner.setInputs(nodeId, map);
2911
3029
  }
2912
3030
  }
2913
- runAutoLayout();
2914
3031
  };
2915
3032
  onInit({ wb, runner, setInitialGraph });
2916
3033
  }, [onInit, wb, runner, runAutoLayout, registry, setRegistry]);
@@ -3098,7 +3215,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3098
3215
  createRuntime: () => ({
3099
3216
  async onInputsChanged() { },
3100
3217
  }),
3101
- policy: { mode: "push", asyncConcurrency: "switch" },
3218
+ policy: { asyncConcurrency: "switch" },
3102
3219
  };
3103
3220
  r.categories.register(category);
3104
3221
  }
@@ -3167,23 +3284,32 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3167
3284
  }, [engine, runner, wb, backendKind]);
3168
3285
  // When switching to remote backend, auto-hydrate registry from backend
3169
3286
  React.useEffect(() => {
3287
+ let hydrate;
3170
3288
  if (backendKind === "remote-http" && httpBaseUrl) {
3171
- hydrateFromBackend("remote-http", httpBaseUrl);
3289
+ hydrate = hydrateFromBackend("remote-http", httpBaseUrl);
3172
3290
  }
3173
3291
  else if (backendKind === "remote-ws" && wsUrl) {
3174
- hydrateFromBackend("remote-ws", wsUrl);
3292
+ hydrate = hydrateFromBackend("remote-ws", wsUrl);
3175
3293
  }
3176
- }, [backendKind, httpBaseUrl, wsUrl, hydrateFromBackend]);
3294
+ if (hydrate) {
3295
+ hydrate.then(() => {
3296
+ setHydrated(true);
3297
+ });
3298
+ }
3299
+ }, [backendKind, httpBaseUrl, wsUrl, hydrateFromBackend, setHydrated]);
3177
3300
  React.useEffect(() => {
3178
3301
  if (autoLayoutRan.current)
3179
3302
  return;
3303
+ if (backendKind !== "local" && !hydrated)
3304
+ return;
3180
3305
  const cur = wb.export();
3181
- const allMissing = cur.nodes.every((n) => !wb.getPositions()[n.nodeId]);
3306
+ const positions = wb.getPositions();
3307
+ const allMissing = cur.nodes.every((n) => !positions[n.nodeId]);
3182
3308
  if (allMissing) {
3183
3309
  autoLayoutRan.current = true;
3184
3310
  runAutoLayout();
3185
3311
  }
3186
- }, [wb, runAutoLayout]);
3312
+ }, [wb, runAutoLayout, backendKind, hydrated]);
3187
3313
  const baseSetInput = React.useCallback((handle, raw) => {
3188
3314
  if (!selectedNodeId)
3189
3315
  return;
@@ -3409,7 +3535,7 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
3409
3535
  overrides?.registerUI?.(baseRegisterUI, { wb, wbRunner: runner });
3410
3536
  // eslint-disable-next-line react-hooks/exhaustive-deps
3411
3537
  }, [wb, runner, overrides]);
3412
- return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
3538
+ return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
3413
3539
  if (runner.isRunning())
3414
3540
  runner.dispose();
3415
3541
  onBackendKindChange(v);