@bian-womp/spark-workbench 0.1.9 → 0.1.10

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 (31) hide show
  1. package/lib/cjs/index.cjs +1136 -1109
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/index.d.ts +7 -2
  4. package/lib/cjs/src/index.d.ts.map +1 -1
  5. package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
  6. package/lib/cjs/src/misc/Inspector.d.ts +3 -2
  7. package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
  8. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts +3 -2
  9. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  10. package/lib/cjs/src/misc/WorkbenchStudio.d.ts +42 -0
  11. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -0
  12. package/lib/cjs/src/misc/mapping.d.ts +2 -1
  13. package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
  14. package/lib/esm/index.js +1124 -1109
  15. package/lib/esm/index.js.map +1 -1
  16. package/lib/esm/src/index.d.ts +7 -2
  17. package/lib/esm/src/index.d.ts.map +1 -1
  18. package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
  19. package/lib/esm/src/misc/Inspector.d.ts +3 -2
  20. package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
  21. package/lib/esm/src/misc/WorkbenchCanvas.d.ts +3 -2
  22. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  23. package/lib/esm/src/misc/WorkbenchStudio.d.ts +42 -0
  24. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -0
  25. package/lib/esm/src/misc/mapping.d.ts +2 -1
  26. package/lib/esm/src/misc/mapping.d.ts.map +1 -1
  27. package/package.json +4 -4
  28. package/lib/cjs/src/examples/reactflow/WorkbenchStudio.d.ts +0 -21
  29. package/lib/cjs/src/examples/reactflow/WorkbenchStudio.d.ts.map +0 -1
  30. package/lib/esm/src/examples/reactflow/WorkbenchStudio.d.ts +0 -21
  31. package/lib/esm/src/examples/reactflow/WorkbenchStudio.d.ts.map +0 -1
package/lib/cjs/index.cjs CHANGED
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  var sparkGraph = require('@bian-womp/spark-graph');
4
- var jsxRuntime = require('react/jsx-runtime');
5
- var React = require('react');
6
4
  var sparkRemote = require('@bian-womp/spark-remote');
5
+ var React = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+ var react = require('@phosphor-icons/react');
7
8
  var ReactFlow = require('reactflow');
8
9
  var cx = require('classnames');
9
- var react = require('@phosphor-icons/react');
10
10
 
11
11
  class DefaultUIExtensionRegistry {
12
12
  constructor() {
@@ -350,1144 +350,1145 @@ class ReactFlowWorkbench {
350
350
  }
351
351
  }
352
352
 
353
- const WorkbenchContext = React.createContext(null);
354
- function useWorkbenchContext() {
355
- const ctx = React.useContext(WorkbenchContext);
356
- if (!ctx)
357
- throw new Error("useWorkbenchContext must be used within WorkbenchProvider");
358
- return ctx;
359
- }
360
-
361
- function useWorkbenchBridge(wb) {
362
- const onConnect = React.useCallback((params) => {
363
- if (!params.source || !params.target)
364
- return;
365
- if (!params.sourceHandle || !params.targetHandle)
353
+ class GraphRunner {
354
+ constructor(registry, backend) {
355
+ this.registry = registry;
356
+ this.listeners = new Map();
357
+ this.stagedInputs = {};
358
+ this.backend = { kind: "local" };
359
+ if (backend)
360
+ this.backend = backend;
361
+ }
362
+ build(def) {
363
+ if (this.backend.kind === "local") {
364
+ const builder = new sparkGraph.GraphBuilder(this.registry);
365
+ this.runtime = builder.build(def);
366
366
  return;
367
- wb.connect({
368
- source: { nodeId: params.source, handle: params.sourceHandle },
369
- target: { nodeId: params.target, handle: params.targetHandle },
370
- });
371
- }, [wb]);
372
- const onNodesChange = React.useCallback((changes) => {
373
- changes.forEach((c) => {
374
- if (c.type === "position" && c.position)
375
- wb.setPosition(c.id, c.position);
376
- if (c.type === "remove")
377
- wb.removeNode(c.id);
378
- if (c.type === "select")
379
- wb.toggleNodeSelection(c.id);
380
- });
381
- }, [wb]);
382
- const onEdgesDelete = React.useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
383
- const onEdgesChange = React.useCallback((changes) => {
384
- changes.forEach((c) => {
385
- if (c.type === "remove")
386
- wb.disconnect(c.id);
387
- else if (c.type === "select")
388
- wb.toggleEdgeSelection(c.id);
389
- });
390
- }, [wb]);
391
- const onNodesDelete = React.useCallback((nodes) => {
392
- for (const n of nodes)
393
- wb.removeNode(n.id);
394
- }, [wb]);
395
- const onSelectionChange = React.useCallback((sel) => {
396
- const next = {
397
- nodes: sel.nodes.map((n) => n.id),
398
- edges: sel.edges.map((e) => e.id),
399
- };
400
- const cur = wb.getSelection();
401
- const sameLen = cur.nodes.length === next.nodes.length &&
402
- cur.edges.length === next.edges.length;
403
- const same = sameLen &&
404
- cur.nodes.every((id, i) => id === next.nodes[i]) &&
405
- cur.edges.every((id, i) => id === next.edges[i]);
406
- if (!same)
407
- wb.setSelection(next);
408
- }, [wb]);
409
- return {
410
- onConnect,
411
- onNodesChange,
412
- onEdgesChange,
413
- onEdgesDelete,
414
- onNodesDelete,
415
- onSelectionChange,
416
- };
417
- }
418
- function useWorkbenchGraphTick(wb) {
419
- const [tick, setTick] = React.useState(0);
420
- React.useEffect(() => {
421
- const bump = () => setTick((t) => t + 1);
422
- const off = wb.on("graphChanged", bump);
423
- return () => off();
424
- }, [wb]);
425
- return tick;
426
- }
427
- function useWorkbenchGraphUiTick(wb) {
428
- const [tick, setTick] = React.useState(0);
429
- React.useEffect(() => {
430
- const bump = () => setTick((t) => t + 1);
431
- const off = wb.on("graphUiChanged", bump);
432
- return () => off();
433
- }, [wb]);
434
- return tick;
435
- }
436
- function useWorkbenchVersionTick(runner) {
437
- const [version, setVersion] = React.useState(0);
438
- React.useEffect(() => {
439
- const bump = () => setVersion((v) => v + 1);
440
- const u1 = runner.on("value", bump);
441
- const u2 = runner.on("error", bump);
442
- const u3 = runner.on("invalidate", bump);
443
- const u4 = runner.on("status", bump);
444
- const u5 = runner.on("stats", bump);
445
- return () => {
446
- u1();
447
- u2();
448
- u3();
449
- u4();
450
- u5();
451
- };
452
- }, [runner]);
453
- return version;
454
- }
455
- // Query param helpers
456
- function setSearchParam(key, val) {
457
- if (typeof window === "undefined")
458
- return;
459
- const url = new URL(window.location.href);
460
- if (val === undefined || val === "")
461
- url.searchParams.delete(key);
462
- else
463
- url.searchParams.set(key, val);
464
- window.history.replaceState({}, "", url.toString());
465
- }
466
- function useQueryParamBoolean(key, defaultValue) {
467
- const initial = React.useMemo(() => {
468
- if (typeof window === "undefined")
469
- return defaultValue;
470
- const sp = new URLSearchParams(window.location.search);
471
- const v = sp.get(key);
472
- if (v === null)
473
- return defaultValue;
474
- return v === "1" || v === "true";
475
- }, [key, defaultValue]);
476
- const [val, setVal] = React.useState(initial);
477
- const set = React.useCallback((v) => {
478
- setVal(v);
479
- setSearchParam(key, v ? "1" : undefined);
480
- }, [key]);
481
- React.useEffect(() => {
482
- const onPop = () => {
483
- const sp = new URLSearchParams(window.location.search);
484
- const v = sp.get(key);
485
- setVal(v === "1" || v === "true");
486
- };
487
- window.addEventListener("popstate", onPop);
488
- return () => window.removeEventListener("popstate", onPop);
489
- }, [key]);
490
- return [val, set];
491
- }
492
- function useQueryParamString(key, defaultValue) {
493
- const initial = React.useMemo(() => {
494
- if (typeof window === "undefined")
495
- return defaultValue;
496
- const sp = new URLSearchParams(window.location.search);
497
- const v = sp.get(key);
498
- return v ?? defaultValue;
499
- }, [key, defaultValue]);
500
- const [val, setVal] = React.useState(initial);
501
- const set = React.useCallback((v) => {
502
- setVal(v);
503
- setSearchParam(key, v);
504
- }, [key]);
505
- React.useEffect(() => {
506
- const onPop = () => {
507
- const sp = new URLSearchParams(window.location.search);
508
- const v = sp.get(key) ?? undefined;
509
- setVal(v);
510
- };
511
- window.addEventListener("popstate", onPop);
512
- return () => window.removeEventListener("popstate", onPop);
513
- }, [key]);
514
- return [val, set];
515
- }
516
-
517
- function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
518
- const [nodeStatus, setNodeStatus] = React.useState({});
519
- const [edgeStatus, setEdgeStatus] = React.useState({});
520
- const [events, setEvents] = React.useState([]);
521
- const clearEvents = React.useCallback(() => setEvents([]), []);
522
- // Validation
523
- const [validation, setValidation] = React.useState(undefined);
524
- // Selection (mirror workbench selectionChanged)
525
- const [selectedNodeId, setSelectedNodeId] = React.useState();
526
- const [selectedEdgeId, setSelectedEdgeId] = React.useState();
527
- const setSelection = React.useCallback((sel) => wb.setSelection(sel), [wb]);
528
- // Ticks
529
- const graphTick = useWorkbenchGraphTick(wb);
530
- const graphUiTick = useWorkbenchGraphUiTick(wb);
531
- const versionTick = useWorkbenchVersionTick(runner);
532
- const valuesTick = versionTick + graphTick + graphUiTick;
533
- // Def and IO values
534
- const def = wb.export();
535
- const inputsMap = React.useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
536
- const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
537
- // Auto layout (simple layered layout)
538
- const runAutoLayout = React.useCallback(() => {
539
- const cur = wb.export();
540
- const indegree = {};
541
- const adj = {};
542
- for (const n of cur.nodes) {
543
- indegree[n.nodeId] = 0;
544
- adj[n.nodeId] = [];
545
367
  }
546
- for (const e of cur.edges) {
547
- indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
548
- adj[e.source.nodeId].push(e.target.nodeId);
368
+ // Remote: no-op here; build is performed on remote server during launch
369
+ }
370
+ update(def) {
371
+ if (this.backend.kind === "local") {
372
+ if (!this.runtime)
373
+ return;
374
+ this.runtime.update(def, this.registry);
375
+ this.emit("invalidate", { reason: "graph-updated" });
376
+ return;
549
377
  }
550
- const q = Object.keys(indegree).filter((k) => indegree[k] === 0);
551
- const layers = [];
552
- while (q.length) {
553
- const layer = [];
554
- const next = [];
555
- for (const id of q) {
556
- layer.push(id);
557
- for (const nb of adj[id]) {
558
- indegree[nb] -= 1;
559
- if (indegree[nb] === 0)
560
- next.push(nb);
561
- }
378
+ // Remote: forward update; ignore errors (fire-and-forget)
379
+ void this.ensureRemote().then(async (rc) => {
380
+ try {
381
+ await rc.runner.update(def);
382
+ this.emit("invalidate", { reason: "graph-updated" });
562
383
  }
563
- layers.push(layer);
564
- q.splice(0, q.length, ...next);
565
- }
566
- const X = 360;
567
- const Y = 180;
568
- const pos = {};
569
- layers.forEach((layer, layerIndex) => {
570
- layer.forEach((id, itemIndex) => {
571
- pos[id] = { x: layerIndex * X, y: itemIndex * Y };
572
- });
384
+ catch { }
573
385
  });
574
- wb.setPositions(pos);
575
- }, [wb]);
576
- // Subscribe to runner/workbench events
577
- React.useEffect(() => {
578
- const add = (source, type) => (payload) => setEvents((prev) => {
579
- if (source === "workbench" &&
580
- (type === "graphChanged" || type === "graphUiChanged")) {
581
- const changeType = payload?.change?.type;
582
- if (changeType === "moveNode" || changeType === "moveNodes")
583
- return prev;
386
+ }
387
+ launch(def, opts) {
388
+ if (this.engine) {
389
+ throw new Error("Engine already running. Stop the current engine first.");
390
+ }
391
+ if (this.backend.kind === "local") {
392
+ this.build(def);
393
+ if (!this.runtime)
394
+ throw new Error("Runtime not built");
395
+ const rt = this.runtime;
396
+ switch (opts.engine) {
397
+ case "push":
398
+ this.engine = new sparkGraph.PushEngine(rt);
399
+ break;
400
+ case "batched":
401
+ this.engine = new sparkGraph.BatchedEngine(rt, {
402
+ flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
403
+ });
404
+ break;
405
+ case "pull":
406
+ this.engine = new sparkGraph.PullEngine(rt);
407
+ break;
408
+ case "hybrid":
409
+ this.engine = new sparkGraph.HybridEngine(rt, {
410
+ windowMs: opts.hybrid?.windowMs ?? 250,
411
+ batchThreshold: opts.hybrid?.batchThreshold ?? 3,
412
+ });
413
+ break;
414
+ case "step":
415
+ this.engine = new sparkGraph.StepEngine(rt);
416
+ break;
417
+ default:
418
+ throw new Error("Unknown engine kind");
584
419
  }
585
- const next = [
586
- { at: Date.now(), source, type, payload: structuredClone(payload) },
587
- ...prev,
588
- ];
589
- return next.length > 200 ? next.slice(0, 200) : next;
590
- });
591
- const off1 = runner.on("value", (e) => {
592
- if (e?.io === "input") {
593
- const nodeId = e?.nodeId;
594
- setNodeStatus((s) => ({
595
- ...s,
596
- [nodeId]: { ...(s[nodeId] ?? {}), invalidated: true },
597
- }));
420
+ this.engine.on("value", (e) => this.emit("value", e));
421
+ this.engine.on("error", (e) => this.emit("error", e));
422
+ this.engine.on("invalidate", (e) => this.emit("invalidate", e));
423
+ this.engine.on("stats", (e) => this.emit("stats", e));
424
+ this.engine.launch();
425
+ this.runningKind = opts.engine;
426
+ this.emit("status", { running: true, engine: this.runningKind });
427
+ for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
428
+ for (const [handle, value] of Object.entries(map)) {
429
+ this.engine.setInput(nodeId, handle, value);
430
+ }
598
431
  }
599
- return add("runner", "value")(e);
600
- });
601
- const off2 = runner.on("error", (e) => {
602
- const edgeError = e;
603
- const nodeError = e;
604
- if (edgeError?.kind === "edge-convert") {
605
- const edgeId = edgeError.edgeId;
606
- setEdgeStatus((s) => ({
607
- ...s,
608
- [edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
609
- }));
432
+ return;
433
+ }
434
+ // Remote: build remotely then launch
435
+ void this.ensureRemote().then(async (rc) => {
436
+ await rc.runner.build(def);
437
+ const eng = rc.runner.getEngine();
438
+ if (!rc.listenersBound) {
439
+ eng.on("value", (e) => {
440
+ rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
441
+ io: e.io,
442
+ value: e.value,
443
+ });
444
+ this.emit("value", e);
445
+ });
446
+ eng.on("error", (e) => this.emit("error", e));
447
+ eng.on("invalidate", (e) => this.emit("invalidate", e));
448
+ eng.on("stats", (e) => this.emit("stats", e));
449
+ rc.listenersBound = true;
610
450
  }
611
- else if (nodeError?.nodeId) {
612
- const nodeId = nodeError?.nodeId;
613
- setNodeStatus((s) => ({
614
- ...s,
615
- [nodeId]: {
616
- ...(s[nodeId] ?? {}),
617
- lastError: nodeError?.err,
618
- },
619
- }));
451
+ this.engine = eng;
452
+ this.engine.launch();
453
+ this.runningKind = "push";
454
+ this.emit("status", { running: true, engine: this.runningKind });
455
+ for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
456
+ for (const [handle, value] of Object.entries(map)) {
457
+ this.engine.setInput(nodeId, handle, value);
458
+ }
620
459
  }
621
- return add("runner", "error")(e);
622
460
  });
623
- const off3 = runner.on("invalidate", (e) => {
624
- if (e?.reason === "graph-updated") {
625
- setNodeStatus((s) => {
626
- const next = {};
627
- for (const n of wb.export().nodes) {
628
- next[n.nodeId] = { ...(s[n.nodeId] ?? {}), invalidated: true };
461
+ }
462
+ setInput(nodeId, handle, value) {
463
+ if (!this.stagedInputs[nodeId])
464
+ this.stagedInputs[nodeId] = {};
465
+ this.stagedInputs[nodeId][handle] = value;
466
+ if (this.engine)
467
+ this.engine.setInput(nodeId, handle, value);
468
+ }
469
+ async step() {
470
+ if (this.backend.kind !== "local")
471
+ return; // unsupported remotely
472
+ const eng = this.engine;
473
+ if (eng instanceof sparkGraph.StepEngine)
474
+ await eng.step();
475
+ }
476
+ async computeNode(nodeId) {
477
+ if (this.backend.kind !== "local")
478
+ return; // unsupported remotely
479
+ const eng = this.engine;
480
+ if (eng instanceof sparkGraph.PullEngine)
481
+ await eng.computeNode(nodeId);
482
+ }
483
+ flush() {
484
+ if (this.backend.kind !== "local")
485
+ return; // unsupported remotely
486
+ const eng = this.engine;
487
+ if (eng instanceof sparkGraph.BatchedEngine)
488
+ eng.flush();
489
+ }
490
+ getOutputs(def) {
491
+ const out = {};
492
+ if (this.backend.kind === "local") {
493
+ if (!this.runtime)
494
+ return out;
495
+ for (const n of def.nodes) {
496
+ const desc = this.registry.nodes.get(n.typeId);
497
+ const handles = Object.keys(desc?.outputs ?? {});
498
+ for (const h of handles) {
499
+ const v = this.runtime.getOutput(n.nodeId, h);
500
+ if (v !== undefined) {
501
+ if (!out[n.nodeId])
502
+ out[n.nodeId] = {};
503
+ out[n.nodeId][h] = v;
629
504
  }
630
- return next;
631
- });
632
- }
633
- return add("runner", "invalidate")(e);
634
- });
635
- const off3b = runner.on("stats", (s) => {
636
- if (!s)
637
- return;
638
- if (s.kind === "node-start") {
639
- const id = s.nodeId;
640
- setNodeStatus((prev) => ({
641
- ...prev,
642
- [id]: {
643
- ...(prev[id] ?? {}),
644
- running: true,
645
- progress: 0,
646
- invalidated: false,
647
- },
648
- }));
649
- }
650
- else if (s.kind === "node-progress") {
651
- const id = s.nodeId;
652
- setNodeStatus((prev) => ({
653
- ...prev,
654
- [id]: {
655
- ...(prev[id] ?? {}),
656
- running: true,
657
- progress: Number(s.progress) || 0,
658
- },
659
- }));
505
+ }
660
506
  }
661
- else if (s.kind === "node-done") {
662
- const id = s.nodeId;
663
- setNodeStatus((prev) => ({
664
- ...prev,
665
- [id]: { ...(prev[id] ?? {}), running: false },
666
- }));
507
+ return out;
508
+ }
509
+ const cache = this.remote?.valueCache;
510
+ if (!cache)
511
+ return out;
512
+ for (const n of def.nodes) {
513
+ const desc = this.registry.nodes.get(n.typeId);
514
+ const handles = Object.keys(desc?.outputs ?? {});
515
+ for (const h of handles) {
516
+ const key = `${n.nodeId}.${h}`;
517
+ const rec = cache.get(key);
518
+ if (rec && rec.io === "output") {
519
+ if (!out[n.nodeId])
520
+ out[n.nodeId] = {};
521
+ out[n.nodeId][h] = rec.value;
522
+ }
667
523
  }
668
- else if (s.kind === "edge-start") {
669
- const id = s.edgeId;
670
- setEdgeStatus((prev) => ({
671
- ...prev,
672
- [id]: { ...(prev[id] ?? {}), running: true },
673
- }));
524
+ }
525
+ return out;
526
+ }
527
+ getInputs(def) {
528
+ const out = {};
529
+ if (this.backend.kind === "local") {
530
+ for (const n of def.nodes) {
531
+ const staged = this.stagedInputs[n.nodeId] ?? {};
532
+ const runtimeInputs = this.runtime
533
+ ? this.runtime.__unsafe_getNodeData?.(n.nodeId)?.inputs ?? {}
534
+ : {};
535
+ if (this.isRunning()) {
536
+ out[n.nodeId] = runtimeInputs;
537
+ }
538
+ else {
539
+ const merged = { ...runtimeInputs, ...staged };
540
+ if (Object.keys(merged).length > 0)
541
+ out[n.nodeId] = merged;
542
+ }
674
543
  }
675
- else if (s.kind === "edge-done") {
676
- const id = s.edgeId;
677
- setEdgeStatus((prev) => ({
678
- ...prev,
679
- [id]: { ...(prev[id] ?? {}), running: false },
680
- }));
681
- }
682
- return add("runner", "stats")(s);
683
- });
684
- const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
685
- const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
686
- const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
687
- const off5b = wb.on("validationChanged", (r) => setValidation(r));
688
- const off6 = wb.on("selectionChanged", (sel) => {
689
- setSelectedNodeId(sel.nodes?.[0]);
690
- setSelectedEdgeId(sel.edges?.[0]);
691
- });
692
- const off7 = wb.on("error", add("workbench", "error"));
693
- wb.refreshValidation();
694
- return () => {
695
- off1();
696
- off2();
697
- off3();
698
- off3b();
699
- off4();
700
- off4b();
701
- off5();
702
- off5b();
703
- off6();
704
- off7();
705
- };
706
- }, [runner, wb]);
707
- // Push incremental updates into running engine without full reload
708
- React.useEffect(() => {
709
- if (runner.isRunning()) {
710
- try {
711
- runner.update(def);
712
- }
713
- catch { }
714
- }
715
- }, [runner, def, graphTick]);
716
- const validationByNode = React.useMemo(() => {
717
- const inputs = {};
718
- const outputs = {};
719
- const issues = {};
720
- if (!validation)
721
- return { inputs, outputs, issues };
722
- for (const is of validation.issues ?? []) {
723
- const d = is?.data;
724
- const level = is?.level;
725
- const code = String(is?.code ?? "");
726
- const message = String(is?.message ?? code);
727
- if (!d)
728
- continue;
729
- if (d.nodeId) {
730
- if (d.input) {
731
- const arr = inputs[d.nodeId] ?? (inputs[d.nodeId] = []);
732
- arr.push({ handle: String(d.input), level, message, code });
733
- const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
734
- nodeArr.push({ level, message, code });
735
- }
736
- if (d.output) {
737
- const arr = outputs[d.nodeId] ?? (outputs[d.nodeId] = []);
738
- arr.push({ handle: String(d.output), level, message, code });
739
- const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
740
- nodeArr.push({ level, message, code });
741
- }
742
- if (!d.input && !d.output) {
743
- const arr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
744
- arr.push({ level, message, code });
745
- }
746
- }
747
- }
748
- return { inputs, outputs, issues };
749
- }, [validation]);
750
- const validationGlobal = React.useMemo(() => {
751
- const list = [];
752
- if (!validation)
753
- return list;
754
- for (const is of validation.issues ?? []) {
755
- const d = is?.data;
756
- const level = is?.level;
757
- const code = String(is?.code ?? "");
758
- const message = String(is?.message ?? code);
759
- if (!d || (!d.nodeId && !d.edgeId)) {
760
- list.push({ level, code, message });
761
- }
762
- }
763
- return list;
764
- }, [validation]);
765
- const validationByEdge = React.useMemo(() => {
766
- const errors = {};
767
- const issues = {};
768
- if (!validation)
769
- return { errors, issues };
770
- for (const is of validation.issues ?? []) {
771
- const d = is?.data;
772
- const level = is?.level;
773
- const code = String(is?.code ?? "");
774
- const message = String(is?.message ?? code);
775
- if (d?.edgeId) {
776
- if (level === "error")
777
- errors[d.edgeId] = true;
778
- const arr = issues[d.edgeId] ?? (issues[d.edgeId] = []);
779
- arr.push({ level, message, code });
544
+ return out;
545
+ }
546
+ const cache = this.remote?.valueCache;
547
+ for (const n of def.nodes) {
548
+ const staged = this.stagedInputs[n.nodeId] ?? {};
549
+ const desc = this.registry.nodes.get(n.typeId);
550
+ const handles = Object.keys(desc?.inputs ?? {});
551
+ const cur = {};
552
+ for (const h of handles) {
553
+ const rec = cache?.get(`${n.nodeId}.${h}`);
554
+ if (rec && rec.io === "input")
555
+ cur[h] = rec.value;
780
556
  }
557
+ const merged = this.isRunning() ? cur : { ...cur, ...staged };
558
+ if (Object.keys(merged).length > 0)
559
+ out[n.nodeId] = merged;
781
560
  }
782
- return { errors, issues };
783
- }, [validation]);
784
- const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
785
- const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
786
- const start = React.useCallback((engine) => {
787
- try {
788
- runner.launch(wb.export(), { engine });
561
+ return out;
562
+ }
563
+ async whenIdle() {
564
+ await this.engine?.whenIdle();
565
+ }
566
+ on(event, handler) {
567
+ if (!this.listeners.has(event))
568
+ this.listeners.set(event, new Set());
569
+ const set = this.listeners.get(event);
570
+ set.add(handler);
571
+ return () => set.delete(handler);
572
+ }
573
+ emit(event, payload) {
574
+ const set = this.listeners.get(event);
575
+ if (set)
576
+ for (const h of Array.from(set))
577
+ h(payload);
578
+ }
579
+ dispose() {
580
+ this.engine?.dispose();
581
+ this.engine = undefined;
582
+ this.runtime?.dispose();
583
+ this.runtime = undefined;
584
+ this.remote = undefined;
585
+ if (this.runningKind) {
586
+ this.runningKind = undefined;
587
+ this.emit("status", { running: false, engine: undefined });
789
588
  }
790
- catch { }
791
- }, [runner, wb]);
792
- const stop = React.useCallback(() => runner.dispose(), [runner]);
793
- const step = React.useCallback(() => runner.step(), [runner]);
794
- const flush = React.useCallback(() => runner.flush(), [runner]);
795
- const value = React.useMemo(() => ({
796
- wb,
797
- runner,
798
- registry,
799
- setRegistry,
800
- def,
801
- selectedNodeId,
802
- selectedEdgeId,
803
- setSelection,
804
- nodeStatus,
805
- edgeStatus,
806
- valuesTick,
807
- inputsMap,
808
- outputsMap,
809
- validationByNode,
810
- validationByEdge,
811
- validationGlobal,
812
- events,
813
- clearEvents,
814
- isRunning,
815
- engineKind,
816
- start,
817
- stop,
818
- step,
819
- flush,
820
- runAutoLayout,
821
- }), [
822
- wb,
823
- runner,
824
- registry,
825
- setRegistry,
826
- def,
827
- selectedNodeId,
828
- selectedEdgeId,
829
- setSelection,
830
- nodeStatus,
831
- edgeStatus,
832
- valuesTick,
833
- inputsMap,
834
- outputsMap,
835
- validationByNode,
836
- validationByEdge,
837
- validationGlobal,
838
- events,
839
- clearEvents,
840
- isRunning,
841
- engineKind,
842
- start,
843
- stop,
844
- step,
845
- flush,
846
- runAutoLayout,
847
- ]);
848
- return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
849
- }
850
-
851
- function toReactFlow(def, positions, registry, selectedNodeIds, selectedEdgeIds, opts) {
852
- const nodeHandleMap = {};
853
- const nodes = def.nodes.map((n) => {
854
- const desc = registry.nodes.get(n.typeId);
855
- const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
856
- const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
857
- nodeHandleMap[n.nodeId] = {
858
- inputs: new Set(inputHandles.map((h) => h.id)),
859
- outputs: new Set(outputHandles.map((h) => h.id)),
860
- };
861
- return {
862
- id: n.nodeId,
863
- data: {
864
- typeId: n.typeId,
865
- params: n.params,
866
- inputHandles,
867
- outputHandles,
868
- showValues: opts?.showValues,
869
- inputValues: opts?.inputs?.[n.nodeId],
870
- outputValues: opts?.outputs?.[n.nodeId],
871
- status: opts?.nodeStatus?.[n.nodeId],
872
- validation: {
873
- inputs: opts?.nodeValidation?.inputs?.[n.nodeId] ?? [],
874
- outputs: opts?.nodeValidation?.outputs?.[n.nodeId] ?? [],
875
- issues: opts?.nodeValidation?.issues?.[n.nodeId] ?? [],
876
- },
877
- toDisplay: opts?.toDisplay,
878
- },
879
- position: positions[n.nodeId] ?? { x: 0, y: 0 },
880
- type: opts?.resolveNodeType?.(n.typeId) ?? "spark:default",
881
- selected: selectedNodeIds ? selectedNodeIds.has(n.nodeId) : undefined,
882
- };
883
- });
884
- const edges = def.edges
885
- .filter((e) => {
886
- const src = nodeHandleMap[e.source.nodeId];
887
- const dst = nodeHandleMap[e.target.nodeId];
888
- if (!src || !dst)
889
- return false;
890
- return src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle);
891
- })
892
- .map((e) => {
893
- const st = opts?.edgeStatus?.[e.id];
894
- const isRunning = !!st?.running;
895
- const hasError = !!st?.lastError;
896
- const isInvalidEdge = !!opts?.edgeValidation?.[e.id];
897
- const style = hasError || isInvalidEdge
898
- ? { stroke: "#ef4444", strokeWidth: 2 }
899
- : isRunning
900
- ? { stroke: "#3b82f6" }
901
- : undefined;
902
- return {
903
- id: e.id,
904
- source: e.source.nodeId,
905
- target: e.target.nodeId,
906
- sourceHandle: e.source.handle,
907
- targetHandle: e.target.handle,
908
- selected: selectedEdgeIds ? selectedEdgeIds.has(e.id) : undefined,
909
- animated: isRunning,
910
- style,
911
- };
912
- });
913
- return { nodes, edges };
914
- }
915
-
916
- function IssueBadge({ level, title, size = 12, className, }) {
917
- const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
918
- return (jsxRuntime.jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsxRuntime.jsx(react.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react.WarningCircleIcon, { size: size, weight: "fill" })) }));
919
- }
920
-
921
- const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
922
- const typeId = data.typeId;
923
- const inputEntries = data.inputHandles ?? [];
924
- const outputEntries = data.outputHandles ?? [];
925
- const status = data.status ?? {};
926
- const validation = data.validation ?? {
927
- inputs: [],
928
- outputs: [],
929
- issues: [],
930
- };
931
- const HEADER_SIZE = 24;
932
- const ROW_SIZE = 22;
933
- const maxRows = Math.max(inputEntries.length, outputEntries.length);
934
- const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
935
- const minWidth = data.showValues ? 320 : 160;
936
- const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
937
- const hasError = !!status.lastError;
938
- const isRunning = !!status.running;
939
- const isInvalid = !!status.invalidated && !isRunning && !hasError;
940
- const borderClasses = selected
941
- ? "border-2 border-gray-900 dark:border-gray-100"
942
- : hasError
943
- ? "border-2 border-red-500"
944
- : isRunning
945
- ? "border-2 border-blue-500 ring-2 ring-blue-200 dark:ring-blue-900"
946
- : isInvalid
947
- ? "border-2 border-amber-500 border-dashed"
948
- : "border border-gray-500 dark:border-gray-400";
949
- const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
950
- return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900 border-solid", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [jsxRuntime.jsxs("div", { className: "flex h-6 items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", children: [jsxRuntime.jsx("strong", { className: "flex-1 h-full leading-6 text-xs", children: typeId }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsxRuntime.jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsxRuntime.jsx(react.XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
951
- ? "error"
952
- : "warning", size: 12, className: "w-3 h-3", title: validation.issues
953
- .map((v) => `${v.code}: ${v.message}`)
954
- .join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), (isRunning || pct > 0) && (jsxRuntime.jsx("div", { className: "h-1 bg-blue-200 dark:bg-blue-900", children: jsxRuntime.jsx("div", { className: "h-1 bg-blue-500 transition-all", style: { width: `${pct}%` } }) })), inputEntries.map((entry, i) => {
955
- const vIssues = validation.inputs.filter((v) => v.handle === entry.id);
956
- const hasAny = vIssues.length > 0;
957
- const hasErr = vIssues.some((v) => v.level === "error");
958
- const title = vIssues
959
- .map((v) => `${v.code}: ${v.message}`)
960
- .join("; ");
961
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8 }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), data.showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: data.toDisplay
962
- ? data.toDisplay(entry.typeId, data.inputValues?.[entry.id])
963
- : String(data.inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
964
- }), outputEntries.map((entry, i) => {
965
- const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
966
- const hasAny = vIssues.length > 0;
967
- const hasErr = vIssues.some((v) => v.level === "error");
968
- const title = vIssues
969
- .map((v) => `${v.code}: ${v.message}`)
970
- .join("; ");
971
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8, textAlign: "right" }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), data.showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: data.toDisplay
972
- ? data.toDisplay(entry.typeId, data.outputValues?.[entry.id])
973
- : String(data.outputValues?.[entry.id]) }))] })] }, `out-${entry.id}`));
974
- })] }));
975
- });
976
- DefaultNode.displayName = "DefaultNode";
977
-
978
- function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
979
- const { registry } = useWorkbenchContext();
980
- const rf = ReactFlow.useReactFlow();
981
- if (!open || !clientPos)
982
- return null;
983
- const items = Array.from(registry.nodes.keys());
984
- const handleClick = (typeId) => {
985
- const p = rf.project({ x: clientPos.x, y: clientPos.y });
986
- onAdd(typeId, p);
987
- onClose();
988
- };
989
- return (jsxRuntime.jsxs("div", { className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: clientPos.x, top: clientPos.y }, onMouseLeave: onClose, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Add Node" }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: items.map((id) => (jsxRuntime.jsx("button", { onClick: () => handleClick(id), className: "block w-full text-left px-2 py-1 hover:bg-gray-100 cursor-pointer", children: id }, id))) })] }));
990
- }
991
-
992
- function WorkbenchCanvas({ showValues, toDisplay, }) {
993
- const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
994
- const ioValues = { inputs: inputsMap, outputs: outputsMap };
995
- const nodeValidation = validationByNode;
996
- const edgeValidation = validationByEdge.errors;
997
- const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
998
- const { nodeTypes, resolveNodeType } = React.useMemo(() => {
999
- // Build nodeTypes map using UI extension registry
1000
- const ui = wb.getUI();
1001
- const custom = new Map();
1002
- for (const typeId of Array.from(registry.nodes.keys())) {
1003
- const renderer = ui.getNodeRenderer(typeId);
1004
- if (renderer)
1005
- custom.set(typeId, renderer);
589
+ }
590
+ isRunning() {
591
+ return !!this.engine;
592
+ }
593
+ getRunningEngine() {
594
+ return this.runningKind;
595
+ }
596
+ // Ensure remote transport/runner
597
+ async ensureRemote() {
598
+ if (this.remote)
599
+ return this.remote;
600
+ let transport;
601
+ if (this.backend.kind === "remote-http") {
602
+ if (!sparkRemote.HttpPollingTransport)
603
+ throw new Error("HttpPollingTransport not available");
604
+ transport = new sparkRemote.HttpPollingTransport(this.backend.baseUrl);
605
+ await transport.connect();
1006
606
  }
1007
- const types = { "spark:default": DefaultNode };
1008
- for (const [typeId, comp] of custom.entries()) {
1009
- types[`spark:${typeId}`] = comp;
607
+ else if (this.backend.kind === "remote-ws") {
608
+ if (!sparkRemote.WebSocketTransport)
609
+ throw new Error("WebSocketTransport not available");
610
+ transport = new sparkRemote.WebSocketTransport(this.backend.url);
611
+ await transport.connect();
1010
612
  }
1011
- const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark:${nodeTypeId}` : "spark:default";
1012
- return { nodeTypes: types, resolveNodeType: resolver };
1013
- // registry is stable; ui renderers expected to be set up before mount
1014
- }, [wb, registry]);
1015
- const { nodes, edges } = React.useMemo(() => {
1016
- const def = wb.export();
1017
- const sel = wb.getSelection();
1018
- return toReactFlow(def, wb.getPositions(), registry, new Set(sel.nodes), new Set(sel.edges), {
1019
- showValues,
1020
- inputs: ioValues.inputs,
1021
- outputs: ioValues.outputs,
1022
- resolveNodeType,
1023
- toDisplay,
1024
- nodeStatus,
1025
- edgeStatus,
1026
- nodeValidation,
1027
- edgeValidation,
1028
- });
1029
- }, [
1030
- showValues,
1031
- ioValues,
1032
- valuesTick,
1033
- toDisplay,
1034
- nodeStatus,
1035
- edgeStatus,
1036
- nodeValidation,
1037
- edgeValidation,
1038
- ]);
1039
- const [menuOpen, setMenuOpen] = React.useState(false);
1040
- const [menuPos, setMenuPos] = React.useState(null);
1041
- const onContextMenu = (e) => {
1042
- e.preventDefault();
1043
- setMenuPos({ x: e.clientX, y: e.clientY });
1044
- setMenuOpen(true);
1045
- };
1046
- const addNodeAt = (typeId, pos) => {
1047
- wb.addNode({ typeId, position: pos });
1048
- };
1049
- return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onSelectionChange: onSelectionChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, children: [jsxRuntime.jsx(ReactFlow.Background, {}), jsxRuntime.jsx(ReactFlow.MiniMap, {}), jsxRuntime.jsx(ReactFlow.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) })] }) }));
613
+ else {
614
+ throw new Error("Remote backend not configured");
615
+ }
616
+ const runner = new sparkRemote.RemoteRunner(transport);
617
+ this.remote = {
618
+ runner,
619
+ transport,
620
+ valueCache: new Map(),
621
+ listenersBound: false,
622
+ };
623
+ return this.remote;
624
+ }
1050
625
  }
1051
626
 
1052
- function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
1053
- const { events, clearEvents } = useWorkbenchContext();
1054
- const scrollRef = React.useRef(null);
1055
- const rows = React.useMemo(() => {
1056
- const filtered = hideWorkbench
1057
- ? events.filter((e) => e.source !== "workbench")
1058
- : events;
1059
- return filtered.slice().reverse();
1060
- }, [events, hideWorkbench]);
1061
- React.useEffect(() => {
1062
- if (!autoScroll)
627
+ function useWorkbenchBridge(wb) {
628
+ const onConnect = React.useCallback((params) => {
629
+ if (!params.source || !params.target)
1063
630
  return;
1064
- const el = scrollRef.current;
1065
- if (!el)
631
+ if (!params.sourceHandle || !params.targetHandle)
1066
632
  return;
1067
- el.scrollTop = el.scrollHeight;
1068
- }, [rows, autoScroll]);
1069
- const renderPayload = (v) => {
1070
- try {
1071
- return JSON.stringify(v, null, 0);
1072
- }
1073
- catch {
1074
- return String(v);
1075
- }
633
+ wb.connect({
634
+ source: { nodeId: params.source, handle: params.sourceHandle },
635
+ target: { nodeId: params.target, handle: params.targetHandle },
636
+ });
637
+ }, [wb]);
638
+ const onNodesChange = React.useCallback((changes) => {
639
+ changes.forEach((c) => {
640
+ if (c.type === "position" && c.position)
641
+ wb.setPosition(c.id, c.position);
642
+ if (c.type === "remove")
643
+ wb.removeNode(c.id);
644
+ if (c.type === "select")
645
+ wb.toggleNodeSelection(c.id);
646
+ });
647
+ }, [wb]);
648
+ const onEdgesDelete = React.useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
649
+ const onEdgesChange = React.useCallback((changes) => {
650
+ changes.forEach((c) => {
651
+ if (c.type === "remove")
652
+ wb.disconnect(c.id);
653
+ else if (c.type === "select")
654
+ wb.toggleEdgeSelection(c.id);
655
+ });
656
+ }, [wb]);
657
+ const onNodesDelete = React.useCallback((nodes) => {
658
+ for (const n of nodes)
659
+ wb.removeNode(n.id);
660
+ }, [wb]);
661
+ const onSelectionChange = React.useCallback((sel) => {
662
+ const next = {
663
+ nodes: sel.nodes.map((n) => n.id),
664
+ edges: sel.edges.map((e) => e.id),
665
+ };
666
+ const cur = wb.getSelection();
667
+ const sameLen = cur.nodes.length === next.nodes.length &&
668
+ cur.edges.length === next.edges.length;
669
+ const same = sameLen &&
670
+ cur.nodes.every((id, i) => id === next.nodes[i]) &&
671
+ cur.edges.every((id, i) => id === next.edges[i]);
672
+ if (!same)
673
+ wb.setSelection(next);
674
+ }, [wb]);
675
+ return {
676
+ onConnect,
677
+ onNodesChange,
678
+ onEdgesChange,
679
+ onEdgesDelete,
680
+ onNodesDelete,
681
+ onSelectionChange,
1076
682
  };
1077
- return (jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Events" }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Hide workbench" })] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Auto scroll" })] }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev, idx) => (jsxRuntime.jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [jsxRuntime.jsx("span", { className: "w-8 shrink-0 text-right text-gray-500 select-none", children: idx + 1 }), jsxRuntime.jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsxRuntime.jsx("pre", { className: "m-0 whitespace-pre-wrap ml-10", children: renderPayload(ev.payload) })] }, `${ev.at}:${idx}`))) })] }));
1078
683
  }
1079
-
1080
- function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toDisplay, setInput, }) {
1081
- const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
1082
- const nodeValidationIssues = validationByNode.issues;
1083
- const edgeValidationIssues = validationByEdge.issues;
1084
- const nodeValidationHandles = validationByNode;
1085
- const globalValidationIssues = validationGlobal;
1086
- const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
1087
- const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
1088
- const selectedDesc = selectedNode
1089
- ? registry.nodes.get(selectedNode.typeId)
1090
- : undefined;
1091
- const inputHandles = Object.keys(selectedDesc?.inputs ?? {});
1092
- const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
1093
- const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
1094
- const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
1095
- const selectedNodeStatus = selectedNodeId
1096
- ? nodeStatus?.[selectedNodeId]
1097
- : undefined;
1098
- const selectedNodeValidation = selectedNodeId
1099
- ? nodeValidationIssues?.[selectedNodeId] ?? []
1100
- : [];
1101
- const selectedEdgeValidation = selectedEdge
1102
- ? edgeValidationIssues?.[selectedEdge.id] ?? []
1103
- : [];
1104
- const selectedNodeHandleValidation = selectedNodeId
1105
- ? {
1106
- inputs: nodeValidationHandles?.inputs?.[selectedNodeId] ?? [],
1107
- outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
1108
- }
1109
- : { inputs: [], outputs: [] };
1110
- // Local drafts and originals for commit-on-blur/enter behavior
1111
- const [drafts, setDrafts] = React.useState({});
1112
- const [originals, setOriginals] = React.useState({});
1113
- // Initialize drafts from current inputs whenever selection or valuesTick change,
1114
- // but do not clobber fields currently being edited (dirty drafts)
684
+ function useWorkbenchGraphTick(wb) {
685
+ const [tick, setTick] = React.useState(0);
686
+ React.useEffect(() => {
687
+ const bump = () => setTick((t) => t + 1);
688
+ const off = wb.on("graphChanged", bump);
689
+ return () => off();
690
+ }, [wb]);
691
+ return tick;
692
+ }
693
+ function useWorkbenchGraphUiTick(wb) {
694
+ const [tick, setTick] = React.useState(0);
695
+ React.useEffect(() => {
696
+ const bump = () => setTick((t) => t + 1);
697
+ const off = wb.on("graphUiChanged", bump);
698
+ return () => off();
699
+ }, [wb]);
700
+ return tick;
701
+ }
702
+ function useWorkbenchVersionTick(runner) {
703
+ const [version, setVersion] = React.useState(0);
704
+ React.useEffect(() => {
705
+ const bump = () => setVersion((v) => v + 1);
706
+ const u1 = runner.on("value", bump);
707
+ const u2 = runner.on("error", bump);
708
+ const u3 = runner.on("invalidate", bump);
709
+ const u4 = runner.on("status", bump);
710
+ const u5 = runner.on("stats", bump);
711
+ return () => {
712
+ u1();
713
+ u2();
714
+ u3();
715
+ u4();
716
+ u5();
717
+ };
718
+ }, [runner]);
719
+ return version;
720
+ }
721
+ // Query param helpers
722
+ function setSearchParam(key, val) {
723
+ if (typeof window === "undefined")
724
+ return;
725
+ const url = new URL(window.location.href);
726
+ if (val === undefined || val === "")
727
+ url.searchParams.delete(key);
728
+ else
729
+ url.searchParams.set(key, val);
730
+ window.history.replaceState({}, "", url.toString());
731
+ }
732
+ function useQueryParamBoolean(key, defaultValue) {
733
+ const initial = React.useMemo(() => {
734
+ if (typeof window === "undefined")
735
+ return defaultValue;
736
+ const sp = new URLSearchParams(window.location.search);
737
+ const v = sp.get(key);
738
+ if (v === null)
739
+ return defaultValue;
740
+ return v === "1" || v === "true";
741
+ }, [key, defaultValue]);
742
+ const [val, setVal] = React.useState(initial);
743
+ const set = React.useCallback((v) => {
744
+ setVal(v);
745
+ setSearchParam(key, v ? "1" : undefined);
746
+ }, [key]);
747
+ React.useEffect(() => {
748
+ const onPop = () => {
749
+ const sp = new URLSearchParams(window.location.search);
750
+ const v = sp.get(key);
751
+ setVal(v === "1" || v === "true");
752
+ };
753
+ window.addEventListener("popstate", onPop);
754
+ return () => window.removeEventListener("popstate", onPop);
755
+ }, [key]);
756
+ return [val, set];
757
+ }
758
+ function useQueryParamString(key, defaultValue) {
759
+ const initial = React.useMemo(() => {
760
+ if (typeof window === "undefined")
761
+ return defaultValue;
762
+ const sp = new URLSearchParams(window.location.search);
763
+ const v = sp.get(key);
764
+ return v ?? defaultValue;
765
+ }, [key, defaultValue]);
766
+ const [val, setVal] = React.useState(initial);
767
+ const set = React.useCallback((v) => {
768
+ setVal(v);
769
+ setSearchParam(key, v);
770
+ }, [key]);
1115
771
  React.useEffect(() => {
1116
- const shallowEqual = (a, b) => {
1117
- const ak = Object.keys(a);
1118
- const bk = Object.keys(b);
1119
- if (ak.length !== bk.length)
1120
- return false;
1121
- for (const k of ak)
1122
- if (a[k] !== b[k])
1123
- return false;
1124
- return true;
772
+ const onPop = () => {
773
+ const sp = new URLSearchParams(window.location.search);
774
+ const v = sp.get(key) ?? undefined;
775
+ setVal(v);
1125
776
  };
1126
- if (!selectedNodeId) {
1127
- if (Object.keys(drafts).length || Object.keys(originals).length) {
1128
- setDrafts({});
1129
- setOriginals({});
1130
- }
1131
- return;
1132
- }
1133
- const desc = selectedDesc;
1134
- const handles = Object.keys(desc?.inputs ?? {});
1135
- const nextDrafts = { ...drafts };
1136
- const nextOriginals = { ...originals };
1137
- for (const h of handles) {
1138
- const typeId = desc?.inputs?.[h];
1139
- const current = nodeInputs[h];
1140
- const display = toDisplay(typeId, current);
1141
- const wasOriginal = originals[h];
1142
- const isDirty = drafts[h] !== undefined &&
1143
- wasOriginal !== undefined &&
1144
- drafts[h] !== wasOriginal;
1145
- if (!isDirty) {
1146
- nextDrafts[h] = display;
1147
- nextOriginals[h] = display;
1148
- }
1149
- }
1150
- if (!shallowEqual(drafts, nextDrafts))
1151
- setDrafts(nextDrafts);
1152
- if (!shallowEqual(originals, nextOriginals))
1153
- setOriginals(nextOriginals);
1154
- }, [selectedNodeId, selectedDesc, valuesTick]);
1155
- const widthClass = debug ? "w-[480px]" : "w-[320px]";
1156
- return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
1157
- selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
1158
- const typeId = (selectedDesc?.inputs ?? {})[h];
1159
- const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
1160
- e.target.handle === h);
1161
- const commonProps = {
1162
- style: { flex: 1 },
1163
- disabled: isLinked,
1164
- };
1165
- const current = nodeInputs[h];
1166
- const value = drafts[h] ?? toDisplay(typeId, current);
1167
- const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
1168
- const commit = () => {
1169
- const draft = drafts[h];
1170
- if (draft === undefined)
1171
- return;
1172
- setInput(h, draft);
1173
- setOriginals((o) => ({ ...o, [h]: draft }));
1174
- };
1175
- const revert = () => {
1176
- const orig = originals[h] ?? toDisplay(typeId, current);
1177
- setDrafts((d) => ({ ...d, [h]: orig }));
1178
- };
1179
- const isEnum = typeId?.includes("enum:");
1180
- const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
1181
- const hasValidation = inIssues.length > 0;
1182
- const hasErr = inIssues.some((m) => m.level === "error");
1183
- const title = inIssues
1184
- .map((v) => `${v.code}: ${v.message}`)
1185
- .join("; ");
1186
- return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-28", children: [h, jsxRuntime.jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: drafts[h] ?? toDisplay(typeId, current), onChange: (e) => {
1187
- const label = String(e.target.value);
1188
- const byLabel = registry.enums
1189
- .get(typeId)
1190
- ?.labelToValue.get(label.toLowerCase());
1191
- let raw = (byLabel !== undefined ? byLabel : Number(label));
1192
- if (!Number.isFinite(raw))
1193
- raw = undefined;
1194
- setInput(h, raw);
1195
- const display = toDisplay(typeId, raw);
1196
- setDrafts((d) => ({ ...d, [h]: display }));
1197
- setOriginals((o) => ({ ...o, [h]: display }));
1198
- }, ...commonProps, children: [jsxRuntime.jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsxRuntime.jsx("option", { value: opt.label, children: opt.label }, opt.value)))] })) : (jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", placeholder: isLinked ? "wired" : undefined, value: value, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
1199
- if (e.key === "Enter")
1200
- commit();
1201
- if (e.key === "Escape")
1202
- revert();
1203
- }, ...commonProps }))] }, h));
1204
- }))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsx("label", { className: "w-20", children: h }), jsxRuntime.jsx("div", { className: "flex-1", children: toDisplay(selectedDesc?.outputs?.[h], nodeOutputs[h]) }), (() => {
1205
- const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
1206
- if (outIssues.length === 0)
1207
- return null;
1208
- const outErr = outIssues.some((m) => m.level === "error");
1209
- const outTitle = outIssues
1210
- .map((v) => `${v.code}: ${v.message}`)
1211
- .join("; ");
1212
- return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
1213
- })()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, 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 }) }))] }));
777
+ window.addEventListener("popstate", onPop);
778
+ return () => window.removeEventListener("popstate", onPop);
779
+ }, [key]);
780
+ return [val, set];
1214
781
  }
1215
782
 
1216
- class GraphRunner {
1217
- constructor(registry, backend) {
1218
- this.registry = registry;
1219
- this.listeners = new Map();
1220
- this.stagedInputs = {};
1221
- this.backend = { kind: "local" };
1222
- if (backend)
1223
- this.backend = backend;
1224
- }
1225
- build(def) {
1226
- if (this.backend.kind === "local") {
1227
- const builder = new sparkGraph.GraphBuilder(this.registry);
1228
- this.runtime = builder.build(def);
1229
- return;
783
+ const WorkbenchContext = React.createContext(null);
784
+ function useWorkbenchContext() {
785
+ const ctx = React.useContext(WorkbenchContext);
786
+ if (!ctx)
787
+ throw new Error("useWorkbenchContext must be used within WorkbenchProvider");
788
+ return ctx;
789
+ }
790
+
791
+ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
792
+ const [nodeStatus, setNodeStatus] = React.useState({});
793
+ const [edgeStatus, setEdgeStatus] = React.useState({});
794
+ const [events, setEvents] = React.useState([]);
795
+ const clearEvents = React.useCallback(() => setEvents([]), []);
796
+ // Validation
797
+ const [validation, setValidation] = React.useState(undefined);
798
+ // Selection (mirror workbench selectionChanged)
799
+ const [selectedNodeId, setSelectedNodeId] = React.useState();
800
+ const [selectedEdgeId, setSelectedEdgeId] = React.useState();
801
+ const setSelection = React.useCallback((sel) => wb.setSelection(sel), [wb]);
802
+ // Ticks
803
+ const graphTick = useWorkbenchGraphTick(wb);
804
+ const graphUiTick = useWorkbenchGraphUiTick(wb);
805
+ const versionTick = useWorkbenchVersionTick(runner);
806
+ const valuesTick = versionTick + graphTick + graphUiTick;
807
+ // Def and IO values
808
+ const def = wb.export();
809
+ const inputsMap = React.useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
810
+ const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
811
+ // Auto layout (simple layered layout)
812
+ const runAutoLayout = React.useCallback(() => {
813
+ const cur = wb.export();
814
+ const indegree = {};
815
+ const adj = {};
816
+ for (const n of cur.nodes) {
817
+ indegree[n.nodeId] = 0;
818
+ adj[n.nodeId] = [];
1230
819
  }
1231
- // Remote: no-op here; build is performed on remote server during launch
1232
- }
1233
- update(def) {
1234
- if (this.backend.kind === "local") {
1235
- if (!this.runtime)
1236
- return;
1237
- this.runtime.update(def, this.registry);
1238
- this.emit("invalidate", { reason: "graph-updated" });
1239
- return;
820
+ for (const e of cur.edges) {
821
+ indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
822
+ adj[e.source.nodeId].push(e.target.nodeId);
1240
823
  }
1241
- // Remote: forward update; ignore errors (fire-and-forget)
1242
- void this.ensureRemote().then(async (rc) => {
1243
- try {
1244
- await rc.runner.update(def);
1245
- this.emit("invalidate", { reason: "graph-updated" });
824
+ const q = Object.keys(indegree).filter((k) => indegree[k] === 0);
825
+ const layers = [];
826
+ while (q.length) {
827
+ const layer = [];
828
+ const next = [];
829
+ for (const id of q) {
830
+ layer.push(id);
831
+ for (const nb of adj[id]) {
832
+ indegree[nb] -= 1;
833
+ if (indegree[nb] === 0)
834
+ next.push(nb);
835
+ }
1246
836
  }
1247
- catch { }
1248
- });
1249
- }
1250
- launch(def, opts) {
1251
- if (this.engine) {
1252
- throw new Error("Engine already running. Stop the current engine first.");
837
+ layers.push(layer);
838
+ q.splice(0, q.length, ...next);
1253
839
  }
1254
- if (this.backend.kind === "local") {
1255
- this.build(def);
1256
- if (!this.runtime)
1257
- throw new Error("Runtime not built");
1258
- const rt = this.runtime;
1259
- switch (opts.engine) {
1260
- case "push":
1261
- this.engine = new sparkGraph.PushEngine(rt);
1262
- break;
1263
- case "batched":
1264
- this.engine = new sparkGraph.BatchedEngine(rt, {
1265
- flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
1266
- });
1267
- break;
1268
- case "pull":
1269
- this.engine = new sparkGraph.PullEngine(rt);
1270
- break;
1271
- case "hybrid":
1272
- this.engine = new sparkGraph.HybridEngine(rt, {
1273
- windowMs: opts.hybrid?.windowMs ?? 250,
1274
- batchThreshold: opts.hybrid?.batchThreshold ?? 3,
1275
- });
1276
- break;
1277
- case "step":
1278
- this.engine = new sparkGraph.StepEngine(rt);
1279
- break;
1280
- default:
1281
- throw new Error("Unknown engine kind");
840
+ const X = 360;
841
+ const Y = 180;
842
+ const pos = {};
843
+ layers.forEach((layer, layerIndex) => {
844
+ layer.forEach((id, itemIndex) => {
845
+ pos[id] = { x: layerIndex * X, y: itemIndex * Y };
846
+ });
847
+ });
848
+ wb.setPositions(pos);
849
+ }, [wb]);
850
+ // Subscribe to runner/workbench events
851
+ React.useEffect(() => {
852
+ const add = (source, type) => (payload) => setEvents((prev) => {
853
+ if (source === "workbench" &&
854
+ (type === "graphChanged" || type === "graphUiChanged")) {
855
+ const changeType = payload?.change?.type;
856
+ if (changeType === "moveNode" || changeType === "moveNodes")
857
+ return prev;
858
+ }
859
+ const next = [
860
+ { at: Date.now(), source, type, payload: structuredClone(payload) },
861
+ ...prev,
862
+ ];
863
+ return next.length > 200 ? next.slice(0, 200) : next;
864
+ });
865
+ const off1 = runner.on("value", (e) => {
866
+ if (e?.io === "input") {
867
+ const nodeId = e?.nodeId;
868
+ setNodeStatus((s) => ({
869
+ ...s,
870
+ [nodeId]: { ...(s[nodeId] ?? {}), invalidated: true },
871
+ }));
872
+ }
873
+ return add("runner", "value")(e);
874
+ });
875
+ const off2 = runner.on("error", (e) => {
876
+ const edgeError = e;
877
+ const nodeError = e;
878
+ if (edgeError?.kind === "edge-convert") {
879
+ const edgeId = edgeError.edgeId;
880
+ setEdgeStatus((s) => ({
881
+ ...s,
882
+ [edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
883
+ }));
1282
884
  }
1283
- this.engine.on("value", (e) => this.emit("value", e));
1284
- this.engine.on("error", (e) => this.emit("error", e));
1285
- this.engine.on("invalidate", (e) => this.emit("invalidate", e));
1286
- this.engine.on("stats", (e) => this.emit("stats", e));
1287
- this.engine.launch();
1288
- this.runningKind = opts.engine;
1289
- this.emit("status", { running: true, engine: this.runningKind });
1290
- for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
1291
- for (const [handle, value] of Object.entries(map)) {
1292
- this.engine.setInput(nodeId, handle, value);
1293
- }
885
+ else if (nodeError?.nodeId) {
886
+ const nodeId = nodeError?.nodeId;
887
+ setNodeStatus((s) => ({
888
+ ...s,
889
+ [nodeId]: {
890
+ ...(s[nodeId] ?? {}),
891
+ lastError: nodeError?.err,
892
+ },
893
+ }));
1294
894
  }
1295
- return;
1296
- }
1297
- // Remote: build remotely then launch
1298
- void this.ensureRemote().then(async (rc) => {
1299
- await rc.runner.build(def);
1300
- const eng = rc.runner.getEngine();
1301
- if (!rc.listenersBound) {
1302
- eng.on("value", (e) => {
1303
- rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
1304
- io: e.io,
1305
- value: e.value,
1306
- });
1307
- this.emit("value", e);
895
+ return add("runner", "error")(e);
896
+ });
897
+ const off3 = runner.on("invalidate", (e) => {
898
+ if (e?.reason === "graph-updated") {
899
+ setNodeStatus((s) => {
900
+ const next = {};
901
+ for (const n of wb.export().nodes) {
902
+ next[n.nodeId] = { ...(s[n.nodeId] ?? {}), invalidated: true };
903
+ }
904
+ return next;
1308
905
  });
1309
- eng.on("error", (e) => this.emit("error", e));
1310
- eng.on("invalidate", (e) => this.emit("invalidate", e));
1311
- eng.on("stats", (e) => this.emit("stats", e));
1312
- rc.listenersBound = true;
1313
906
  }
1314
- this.engine = eng;
1315
- this.engine.launch();
1316
- this.runningKind = "push";
1317
- this.emit("status", { running: true, engine: this.runningKind });
1318
- for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
1319
- for (const [handle, value] of Object.entries(map)) {
1320
- this.engine.setInput(nodeId, handle, value);
1321
- }
907
+ return add("runner", "invalidate")(e);
908
+ });
909
+ const off3b = runner.on("stats", (s) => {
910
+ if (!s)
911
+ return;
912
+ if (s.kind === "node-start") {
913
+ const id = s.nodeId;
914
+ setNodeStatus((prev) => ({
915
+ ...prev,
916
+ [id]: {
917
+ ...(prev[id] ?? {}),
918
+ running: true,
919
+ progress: 0,
920
+ invalidated: false,
921
+ },
922
+ }));
923
+ }
924
+ else if (s.kind === "node-progress") {
925
+ const id = s.nodeId;
926
+ setNodeStatus((prev) => ({
927
+ ...prev,
928
+ [id]: {
929
+ ...(prev[id] ?? {}),
930
+ running: true,
931
+ progress: Number(s.progress) || 0,
932
+ },
933
+ }));
934
+ }
935
+ else if (s.kind === "node-done") {
936
+ const id = s.nodeId;
937
+ setNodeStatus((prev) => ({
938
+ ...prev,
939
+ [id]: { ...(prev[id] ?? {}), running: false },
940
+ }));
941
+ }
942
+ else if (s.kind === "edge-start") {
943
+ const id = s.edgeId;
944
+ setEdgeStatus((prev) => ({
945
+ ...prev,
946
+ [id]: { ...(prev[id] ?? {}), running: true },
947
+ }));
948
+ }
949
+ else if (s.kind === "edge-done") {
950
+ const id = s.edgeId;
951
+ setEdgeStatus((prev) => ({
952
+ ...prev,
953
+ [id]: { ...(prev[id] ?? {}), running: false },
954
+ }));
1322
955
  }
956
+ return add("runner", "stats")(s);
1323
957
  });
1324
- }
1325
- setInput(nodeId, handle, value) {
1326
- if (!this.stagedInputs[nodeId])
1327
- this.stagedInputs[nodeId] = {};
1328
- this.stagedInputs[nodeId][handle] = value;
1329
- if (this.engine)
1330
- this.engine.setInput(nodeId, handle, value);
1331
- }
1332
- async step() {
1333
- if (this.backend.kind !== "local")
1334
- return; // unsupported remotely
1335
- const eng = this.engine;
1336
- if (eng instanceof sparkGraph.StepEngine)
1337
- await eng.step();
1338
- }
1339
- async computeNode(nodeId) {
1340
- if (this.backend.kind !== "local")
1341
- return; // unsupported remotely
1342
- const eng = this.engine;
1343
- if (eng instanceof sparkGraph.PullEngine)
1344
- await eng.computeNode(nodeId);
1345
- }
1346
- flush() {
1347
- if (this.backend.kind !== "local")
1348
- return; // unsupported remotely
1349
- const eng = this.engine;
1350
- if (eng instanceof sparkGraph.BatchedEngine)
1351
- eng.flush();
1352
- }
1353
- getOutputs(def) {
1354
- const out = {};
1355
- if (this.backend.kind === "local") {
1356
- if (!this.runtime)
1357
- return out;
1358
- for (const n of def.nodes) {
1359
- const desc = this.registry.nodes.get(n.typeId);
1360
- const handles = Object.keys(desc?.outputs ?? {});
1361
- for (const h of handles) {
1362
- const v = this.runtime.getOutput(n.nodeId, h);
1363
- if (v !== undefined) {
1364
- if (!out[n.nodeId])
1365
- out[n.nodeId] = {};
1366
- out[n.nodeId][h] = v;
1367
- }
1368
- }
958
+ const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
959
+ const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
960
+ const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
961
+ const off5b = wb.on("validationChanged", (r) => setValidation(r));
962
+ const off6 = wb.on("selectionChanged", (sel) => {
963
+ setSelectedNodeId(sel.nodes?.[0]);
964
+ setSelectedEdgeId(sel.edges?.[0]);
965
+ });
966
+ const off7 = wb.on("error", add("workbench", "error"));
967
+ wb.refreshValidation();
968
+ return () => {
969
+ off1();
970
+ off2();
971
+ off3();
972
+ off3b();
973
+ off4();
974
+ off4b();
975
+ off5();
976
+ off5b();
977
+ off6();
978
+ off7();
979
+ };
980
+ }, [runner, wb]);
981
+ // Push incremental updates into running engine without full reload
982
+ React.useEffect(() => {
983
+ if (runner.isRunning()) {
984
+ try {
985
+ runner.update(def);
1369
986
  }
1370
- return out;
987
+ catch { }
1371
988
  }
1372
- const cache = this.remote?.valueCache;
1373
- if (!cache)
1374
- return out;
1375
- for (const n of def.nodes) {
1376
- const desc = this.registry.nodes.get(n.typeId);
1377
- const handles = Object.keys(desc?.outputs ?? {});
1378
- for (const h of handles) {
1379
- const key = `${n.nodeId}.${h}`;
1380
- const rec = cache.get(key);
1381
- if (rec && rec.io === "output") {
1382
- if (!out[n.nodeId])
1383
- out[n.nodeId] = {};
1384
- out[n.nodeId][h] = rec.value;
989
+ }, [runner, def, graphTick]);
990
+ const validationByNode = React.useMemo(() => {
991
+ const inputs = {};
992
+ const outputs = {};
993
+ const issues = {};
994
+ if (!validation)
995
+ return { inputs, outputs, issues };
996
+ for (const is of validation.issues ?? []) {
997
+ const d = is?.data;
998
+ const level = is?.level;
999
+ const code = String(is?.code ?? "");
1000
+ const message = String(is?.message ?? code);
1001
+ if (!d)
1002
+ continue;
1003
+ if (d.nodeId) {
1004
+ if (d.input) {
1005
+ const arr = inputs[d.nodeId] ?? (inputs[d.nodeId] = []);
1006
+ arr.push({ handle: String(d.input), level, message, code });
1007
+ const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
1008
+ nodeArr.push({ level, message, code });
1385
1009
  }
1386
- }
1387
- }
1388
- return out;
1389
- }
1390
- getInputs(def) {
1391
- const out = {};
1392
- if (this.backend.kind === "local") {
1393
- for (const n of def.nodes) {
1394
- const staged = this.stagedInputs[n.nodeId] ?? {};
1395
- const runtimeInputs = this.runtime
1396
- ? this.runtime.__unsafe_getNodeData?.(n.nodeId)?.inputs ?? {}
1397
- : {};
1398
- if (this.isRunning()) {
1399
- out[n.nodeId] = runtimeInputs;
1010
+ if (d.output) {
1011
+ const arr = outputs[d.nodeId] ?? (outputs[d.nodeId] = []);
1012
+ arr.push({ handle: String(d.output), level, message, code });
1013
+ const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
1014
+ nodeArr.push({ level, message, code });
1400
1015
  }
1401
- else {
1402
- const merged = { ...runtimeInputs, ...staged };
1403
- if (Object.keys(merged).length > 0)
1404
- out[n.nodeId] = merged;
1016
+ if (!d.input && !d.output) {
1017
+ const arr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
1018
+ arr.push({ level, message, code });
1405
1019
  }
1406
1020
  }
1407
- return out;
1408
1021
  }
1409
- const cache = this.remote?.valueCache;
1410
- for (const n of def.nodes) {
1411
- const staged = this.stagedInputs[n.nodeId] ?? {};
1412
- const desc = this.registry.nodes.get(n.typeId);
1413
- const handles = Object.keys(desc?.inputs ?? {});
1414
- const cur = {};
1415
- for (const h of handles) {
1416
- const rec = cache?.get(`${n.nodeId}.${h}`);
1417
- if (rec && rec.io === "input")
1418
- cur[h] = rec.value;
1022
+ return { inputs, outputs, issues };
1023
+ }, [validation]);
1024
+ const validationGlobal = React.useMemo(() => {
1025
+ const list = [];
1026
+ if (!validation)
1027
+ return list;
1028
+ for (const is of validation.issues ?? []) {
1029
+ const d = is?.data;
1030
+ const level = is?.level;
1031
+ const code = String(is?.code ?? "");
1032
+ const message = String(is?.message ?? code);
1033
+ if (!d || (!d.nodeId && !d.edgeId)) {
1034
+ list.push({ level, code, message });
1035
+ }
1036
+ }
1037
+ return list;
1038
+ }, [validation]);
1039
+ const validationByEdge = React.useMemo(() => {
1040
+ const errors = {};
1041
+ const issues = {};
1042
+ if (!validation)
1043
+ return { errors, issues };
1044
+ for (const is of validation.issues ?? []) {
1045
+ const d = is?.data;
1046
+ const level = is?.level;
1047
+ const code = String(is?.code ?? "");
1048
+ const message = String(is?.message ?? code);
1049
+ if (d?.edgeId) {
1050
+ if (level === "error")
1051
+ errors[d.edgeId] = true;
1052
+ const arr = issues[d.edgeId] ?? (issues[d.edgeId] = []);
1053
+ arr.push({ level, message, code });
1419
1054
  }
1420
- const merged = this.isRunning() ? cur : { ...cur, ...staged };
1421
- if (Object.keys(merged).length > 0)
1422
- out[n.nodeId] = merged;
1423
1055
  }
1424
- return out;
1425
- }
1426
- async whenIdle() {
1427
- await this.engine?.whenIdle();
1428
- }
1429
- on(event, handler) {
1430
- if (!this.listeners.has(event))
1431
- this.listeners.set(event, new Set());
1432
- const set = this.listeners.get(event);
1433
- set.add(handler);
1434
- return () => set.delete(handler);
1435
- }
1436
- emit(event, payload) {
1437
- const set = this.listeners.get(event);
1438
- if (set)
1439
- for (const h of Array.from(set))
1440
- h(payload);
1441
- }
1442
- dispose() {
1443
- this.engine?.dispose();
1444
- this.engine = undefined;
1445
- this.runtime?.dispose();
1446
- this.runtime = undefined;
1447
- this.remote = undefined;
1448
- if (this.runningKind) {
1449
- this.runningKind = undefined;
1450
- this.emit("status", { running: false, engine: undefined });
1056
+ return { errors, issues };
1057
+ }, [validation]);
1058
+ const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
1059
+ const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
1060
+ const start = React.useCallback((engine) => {
1061
+ try {
1062
+ runner.launch(wb.export(), { engine });
1451
1063
  }
1452
- }
1453
- isRunning() {
1454
- return !!this.engine;
1455
- }
1456
- getRunningEngine() {
1457
- return this.runningKind;
1458
- }
1459
- // Ensure remote transport/runner
1460
- async ensureRemote() {
1461
- if (this.remote)
1462
- return this.remote;
1463
- let transport;
1464
- if (this.backend.kind === "remote-http") {
1465
- if (!sparkRemote.HttpPollingTransport)
1466
- throw new Error("HttpPollingTransport not available");
1467
- transport = new sparkRemote.HttpPollingTransport(this.backend.baseUrl);
1468
- await transport.connect();
1064
+ catch { }
1065
+ }, [runner, wb]);
1066
+ const stop = React.useCallback(() => runner.dispose(), [runner]);
1067
+ const step = React.useCallback(() => runner.step(), [runner]);
1068
+ const flush = React.useCallback(() => runner.flush(), [runner]);
1069
+ const value = React.useMemo(() => ({
1070
+ wb,
1071
+ runner,
1072
+ registry,
1073
+ setRegistry,
1074
+ def,
1075
+ selectedNodeId,
1076
+ selectedEdgeId,
1077
+ setSelection,
1078
+ nodeStatus,
1079
+ edgeStatus,
1080
+ valuesTick,
1081
+ inputsMap,
1082
+ outputsMap,
1083
+ validationByNode,
1084
+ validationByEdge,
1085
+ validationGlobal,
1086
+ events,
1087
+ clearEvents,
1088
+ isRunning,
1089
+ engineKind,
1090
+ start,
1091
+ stop,
1092
+ step,
1093
+ flush,
1094
+ runAutoLayout,
1095
+ }), [
1096
+ wb,
1097
+ runner,
1098
+ registry,
1099
+ setRegistry,
1100
+ def,
1101
+ selectedNodeId,
1102
+ selectedEdgeId,
1103
+ setSelection,
1104
+ nodeStatus,
1105
+ edgeStatus,
1106
+ valuesTick,
1107
+ inputsMap,
1108
+ outputsMap,
1109
+ validationByNode,
1110
+ validationByEdge,
1111
+ validationGlobal,
1112
+ events,
1113
+ clearEvents,
1114
+ isRunning,
1115
+ engineKind,
1116
+ start,
1117
+ stop,
1118
+ step,
1119
+ flush,
1120
+ runAutoLayout,
1121
+ ]);
1122
+ return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
1123
+ }
1124
+
1125
+ function IssueBadge({ level, title, size = 12, className, }) {
1126
+ const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
1127
+ return (jsxRuntime.jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsxRuntime.jsx(react.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react.WarningCircleIcon, { size: size, weight: "fill" })) }));
1128
+ }
1129
+
1130
+ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
1131
+ const { events, clearEvents } = useWorkbenchContext();
1132
+ const scrollRef = React.useRef(null);
1133
+ const rows = React.useMemo(() => {
1134
+ const filtered = hideWorkbench
1135
+ ? events.filter((e) => e.source !== "workbench")
1136
+ : events;
1137
+ return filtered.slice().reverse();
1138
+ }, [events, hideWorkbench]);
1139
+ React.useEffect(() => {
1140
+ if (!autoScroll)
1141
+ return;
1142
+ const el = scrollRef.current;
1143
+ if (!el)
1144
+ return;
1145
+ el.scrollTop = el.scrollHeight;
1146
+ }, [rows, autoScroll]);
1147
+ const renderPayload = (v) => {
1148
+ try {
1149
+ return JSON.stringify(v, null, 0);
1469
1150
  }
1470
- else if (this.backend.kind === "remote-ws") {
1471
- if (!sparkRemote.WebSocketTransport)
1472
- throw new Error("WebSocketTransport not available");
1473
- transport = new sparkRemote.WebSocketTransport(this.backend.url);
1474
- await transport.connect();
1151
+ catch {
1152
+ return String(v);
1475
1153
  }
1476
- else {
1477
- throw new Error("Remote backend not configured");
1154
+ };
1155
+ return (jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Events" }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Hide workbench" })] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Auto scroll" })] }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev, idx) => (jsxRuntime.jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [jsxRuntime.jsx("span", { className: "w-8 shrink-0 text-right text-gray-500 select-none", children: idx + 1 }), jsxRuntime.jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsxRuntime.jsx("pre", { className: "m-0 whitespace-pre-wrap ml-10", children: renderPayload(ev.payload) })] }, `${ev.at}:${idx}`))) })] }));
1156
+ }
1157
+
1158
+ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, setInput, }) {
1159
+ const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
1160
+ const nodeValidationIssues = validationByNode.issues;
1161
+ const edgeValidationIssues = validationByEdge.issues;
1162
+ const nodeValidationHandles = validationByNode;
1163
+ const globalValidationIssues = validationGlobal;
1164
+ const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
1165
+ const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
1166
+ const selectedDesc = selectedNode
1167
+ ? registry.nodes.get(selectedNode.typeId)
1168
+ : undefined;
1169
+ const inputHandles = Object.keys(selectedDesc?.inputs ?? {});
1170
+ const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
1171
+ const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
1172
+ const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
1173
+ const selectedNodeStatus = selectedNodeId
1174
+ ? nodeStatus?.[selectedNodeId]
1175
+ : undefined;
1176
+ const selectedNodeValidation = selectedNodeId
1177
+ ? nodeValidationIssues?.[selectedNodeId] ?? []
1178
+ : [];
1179
+ const selectedEdgeValidation = selectedEdge
1180
+ ? edgeValidationIssues?.[selectedEdge.id] ?? []
1181
+ : [];
1182
+ const selectedNodeHandleValidation = selectedNodeId
1183
+ ? {
1184
+ inputs: nodeValidationHandles?.inputs?.[selectedNodeId] ?? [],
1185
+ outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
1186
+ }
1187
+ : { inputs: [], outputs: [] };
1188
+ // Local drafts and originals for commit-on-blur/enter behavior
1189
+ const [drafts, setDrafts] = React.useState({});
1190
+ const [originals, setOriginals] = React.useState({});
1191
+ // Initialize drafts from current inputs whenever selection or valuesTick change,
1192
+ // but do not clobber fields currently being edited (dirty drafts)
1193
+ React.useEffect(() => {
1194
+ const shallowEqual = (a, b) => {
1195
+ const ak = Object.keys(a);
1196
+ const bk = Object.keys(b);
1197
+ if (ak.length !== bk.length)
1198
+ return false;
1199
+ for (const k of ak)
1200
+ if (a[k] !== b[k])
1201
+ return false;
1202
+ return true;
1203
+ };
1204
+ if (!selectedNodeId) {
1205
+ if (Object.keys(drafts).length || Object.keys(originals).length) {
1206
+ setDrafts({});
1207
+ setOriginals({});
1208
+ }
1209
+ return;
1210
+ }
1211
+ const desc = selectedDesc;
1212
+ const handles = Object.keys(desc?.inputs ?? {});
1213
+ const nextDrafts = { ...drafts };
1214
+ const nextOriginals = { ...originals };
1215
+ for (const h of handles) {
1216
+ const typeId = desc?.inputs?.[h];
1217
+ const current = nodeInputs[h];
1218
+ const display = toString(typeId, current);
1219
+ const wasOriginal = originals[h];
1220
+ const isDirty = drafts[h] !== undefined &&
1221
+ wasOriginal !== undefined &&
1222
+ drafts[h] !== wasOriginal;
1223
+ if (!isDirty) {
1224
+ nextDrafts[h] = display;
1225
+ nextOriginals[h] = display;
1226
+ }
1478
1227
  }
1479
- const runner = new sparkRemote.RemoteRunner(transport);
1480
- this.remote = {
1481
- runner,
1482
- transport,
1483
- valueCache: new Map(),
1484
- listenersBound: false,
1228
+ if (!shallowEqual(drafts, nextDrafts))
1229
+ setDrafts(nextDrafts);
1230
+ if (!shallowEqual(originals, nextOriginals))
1231
+ setOriginals(nextOriginals);
1232
+ }, [selectedNodeId, selectedDesc, valuesTick]);
1233
+ const widthClass = debug ? "w-[480px]" : "w-[320px]";
1234
+ return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
1235
+ selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
1236
+ const typeId = (selectedDesc?.inputs ?? {})[h];
1237
+ const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
1238
+ e.target.handle === h);
1239
+ const commonProps = {
1240
+ style: { flex: 1 },
1241
+ disabled: isLinked,
1242
+ };
1243
+ const current = nodeInputs[h];
1244
+ const value = drafts[h] ?? toString(typeId, current);
1245
+ const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
1246
+ const commit = () => {
1247
+ const draft = drafts[h];
1248
+ if (draft === undefined)
1249
+ return;
1250
+ setInput(h, draft);
1251
+ setOriginals((o) => ({ ...o, [h]: draft }));
1252
+ };
1253
+ const revert = () => {
1254
+ const orig = originals[h] ?? toString(typeId, current);
1255
+ setDrafts((d) => ({ ...d, [h]: orig }));
1256
+ };
1257
+ const isEnum = typeId?.includes("enum:");
1258
+ const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
1259
+ const hasValidation = inIssues.length > 0;
1260
+ const hasErr = inIssues.some((m) => m.level === "error");
1261
+ const title = inIssues
1262
+ .map((v) => `${v.code}: ${v.message}`)
1263
+ .join("; ");
1264
+ return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-28", children: [h, jsxRuntime.jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: drafts[h] ?? toString(typeId, current), onChange: (e) => {
1265
+ const label = String(e.target.value);
1266
+ const byLabel = registry.enums
1267
+ .get(typeId)
1268
+ ?.labelToValue.get(label.toLowerCase());
1269
+ let raw = (byLabel !== undefined ? byLabel : Number(label));
1270
+ if (!Number.isFinite(raw))
1271
+ raw = undefined;
1272
+ setInput(h, raw);
1273
+ const display = toString(typeId, raw);
1274
+ setDrafts((d) => ({ ...d, [h]: display }));
1275
+ setOriginals((o) => ({ ...o, [h]: display }));
1276
+ }, ...commonProps, children: [jsxRuntime.jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsxRuntime.jsx("option", { value: opt.label, children: opt.label }, opt.value)))] })) : (jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", placeholder: isLinked ? "wired" : undefined, value: value, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
1277
+ if (e.key === "Enter")
1278
+ commit();
1279
+ if (e.key === "Escape")
1280
+ revert();
1281
+ }, ...commonProps }))] }, h));
1282
+ }))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsx("label", { className: "w-20", children: h }), jsxRuntime.jsx("div", { className: "flex-1", children: toElement(selectedDesc?.outputs?.[h], nodeOutputs[h]) }), (() => {
1283
+ const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
1284
+ if (outIssues.length === 0)
1285
+ return null;
1286
+ const outErr = outIssues.some((m) => m.level === "error");
1287
+ const outTitle = outIssues
1288
+ .map((v) => `${v.code}: ${v.message}`)
1289
+ .join("; ");
1290
+ return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
1291
+ })()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, 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 }) }))] }));
1292
+ }
1293
+
1294
+ function toReactFlow(def, positions, registry, selectedNodeIds, selectedEdgeIds, opts) {
1295
+ const nodeHandleMap = {};
1296
+ const nodes = def.nodes.map((n) => {
1297
+ const desc = registry.nodes.get(n.typeId);
1298
+ const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
1299
+ const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
1300
+ nodeHandleMap[n.nodeId] = {
1301
+ inputs: new Set(inputHandles.map((h) => h.id)),
1302
+ outputs: new Set(outputHandles.map((h) => h.id)),
1485
1303
  };
1486
- return this.remote;
1487
- }
1304
+ return {
1305
+ id: n.nodeId,
1306
+ data: {
1307
+ typeId: n.typeId,
1308
+ params: n.params,
1309
+ inputHandles,
1310
+ outputHandles,
1311
+ showValues: opts?.showValues,
1312
+ inputValues: opts?.inputs?.[n.nodeId],
1313
+ outputValues: opts?.outputs?.[n.nodeId],
1314
+ status: opts?.nodeStatus?.[n.nodeId],
1315
+ validation: {
1316
+ inputs: opts?.nodeValidation?.inputs?.[n.nodeId] ?? [],
1317
+ outputs: opts?.nodeValidation?.outputs?.[n.nodeId] ?? [],
1318
+ issues: opts?.nodeValidation?.issues?.[n.nodeId] ?? [],
1319
+ },
1320
+ toString: opts?.toString,
1321
+ toElement: opts?.toElement,
1322
+ },
1323
+ position: positions[n.nodeId] ?? { x: 0, y: 0 },
1324
+ type: opts?.resolveNodeType?.(n.typeId) ?? "spark:default",
1325
+ selected: selectedNodeIds ? selectedNodeIds.has(n.nodeId) : undefined,
1326
+ };
1327
+ });
1328
+ const edges = def.edges
1329
+ .filter((e) => {
1330
+ const src = nodeHandleMap[e.source.nodeId];
1331
+ const dst = nodeHandleMap[e.target.nodeId];
1332
+ if (!src || !dst)
1333
+ return false;
1334
+ return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
1335
+ })
1336
+ .map((e) => {
1337
+ const st = opts?.edgeStatus?.[e.id];
1338
+ const isRunning = !!st?.running;
1339
+ const hasError = !!st?.lastError;
1340
+ const isInvalidEdge = !!opts?.edgeValidation?.[e.id];
1341
+ const style = hasError || isInvalidEdge
1342
+ ? { stroke: "#ef4444", strokeWidth: 2 }
1343
+ : isRunning
1344
+ ? { stroke: "#3b82f6" }
1345
+ : undefined;
1346
+ return {
1347
+ id: e.id,
1348
+ source: e.source.nodeId,
1349
+ target: e.target.nodeId,
1350
+ sourceHandle: e.source.handle,
1351
+ targetHandle: e.target.handle,
1352
+ selected: selectedEdgeIds ? selectedEdgeIds.has(e.id) : undefined,
1353
+ animated: isRunning,
1354
+ style,
1355
+ };
1356
+ });
1357
+ return { nodes, edges };
1358
+ }
1359
+
1360
+ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
1361
+ const { typeId, showValues, inputValues, outputValues, toString, toElement, } = data;
1362
+ const inputEntries = data.inputHandles ?? [];
1363
+ const outputEntries = data.outputHandles ?? [];
1364
+ const status = data.status ?? {};
1365
+ const validation = data.validation ?? {
1366
+ inputs: [],
1367
+ outputs: [],
1368
+ issues: [],
1369
+ };
1370
+ const HEADER_SIZE = 24;
1371
+ const ROW_SIZE = 22;
1372
+ const maxRows = Math.max(inputEntries.length, outputEntries.length);
1373
+ const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
1374
+ const minWidth = data.showValues ? 320 : 160;
1375
+ const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
1376
+ const hasError = !!status.lastError;
1377
+ const isRunning = !!status.running;
1378
+ const isInvalid = !!status.invalidated && !isRunning && !hasError;
1379
+ const borderClasses = selected
1380
+ ? "border-2 border-gray-900 dark:border-gray-100"
1381
+ : hasError
1382
+ ? "border-2 border-red-500"
1383
+ : isRunning
1384
+ ? "border-2 border-blue-500 ring-2 ring-blue-200 dark:ring-blue-900"
1385
+ : isInvalid
1386
+ ? "border-2 border-amber-500 border-dashed"
1387
+ : "border border-gray-500 dark:border-gray-400";
1388
+ const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
1389
+ return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900 border-solid", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [jsxRuntime.jsxs("div", { className: "flex h-6 items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", children: [jsxRuntime.jsx("strong", { className: "flex-1 h-full leading-6 text-xs", children: typeId }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsxRuntime.jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsxRuntime.jsx(react.XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
1390
+ ? "error"
1391
+ : "warning", size: 12, className: "w-3 h-3", title: validation.issues
1392
+ .map((v) => `${v.code}: ${v.message}`)
1393
+ .join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), (isRunning || pct > 0) && (jsxRuntime.jsx("div", { className: "h-1 bg-blue-200 dark:bg-blue-900", children: jsxRuntime.jsx("div", { className: "h-1 bg-blue-500 transition-all", style: { width: `${pct}%` } }) })), inputEntries.map((entry, i) => {
1394
+ const vIssues = validation.inputs.filter((v) => v.handle === entry.id);
1395
+ const hasAny = vIssues.length > 0;
1396
+ const hasErr = vIssues.some((v) => v.level === "error");
1397
+ const title = vIssues
1398
+ .map((v) => `${v.code}: ${v.message}`)
1399
+ .join("; ");
1400
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8 }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues &&
1401
+ (toElement ? (toElement(entry.typeId, inputValues?.[entry.id])) : toString ? (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) })) : (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: String(inputValues?.[entry.id]) })))] })] }, `in-${entry.id}`));
1402
+ }), outputEntries.map((entry, i) => {
1403
+ const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
1404
+ const hasAny = vIssues.length > 0;
1405
+ const hasErr = vIssues.some((v) => v.level === "error");
1406
+ const title = vIssues
1407
+ .map((v) => `${v.code}: ${v.message}`)
1408
+ .join("; ");
1409
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8, textAlign: "right" }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues &&
1410
+ (toElement ? (toElement(entry.typeId, outputValues?.[entry.id])) : toString ? (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, outputValues?.[entry.id]) })) : (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: String(outputValues?.[entry.id]) })))] })] }, `out-${entry.id}`));
1411
+ })] }));
1412
+ });
1413
+ DefaultNode.displayName = "DefaultNode";
1414
+
1415
+ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
1416
+ const { registry } = useWorkbenchContext();
1417
+ const rf = ReactFlow.useReactFlow();
1418
+ if (!open || !clientPos)
1419
+ return null;
1420
+ const items = Array.from(registry.nodes.keys());
1421
+ const handleClick = (typeId) => {
1422
+ const p = rf.project({ x: clientPos.x, y: clientPos.y });
1423
+ onAdd(typeId, p);
1424
+ onClose();
1425
+ };
1426
+ return (jsxRuntime.jsxs("div", { className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: clientPos.x, top: clientPos.y }, onMouseLeave: onClose, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Add Node" }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: items.map((id) => (jsxRuntime.jsx("button", { onClick: () => handleClick(id), className: "block w-full text-left px-2 py-1 hover:bg-gray-100 cursor-pointer", children: id }, id))) })] }));
1427
+ }
1428
+
1429
+ function WorkbenchCanvas({ showValues, toString, toElement, }) {
1430
+ const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
1431
+ const ioValues = { inputs: inputsMap, outputs: outputsMap };
1432
+ const nodeValidation = validationByNode;
1433
+ const edgeValidation = validationByEdge.errors;
1434
+ const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
1435
+ const { nodeTypes, resolveNodeType } = React.useMemo(() => {
1436
+ // Build nodeTypes map using UI extension registry
1437
+ const ui = wb.getUI();
1438
+ const custom = new Map();
1439
+ for (const typeId of Array.from(registry.nodes.keys())) {
1440
+ const renderer = ui.getNodeRenderer(typeId);
1441
+ if (renderer)
1442
+ custom.set(typeId, renderer);
1443
+ }
1444
+ const types = { "spark:default": DefaultNode };
1445
+ for (const [typeId, comp] of custom.entries()) {
1446
+ types[`spark:${typeId}`] = comp;
1447
+ }
1448
+ const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark:${nodeTypeId}` : "spark:default";
1449
+ return { nodeTypes: types, resolveNodeType: resolver };
1450
+ // registry is stable; ui renderers expected to be set up before mount
1451
+ }, [wb, registry]);
1452
+ const { nodes, edges } = React.useMemo(() => {
1453
+ const def = wb.export();
1454
+ const sel = wb.getSelection();
1455
+ return toReactFlow(def, wb.getPositions(), registry, new Set(sel.nodes), new Set(sel.edges), {
1456
+ showValues,
1457
+ inputs: ioValues.inputs,
1458
+ outputs: ioValues.outputs,
1459
+ resolveNodeType,
1460
+ toString,
1461
+ toElement,
1462
+ nodeStatus,
1463
+ edgeStatus,
1464
+ nodeValidation,
1465
+ edgeValidation,
1466
+ });
1467
+ }, [
1468
+ showValues,
1469
+ ioValues,
1470
+ valuesTick,
1471
+ toString,
1472
+ toElement,
1473
+ nodeStatus,
1474
+ edgeStatus,
1475
+ nodeValidation,
1476
+ edgeValidation,
1477
+ ]);
1478
+ const [menuOpen, setMenuOpen] = React.useState(false);
1479
+ const [menuPos, setMenuPos] = React.useState(null);
1480
+ const onContextMenu = (e) => {
1481
+ e.preventDefault();
1482
+ setMenuPos({ x: e.clientX, y: e.clientY });
1483
+ setMenuOpen(true);
1484
+ };
1485
+ const addNodeAt = (typeId, pos) => {
1486
+ wb.addNode({ typeId, position: pos });
1487
+ };
1488
+ return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onSelectionChange: onSelectionChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, children: [jsxRuntime.jsx(ReactFlow.Background, {}), jsxRuntime.jsx(ReactFlow.MiniMap, {}), jsxRuntime.jsx(ReactFlow.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) })] }) }));
1488
1489
  }
1489
1490
 
1490
- function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, }) {
1491
+ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
1491
1492
  const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
1492
1493
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
1493
1494
  const selectedDesc = selectedNode
@@ -1653,7 +1654,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1653
1654
  runAutoLayout();
1654
1655
  }
1655
1656
  }, [wb, runAutoLayout]);
1656
- const setInput = React.useCallback((handle, raw) => {
1657
+ const baseSetInput = React.useCallback((handle, raw) => {
1657
1658
  if (!selectedNodeId)
1658
1659
  return;
1659
1660
  // If selected input is wired (has inbound edge), ignore user input to respect runtime value
@@ -1734,7 +1735,17 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1734
1735
  }
1735
1736
  runner.setInput(selectedNodeId, handle, value);
1736
1737
  }, [selectedNodeId, def.edges, selectedDesc, runner]);
1737
- const toDisplay = React.useCallback((typeId, value) => {
1738
+ const setInput = React.useMemo(() => {
1739
+ if (overrides?.setInput) {
1740
+ return overrides.setInput(baseSetInput, {
1741
+ runner,
1742
+ selectedNodeId,
1743
+ registry,
1744
+ });
1745
+ }
1746
+ return baseSetInput;
1747
+ }, [overrides, baseSetInput, runner, selectedNodeId, registry]);
1748
+ const baseToString = React.useCallback((typeId, value) => {
1738
1749
  if (value === undefined || value === null)
1739
1750
  return "";
1740
1751
  if (typeId && typeId.includes("enum:")) {
@@ -1766,6 +1777,21 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1766
1777
  }
1767
1778
  return String(value);
1768
1779
  }, [registry]);
1780
+ const baseToElement = React.useCallback((typeId, value) => {
1781
+ return jsxRuntime.jsx("span", { children: baseToString(typeId, value) });
1782
+ }, [baseToString]);
1783
+ const toString = React.useMemo(() => {
1784
+ if (overrides?.toString)
1785
+ return overrides.toString(baseToString, { registry });
1786
+ return baseToString;
1787
+ }, [overrides, baseToString, registry]);
1788
+ // Optional: toElement (not currently consumed by core UI)
1789
+ // Consumers can access it by passing through their own node renderers.
1790
+ const toElement = React.useMemo(() => {
1791
+ if (overrides?.toElement)
1792
+ return overrides.toElement(baseToElement, { registry });
1793
+ return baseToElement;
1794
+ }, [overrides, baseToElement, registry]);
1769
1795
  return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
1770
1796
  ? "Stop engine before switching example"
1771
1797
  : undefined, children: [jsxRuntime.jsx("option", { value: "simple", children: "Simple" }), jsxRuntime.jsx("option", { value: "async", children: "Async Chain" }), jsxRuntime.jsx("option", { value: "progress", children: "Progress + Errors" }), jsxRuntime.jsx("option", { value: "validation", children: "Validation" })] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
@@ -1783,9 +1809,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1783
1809
  catch (err) {
1784
1810
  alert(String(err?.message ?? err));
1785
1811
  }
1786
- }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), 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, { showValues: showValues, toDisplay: toDisplay }, exampleState) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toDisplay: toDisplay })] })] }));
1812
+ }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), 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, { showValues: showValues, toString: toString, toElement: toElement }, exampleState) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
1787
1813
  }
1788
- function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, }) {
1814
+ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
1789
1815
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
1790
1816
  const [wb] = React.useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
1791
1817
  const runner = React.useMemo(() => {
@@ -1796,35 +1822,36 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
1796
1822
  : { kind: "local" };
1797
1823
  return new GraphRunner(registry, backend);
1798
1824
  }, [registry, backendKind, httpBaseUrl, wsUrl]);
1825
+ // Allow external UI registration (e.g., node renderers) with access to wb
1826
+ React.useEffect(() => {
1827
+ const baseRegisterUI = (_wb) => { };
1828
+ overrides?.registerUI?.(baseRegisterUI, { wb });
1829
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1830
+ }, [wb, overrides]);
1799
1831
  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) => {
1800
1832
  if (runner.isRunning())
1801
1833
  runner.dispose();
1802
1834
  onBackendKindChange(v);
1803
- }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange }) }));
1804
- }
1805
-
1806
- function App() {
1807
- const [engine, setEngine] = useQueryParamString("engine", "");
1808
- const [example, setExample] = useQueryParamString("example", "simple");
1809
- const [debug, setDebug] = useQueryParamBoolean("debug", false);
1810
- const [showValues, setShowValues] = useQueryParamBoolean("values", false);
1811
- const [hideWorkbench, setHideWorkbench] = useQueryParamBoolean("hideWb", false);
1812
- const [autoScroll, setAutoScroll] = useQueryParamBoolean("autoScroll", true);
1813
- // Backend selection via URL params
1814
- const [backendKind, setBackendKind] = useQueryParamString("backend", "local");
1815
- const [httpBaseUrl, setHttpBaseUrl] = useQueryParamString("sparkHttp", "http://127.0.0.1:18080");
1816
- const [wsUrl, setWsUrl] = useQueryParamString("sparkWs", "ws://127.0.0.1:18081");
1817
- React.useEffect(() => {
1818
- document.getElementById("loading-screen")?.remove();
1819
- }, []);
1820
- return (jsxRuntime.jsx(WorkbenchStudio, { engine: engine, onEngineChange: setEngine, example: example, onExampleChange: setExample, backendKind: (backendKind || "local"), onBackendKindChange: (v) => setBackendKind(v), httpBaseUrl: httpBaseUrl || "http://127.0.0.1:18080", onHttpBaseUrlChange: setHttpBaseUrl, wsUrl: wsUrl || "ws://127.0.0.1:18081", onWsUrlChange: setWsUrl, debug: debug, onDebugChange: setDebug, showValues: showValues, onShowValuesChange: setShowValues, hideWorkbench: hideWorkbench, onHideWorkbenchChange: setHideWorkbench, autoScroll: autoScroll, onAutoScrollChange: setAutoScroll }));
1835
+ }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides }) }));
1821
1836
  }
1822
1837
 
1823
1838
  exports.AbstractWorkbench = AbstractWorkbench;
1824
- exports.App = App;
1825
1839
  exports.CLIWorkbench = CLIWorkbench;
1826
1840
  exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
1841
+ exports.GraphRunner = GraphRunner;
1827
1842
  exports.InMemoryWorkbench = InMemoryWorkbench;
1843
+ exports.Inspector = Inspector;
1828
1844
  exports.ReactFlowWorkbench = ReactFlowWorkbench;
1845
+ exports.WorkbenchCanvas = WorkbenchCanvas;
1846
+ exports.WorkbenchContext = WorkbenchContext;
1847
+ exports.WorkbenchProvider = WorkbenchProvider;
1848
+ exports.WorkbenchStudio = WorkbenchStudio;
1829
1849
  exports.toReactFlow = toReactFlow$1;
1850
+ exports.useQueryParamBoolean = useQueryParamBoolean;
1851
+ exports.useQueryParamString = useQueryParamString;
1852
+ exports.useWorkbenchBridge = useWorkbenchBridge;
1853
+ exports.useWorkbenchContext = useWorkbenchContext;
1854
+ exports.useWorkbenchGraphTick = useWorkbenchGraphTick;
1855
+ exports.useWorkbenchGraphUiTick = useWorkbenchGraphUiTick;
1856
+ exports.useWorkbenchVersionTick = useWorkbenchVersionTick;
1830
1857
  //# sourceMappingURL=index.cjs.map