@bian-womp/spark-workbench 0.2.22 → 0.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/lib/cjs/index.cjs +158 -54
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/adapters/cli/index.d.ts +1 -1
  4. package/lib/cjs/src/adapters/cli/index.d.ts.map +1 -1
  5. package/lib/cjs/src/core/AbstractWorkbench.d.ts +3 -3
  6. package/lib/cjs/src/core/AbstractWorkbench.d.ts.map +1 -1
  7. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +4 -3
  8. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  9. package/lib/cjs/src/core/contracts.d.ts +4 -3
  10. package/lib/cjs/src/core/contracts.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/NodeHandles.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/WorkbenchStudio.d.ts +1 -1
  13. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +2 -1
  15. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  16. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  17. package/lib/cjs/src/misc/layout.d.ts +5 -5
  18. package/lib/cjs/src/misc/layout.d.ts.map +1 -1
  19. package/lib/cjs/src/misc/mapping.d.ts +3 -1
  20. package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
  21. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +2 -2
  22. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  23. package/lib/cjs/src/runtime/IGraphRunner.d.ts +2 -2
  24. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
  25. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +2 -2
  26. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  27. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +2 -2
  28. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  29. package/lib/esm/index.js +158 -54
  30. package/lib/esm/index.js.map +1 -1
  31. package/lib/esm/src/adapters/cli/index.d.ts +1 -1
  32. package/lib/esm/src/adapters/cli/index.d.ts.map +1 -1
  33. package/lib/esm/src/core/AbstractWorkbench.d.ts +3 -3
  34. package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -1
  35. package/lib/esm/src/core/InMemoryWorkbench.d.ts +4 -3
  36. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  37. package/lib/esm/src/core/contracts.d.ts +4 -3
  38. package/lib/esm/src/core/contracts.d.ts.map +1 -1
  39. package/lib/esm/src/misc/NodeHandles.d.ts.map +1 -1
  40. package/lib/esm/src/misc/WorkbenchStudio.d.ts +1 -1
  41. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  42. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +2 -1
  43. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  44. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  45. package/lib/esm/src/misc/layout.d.ts +5 -5
  46. package/lib/esm/src/misc/layout.d.ts.map +1 -1
  47. package/lib/esm/src/misc/mapping.d.ts +3 -1
  48. package/lib/esm/src/misc/mapping.d.ts.map +1 -1
  49. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +2 -2
  50. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  51. package/lib/esm/src/runtime/IGraphRunner.d.ts +2 -2
  52. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
  53. package/lib/esm/src/runtime/LocalGraphRunner.d.ts +2 -2
  54. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  55. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +2 -2
  56. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  57. package/package.json +4 -4
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
  };
@@ -2161,6 +2240,17 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2161
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 }) }))] }));
2162
2241
  }
2163
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
+ }
2164
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", }) {
2165
2255
  const layout = data.handleLayout ?? [];
2166
2256
  const byId = React.useMemo(() => {
@@ -2171,40 +2261,49 @@ function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray
2171
2261
  position: h.position,
2172
2262
  y: h.y,
2173
2263
  type: h.type,
2264
+ missing: h.missing,
2174
2265
  });
2175
2266
  // Back-compat: also store by id-only if not already set
2176
2267
  if (!m.has(h.id))
2177
- 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
+ });
2178
2274
  }
2179
2275
  return m;
2180
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]);
2181
2281
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(data.inputHandles ?? []).map((h) => {
2182
2282
  const placed = byId.get(`target:${h.id}`) ?? byId.get(h.id);
2183
2283
  const position = placed?.position ?? react.Position.Left;
2184
2284
  const y = placed?.y;
2185
2285
  const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ??
2186
2286
  inputClassName;
2187
- 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: {
2188
- top: (y ?? 0) - 8,
2189
- right: "50%",
2190
- whiteSpace: "nowrap",
2191
- overflow: "hidden",
2192
- textOverflow: "ellipsis",
2193
- }, 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));
2194
2294
  }), (data.outputHandles ?? []).map((h) => {
2195
2295
  const placed = byId.get(`source:${h.id}`) ?? byId.get(h.id);
2196
2296
  const position = placed?.position ?? react.Position.Right;
2197
2297
  const y = placed?.y;
2198
2298
  const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ??
2199
2299
  outputClassName;
2200
- 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: {
2201
- top: (y ?? 0) - 8,
2202
- left: "50%",
2203
- textAlign: "right",
2204
- whiteSpace: "nowrap",
2205
- overflow: "hidden",
2206
- textOverflow: "ellipsis",
2207
- }, 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));
2208
2307
  })] }));
2209
2308
  }
2210
2309
 
@@ -2297,7 +2396,7 @@ function DefaultNodeContent({ data, isConnectable, }) {
2297
2396
  const entries = kind === "input" ? inputEntries : outputEntries;
2298
2397
  const entry = entries.find((e) => e.id === handleId);
2299
2398
  if (!entry)
2300
- return handleId;
2399
+ return prettyHandle(handleId);
2301
2400
  const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === handleId);
2302
2401
  const hasAny = vIssues.length > 0;
2303
2402
  const hasErr = vIssues.some((v) => v.level === "error");
@@ -2738,7 +2837,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
2738
2837
  })
2739
2838
  .map((n) => n.id);
2740
2839
  // Detect handle updates (ids/length changes) for targeted debug
2741
- const toIds = (arr) => Array.isArray(arr) ? arr.map((h) => h?.id) : [];
2840
+ const toIds = (arr) => Array.isArray(arr) ? arr.map((h) => (h && typeof h === "object" && "id" in h ? String(h.id) : "")).filter(Boolean) : [];
2742
2841
  const handlesEqual = (a, b) => {
2743
2842
  const aIds = toIds(a);
2744
2843
  const bIds = toIds(b);
@@ -3010,7 +3109,8 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3010
3109
  URL.revokeObjectURL(url);
3011
3110
  }
3012
3111
  catch (err) {
3013
- alert(String(err?.message ?? err));
3112
+ const message = err instanceof Error ? err.message : String(err);
3113
+ alert(message);
3014
3114
  }
3015
3115
  }, [wb, runner]);
3016
3116
  const onUploadPicked = React.useCallback(async (e) => {
@@ -3068,7 +3168,8 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3068
3168
  runAutoLayout();
3069
3169
  }
3070
3170
  catch (err) {
3071
- alert(String(err?.message ?? err));
3171
+ const message = err instanceof Error ? err.message : String(err);
3172
+ alert(message);
3072
3173
  }
3073
3174
  finally {
3074
3175
  // reset input so same file can be picked again
@@ -3318,8 +3419,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3318
3419
  return pre;
3319
3420
  if (typeof value === "object" &&
3320
3421
  value !== null &&
3422
+ "url" in value &&
3321
3423
  typeof value.url === "string") {
3322
- const title = value.title || "";
3424
+ const title = ("title" in value && typeof value.title === "string") ? value.title : "";
3323
3425
  const url = String(value.url || "");
3324
3426
  // value.ts handles data URL formatting
3325
3427
  return title || url.slice(0, 32) + (url.length > 32 ? "…" : "");
@@ -3332,7 +3434,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3332
3434
  const round4 = (n) => Math.round(Number(n) * 10000) / 10000;
3333
3435
  if (typeId === "base.vec3" && Array.isArray(value)) {
3334
3436
  const a = value;
3335
- return [round4(a[0] ?? 0), round4(a[1] ?? 0), round4(a[2] ?? 0)].join(",");
3437
+ return [round4(Number(a[0] ?? 0)), round4(Number(a[1] ?? 0)), round4(Number(a[2] ?? 0))].join(",");
3336
3438
  }
3337
3439
  const stringifyRounded = (v) => {
3338
3440
  try {
@@ -3383,7 +3485,8 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3383
3485
  runner.launch(wb.export(), { engine: kind });
3384
3486
  }
3385
3487
  catch (err) {
3386
- alert(String(err?.message ?? err));
3488
+ const message = err instanceof Error ? err.message : String(err);
3489
+ alert(message);
3387
3490
  }
3388
3491
  }, disabled: !engine, title: engine ? "Start engine" : "Select an engine first", children: [jsxRuntime.jsx(react$1.PlayIcon, { size: 14, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Start" })] })), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: async () => {
3389
3492
  try {
@@ -3411,7 +3514,8 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3411
3514
  URL.revokeObjectURL(url);
3412
3515
  }
3413
3516
  catch (err) {
3414
- alert(String(err?.message ?? err));
3517
+ const message = err instanceof Error ? err.message : String(err);
3518
+ alert(message);
3415
3519
  }
3416
3520
  }, children: "Download Snapshot" }), jsxRuntime.jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: "Upload Graph/Snapshot" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement, contextPanel: overrides?.contextPanel })] })] }));
3417
3521
  }