@bian-womp/spark-workbench 0.2.37 → 0.2.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cjs/index.cjs CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  var sparkGraph = require('@bian-womp/spark-graph');
4
4
  var sparkRemote = require('@bian-womp/spark-remote');
5
- var React = require('react');
6
5
  var react = require('@xyflow/react');
6
+ var React = require('react');
7
+ var cx = require('classnames');
7
8
  var jsxRuntime = require('react/jsx-runtime');
8
9
  var react$1 = require('@phosphor-icons/react');
9
- var cx = require('classnames');
10
10
  var isEqual = require('lodash/isEqual');
11
11
 
12
12
  class DefaultUIExtensionRegistry {
@@ -602,8 +602,200 @@ class LocalGraphRunner extends AbstractGraphRunner {
602
602
  }
603
603
 
604
604
  class RemoteGraphRunner extends AbstractGraphRunner {
605
+ /**
606
+ * Fetch full registry description from remote and register it locally.
607
+ * Called automatically on first connection with retry mechanism.
608
+ */
609
+ async fetchRegistry(client, attempt = 1) {
610
+ if (this.registryFetching) {
611
+ // Already fetching, don't start another fetch
612
+ return;
613
+ }
614
+ this.registryFetching = true;
615
+ try {
616
+ const desc = await client.describeRegistry();
617
+ // Register types
618
+ for (const t of desc.types) {
619
+ if (t.options) {
620
+ this.registry.registerEnum({
621
+ id: t.id,
622
+ options: t.options,
623
+ bakeTarget: t.bakeTarget,
624
+ });
625
+ }
626
+ else {
627
+ if (!this.registry.types.has(t.id)) {
628
+ this.registry.registerType({
629
+ id: t.id,
630
+ displayName: t.displayName,
631
+ validate: (_v) => true,
632
+ bakeTarget: t.bakeTarget,
633
+ });
634
+ }
635
+ }
636
+ }
637
+ // Register categories
638
+ for (const c of desc.categories || []) {
639
+ if (!this.registry.categories.has(c.id)) {
640
+ // Create placeholder category descriptor
641
+ const category = {
642
+ id: c.id,
643
+ displayName: c.displayName,
644
+ createRuntime: () => ({
645
+ async onInputsChanged() { },
646
+ }),
647
+ policy: { asyncConcurrency: "switch" },
648
+ };
649
+ this.registry.categories.register(category);
650
+ }
651
+ }
652
+ // Register coercions
653
+ for (const c of desc.coercions) {
654
+ if (c.async) {
655
+ this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
656
+ nonTransitive: c.nonTransitive,
657
+ });
658
+ }
659
+ else {
660
+ this.registry.registerCoercion(c.from, c.to, (v) => v, {
661
+ nonTransitive: c.nonTransitive,
662
+ });
663
+ }
664
+ }
665
+ // Register nodes
666
+ for (const n of desc.nodes) {
667
+ if (!this.registry.nodes.has(n.id)) {
668
+ this.registry.registerNode({
669
+ id: n.id,
670
+ categoryId: n.categoryId,
671
+ displayName: n.displayName,
672
+ inputs: n.inputs || {},
673
+ outputs: n.outputs || {},
674
+ impl: () => { },
675
+ });
676
+ }
677
+ }
678
+ this.registryFetched = true;
679
+ this.registryFetching = false;
680
+ this.emit("registry", this.registry);
681
+ }
682
+ catch (err) {
683
+ this.registryFetching = false;
684
+ const error = err instanceof Error ? err : new Error(String(err));
685
+ // Retry with exponential backoff if attempts remaining
686
+ if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
687
+ const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
688
+ console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, error);
689
+ // Emit error event for UI feedback
690
+ this.emit("error", {
691
+ kind: "registry",
692
+ message: `Registry fetch failed (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying...`,
693
+ err: error,
694
+ attempt,
695
+ maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
696
+ });
697
+ // Retry after delay
698
+ setTimeout(() => {
699
+ this.fetchRegistry(client, attempt + 1).catch(() => {
700
+ // Final failure handled below
701
+ });
702
+ }, delayMs);
703
+ }
704
+ else {
705
+ // Max attempts reached, emit final error
706
+ console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, error);
707
+ this.emit("error", {
708
+ kind: "registry",
709
+ message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
710
+ err: error,
711
+ attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
712
+ maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
713
+ });
714
+ }
715
+ }
716
+ }
717
+ /**
718
+ * Build RuntimeApiClient config from RemoteExecutionBackend config.
719
+ */
720
+ buildClientConfig(backend) {
721
+ if (backend.kind === "remote-http") {
722
+ return {
723
+ kind: "remote-http",
724
+ baseUrl: backend.baseUrl,
725
+ connectOptions: backend.connectOptions,
726
+ };
727
+ }
728
+ else {
729
+ return {
730
+ kind: "remote-ws",
731
+ url: backend.url,
732
+ connectOptions: backend.connectOptions,
733
+ };
734
+ }
735
+ }
736
+ /**
737
+ * Setup event subscriptions for the client.
738
+ */
739
+ setupClientSubscriptions(client, backend) {
740
+ // Subscribe to custom events (for additional listeners if needed)
741
+ if (backend.onCustomEvent) {
742
+ this.customEventUnsubscribe = client.subscribeCustomEvents(backend.onCustomEvent);
743
+ }
744
+ // Subscribe to transport status changes
745
+ // Convert RuntimeApiClient.TransportStatus to IGraphRunner.TransportStatus
746
+ this.transportStatusUnsubscribe = client.onTransportStatus((status) => {
747
+ // Map remote-unix to undefined since RemoteGraphRunner doesn't support it
748
+ const mappedKind = status.kind === "remote-unix" ? undefined : status.kind;
749
+ this.emit("transport", {
750
+ state: status.state,
751
+ kind: mappedKind,
752
+ });
753
+ });
754
+ }
755
+ // Ensure remote client
756
+ async ensureClient() {
757
+ if (this.disposed) {
758
+ throw new Error("Cannot ensure client: RemoteGraphRunner has been disposed");
759
+ }
760
+ if (this.client)
761
+ return this.client;
762
+ // If already connecting, wait for that connection to complete
763
+ if (this.clientPromise)
764
+ return this.clientPromise;
765
+ const backend = this.backend;
766
+ // Create connection promise to prevent concurrent connections
767
+ this.clientPromise = (async () => {
768
+ // Build client config from backend config
769
+ const clientConfig = this.buildClientConfig(backend);
770
+ // Create client with custom event handler if provided
771
+ const client = new sparkRemote.RuntimeApiClient(clientConfig, {
772
+ onCustomEvent: backend.onCustomEvent,
773
+ });
774
+ // Setup event subscriptions
775
+ this.setupClientSubscriptions(client, backend);
776
+ // Connect the client (this will create and connect transport)
777
+ await client.connect();
778
+ this.client = client;
779
+ this.valueCache.clear();
780
+ this.listenersBound = false;
781
+ // Auto-fetch registry on first connection (only once)
782
+ if (!this.registryFetched && !this.registryFetching) {
783
+ // Log loading state (UI can listen to transport status for loading indication)
784
+ console.info("Loading registry from remote...");
785
+ this.fetchRegistry(client).catch((err) => {
786
+ console.error("[RemoteGraphRunner] Failed to fetch registry:", err);
787
+ // Error handling is done inside fetchRegistry, but we catch unhandled rejections
788
+ });
789
+ }
790
+ // Clear promise on success
791
+ this.clientPromise = undefined;
792
+ return client;
793
+ })();
794
+ return this.clientPromise;
795
+ }
605
796
  constructor(registry, backend) {
606
797
  super(registry, backend);
798
+ this.disposed = false;
607
799
  this.valueCache = new Map();
608
800
  this.listenersBound = false;
609
801
  this.registryFetched = false;
@@ -612,8 +804,8 @@ class RemoteGraphRunner extends AbstractGraphRunner {
612
804
  this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
613
805
  // Auto-handle registry-changed invalidations from remote
614
806
  // We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
615
- this.ensureRemoteRunner().then(async (runner) => {
616
- const eng = runner.getEngine();
807
+ this.ensureClient().then(async (client) => {
808
+ const eng = client.getEngine();
617
809
  if (!this.listenersBound) {
618
810
  eng.on("invalidate", async (e) => {
619
811
  if (e.reason === "registry-changed") {
@@ -672,14 +864,12 @@ class RemoteGraphRunner extends AbstractGraphRunner {
672
864
  }
673
865
  });
674
866
  }
675
- build(def) {
676
- console.warn("Unsupported operation for remote runner");
677
- }
867
+ build(def) { }
678
868
  update(def) {
679
869
  // Remote: forward update; ignore errors (fire-and-forget)
680
- this.ensureRemoteRunner().then(async (runner) => {
870
+ this.ensureClient().then(async (client) => {
681
871
  try {
682
- await runner.update(def);
872
+ await client.update(def);
683
873
  this.emit("invalidate", { reason: "graph-updated" });
684
874
  this.lastDef = def;
685
875
  }
@@ -689,14 +879,14 @@ class RemoteGraphRunner extends AbstractGraphRunner {
689
879
  launch(def, opts) {
690
880
  super.launch(def, opts);
691
881
  // Remote: build remotely then launch
692
- this.ensureRemoteRunner().then(async (runner) => {
693
- await runner.build(def);
882
+ this.ensureClient().then(async (client) => {
883
+ await client.build(def);
694
884
  // Signal UI after remote build as well
695
885
  this.emit("invalidate", { reason: "graph-built" });
696
886
  this.lastDef = def;
697
887
  // Hydrate current remote inputs/outputs (including defaults) into cache
698
888
  try {
699
- const snap = await runner.snapshot();
889
+ const snap = await client.snapshot();
700
890
  for (const [nodeId, map] of Object.entries(snap.inputs || {})) {
701
891
  for (const [handle, value] of Object.entries(map || {})) {
702
892
  this.valueCache.set(`${nodeId}.${handle}`, {
@@ -719,7 +909,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
719
909
  catch {
720
910
  console.error("Failed to hydrate remote inputs/outputs");
721
911
  }
722
- const eng = runner.getEngine();
912
+ const eng = client.getEngine();
723
913
  if (!this.listenersBound) {
724
914
  eng.on("value", (e) => {
725
915
  this.valueCache.set(`${e.nodeId}.${e.handle}`, {
@@ -743,36 +933,70 @@ class RemoteGraphRunner extends AbstractGraphRunner {
743
933
  }
744
934
  });
745
935
  }
746
- async step() {
747
- console.warn("Unsupported operation for remote runner");
748
- }
749
- async computeNode(nodeId) {
750
- console.warn("Unsupported operation for remote runner");
751
- }
752
- flush() {
753
- console.warn("Unsupported operation for remote runner");
936
+ /**
937
+ * Launch using an existing backend runtime that has already been built and hydrated.
938
+ * This is used when resuming from a snapshot where the backend has already applied
939
+ * ApplySnapshotFull (which builds the graph and hydrates inputs/outputs).
940
+ * Unlike launch(), this method does NOT call client.build() to avoid destroying
941
+ * the runtime state that was just restored.
942
+ */
943
+ launchExisting(def, opts) {
944
+ super.launch(def, opts);
945
+ // Remote: attach to existing runtime and launch (do NOT rebuild)
946
+ this.ensureClient().then(async (client) => {
947
+ // NOTE: We do NOT call client.build(def) here because the backend runtime
948
+ // has already been built and hydrated via ApplySnapshotFull.
949
+ // Calling build() would create a new runtime and lose the restored state.
950
+ this.lastDef = def;
951
+ // Attach to the existing engine
952
+ const eng = client.getEngine();
953
+ if (!this.listenersBound) {
954
+ eng.on("value", (e) => {
955
+ this.valueCache.set(`${e.nodeId}.${e.handle}`, {
956
+ io: e.io,
957
+ value: e.value,
958
+ runtimeTypeId: e.runtimeTypeId,
959
+ });
960
+ this.emit("value", e);
961
+ });
962
+ eng.on("error", (e) => this.emit("error", e));
963
+ eng.on("invalidate", (e) => this.emit("invalidate", e));
964
+ eng.on("stats", (e) => this.emit("stats", e));
965
+ this.listenersBound = true;
966
+ }
967
+ this.engine = eng;
968
+ this.engine.launch(opts.invalidate);
969
+ this.runningKind = "push";
970
+ this.emit("status", { running: true, engine: this.runningKind });
971
+ for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
972
+ this.engine.setInputs(nodeId, map);
973
+ }
974
+ });
754
975
  }
976
+ async step() { }
977
+ async computeNode(nodeId) { }
978
+ flush() { }
755
979
  triggerExternal(nodeId, event) {
756
- this.ensureRemoteRunner().then(async (runner) => {
980
+ this.ensureClient().then(async (client) => {
757
981
  try {
758
- await runner.getEngine().triggerExternal(nodeId, event);
982
+ await client.getEngine().triggerExternal(nodeId, event);
759
983
  }
760
984
  catch { }
761
985
  });
762
986
  }
763
987
  async coerce(from, to, value) {
764
- const runner = await this.ensureRemoteRunner();
988
+ const client = await this.ensureClient();
765
989
  try {
766
- return await runner.coerce(from, to, value);
990
+ return await client.coerce(from, to, value);
767
991
  }
768
992
  catch {
769
993
  return value;
770
994
  }
771
995
  }
772
996
  async snapshotFull() {
773
- const runner = await this.ensureRemoteRunner();
997
+ const client = await this.ensureClient();
774
998
  try {
775
- return await runner.snapshotFull();
999
+ return await client.snapshotFull();
776
1000
  }
777
1001
  catch {
778
1002
  return { def: undefined, environment: {}, inputs: {}, outputs: {} };
@@ -780,17 +1004,17 @@ class RemoteGraphRunner extends AbstractGraphRunner {
780
1004
  }
781
1005
  async applySnapshotFull(payload) {
782
1006
  // Hydrate local cache first so UI can display values immediately
783
- this.hydrateCacheFromSnapshot(payload);
1007
+ this.hydrateSnapshotFull(payload);
784
1008
  // Then sync with backend
785
- const runner = await this.ensureRemoteRunner();
786
- await runner.applySnapshotFull(payload);
1009
+ const client = await this.ensureClient();
1010
+ await client.applySnapshotFull(payload);
787
1011
  }
788
1012
  /**
789
1013
  * Hydrates the local valueCache from a snapshot and emits value events.
790
1014
  * This ensures the UI can display inputs/outputs immediately without waiting
791
1015
  * for value events from the remote backend.
792
1016
  */
793
- hydrateCacheFromSnapshot(snapshot) {
1017
+ hydrateSnapshotFull(snapshot) {
794
1018
  // Hydrate inputs
795
1019
  for (const [nodeId, map] of Object.entries(snapshot.inputs || {})) {
796
1020
  for (const [handle, value] of Object.entries(map || {})) {
@@ -813,20 +1037,25 @@ class RemoteGraphRunner extends AbstractGraphRunner {
813
1037
  }
814
1038
  }
815
1039
  setEnvironment(env, opts) {
816
- const t = this.transport;
817
- if (!t)
818
- return;
819
- t.request({
820
- message: {
821
- type: "SetEnvironment",
822
- payload: { environment: env, merge: opts?.merge },
823
- },
824
- }).catch(() => { });
1040
+ // Use client if available, otherwise ensure client and then set environment
1041
+ if (this.client) {
1042
+ this.client.setEnvironment(env, opts).catch(() => { });
1043
+ }
1044
+ else {
1045
+ // If client not ready yet, ensure it and then set environment
1046
+ this.ensureClient()
1047
+ .then((client) => {
1048
+ client.setEnvironment(env, opts).catch(() => { });
1049
+ })
1050
+ .catch(() => { });
1051
+ }
825
1052
  }
826
1053
  getEnvironment() {
827
- // Fetch from remote via lightweight command
828
- // Note: returns undefined synchronously; callers needing value should use snapshotFull or call runner directly
829
- // For now, we expose an async helper on RemoteRunner. Keep sync signature per interface.
1054
+ // Interface requires sync return, but RuntimeApiClient.getEnvironment() is async.
1055
+ // Returns undefined synchronously; callers needing the actual value should:
1056
+ // - Use snapshotFull() which includes environment
1057
+ // - Call client.getEnvironment() directly if they have access to the client
1058
+ // This is a limitation of the sync interface signature.
830
1059
  return undefined;
831
1060
  }
832
1061
  getOutputs(def) {
@@ -890,183 +1119,233 @@ class RemoteGraphRunner extends AbstractGraphRunner {
890
1119
  return out;
891
1120
  }
892
1121
  dispose() {
1122
+ // Idempotent: allow multiple calls safely
1123
+ if (this.disposed)
1124
+ return;
1125
+ this.disposed = true;
893
1126
  super.dispose();
894
- this.runner = undefined;
895
- this.transport = undefined;
1127
+ // Clear client promise if any
1128
+ this.clientPromise = undefined;
1129
+ // Unsubscribe from custom events and transport status
1130
+ if (this.customEventUnsubscribe) {
1131
+ this.customEventUnsubscribe();
1132
+ this.customEventUnsubscribe = undefined;
1133
+ }
1134
+ if (this.transportStatusUnsubscribe) {
1135
+ this.transportStatusUnsubscribe();
1136
+ this.transportStatusUnsubscribe = undefined;
1137
+ }
1138
+ // Dispose client (which will close transport)
1139
+ const clientToDispose = this.client;
1140
+ this.client = undefined;
896
1141
  this.registryFetched = false; // Reset so registry is fetched again on reconnect
897
1142
  this.registryFetching = false; // Reset fetching state
1143
+ if (clientToDispose) {
1144
+ clientToDispose.dispose().catch((err) => {
1145
+ console.warn("[RemoteGraphRunner] Error disposing client:", err);
1146
+ });
1147
+ }
898
1148
  this.emit("transport", {
899
1149
  state: "disconnected",
900
1150
  kind: this.backend.kind,
901
1151
  });
902
1152
  }
903
- /**
904
- * Fetch full registry description from remote and register it locally.
905
- * Called automatically on first connection with retry mechanism.
906
- */
907
- async fetchRegistry(runner, attempt = 1) {
908
- if (this.registryFetching) {
909
- // Already fetching, don't start another fetch
910
- return;
911
- }
912
- this.registryFetching = true;
913
- try {
914
- const desc = await runner.describeRegistry();
915
- // Register types
916
- for (const t of desc.types) {
917
- if (t.options) {
918
- this.registry.registerEnum({
919
- id: t.id,
920
- options: t.options,
921
- bakeTarget: t.bakeTarget,
922
- });
923
- }
924
- else {
925
- if (!this.registry.types.has(t.id)) {
926
- this.registry.registerType({
927
- id: t.id,
928
- displayName: t.displayName,
929
- validate: (_v) => true,
930
- bakeTarget: t.bakeTarget,
931
- });
932
- }
933
- }
934
- }
935
- // Register categories
936
- for (const c of desc.categories || []) {
937
- if (!this.registry.categories.has(c.id)) {
938
- // Create placeholder category descriptor
939
- const category = {
940
- id: c.id,
941
- displayName: c.displayName,
942
- createRuntime: () => ({
943
- async onInputsChanged() { },
944
- }),
945
- policy: { asyncConcurrency: "switch" },
946
- };
947
- this.registry.categories.register(category);
948
- }
949
- }
950
- // Register coercions
951
- for (const c of desc.coercions) {
952
- if (c.async) {
953
- this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
954
- nonTransitive: c.nonTransitive,
955
- });
956
- }
957
- else {
958
- this.registry.registerCoercion(c.from, c.to, (v) => v, {
959
- nonTransitive: c.nonTransitive,
960
- });
961
- }
962
- }
963
- // Register nodes
964
- for (const n of desc.nodes) {
965
- if (!this.registry.nodes.has(n.id)) {
966
- this.registry.registerNode({
967
- id: n.id,
968
- categoryId: n.categoryId,
969
- displayName: n.displayName,
970
- inputs: n.inputs || {},
971
- outputs: n.outputs || {},
972
- impl: () => { },
973
- });
974
- }
975
- }
976
- this.registryFetched = true;
977
- this.registryFetching = false;
978
- this.emit("registry", this.registry);
979
- }
980
- catch (err) {
981
- this.registryFetching = false;
982
- const error = err instanceof Error ? err : new Error(String(err));
983
- // Retry with exponential backoff if attempts remaining
984
- if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
985
- const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
986
- console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, error);
987
- // Emit error event for UI feedback
988
- this.emit("error", {
989
- kind: "registry",
990
- message: `Registry fetch failed (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying...`,
991
- err: error,
992
- attempt,
993
- maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
994
- });
995
- // Retry after delay
996
- setTimeout(() => {
997
- this.fetchRegistry(runner, attempt + 1).catch(() => {
998
- // Final failure handled below
999
- });
1000
- }, delayMs);
1001
- }
1002
- else {
1003
- // Max attempts reached, emit final error
1004
- console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, error);
1005
- this.emit("error", {
1006
- kind: "registry",
1007
- message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
1008
- err: error,
1009
- attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
1010
- maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
1011
- });
1012
- }
1013
- }
1153
+ }
1154
+
1155
+ function formatDataUrlAsLabel(dataUrl) {
1156
+ try {
1157
+ const semi = dataUrl.indexOf(";");
1158
+ const comma = dataUrl.indexOf(",");
1159
+ const mime = dataUrl.slice(5, semi > 0 ? semi : undefined).toUpperCase();
1160
+ const b64 = comma >= 0 ? dataUrl.slice(comma + 1) : "";
1161
+ const bytes = Math.floor((b64.length * 3) / 4);
1162
+ return `${mime} Data (${bytes} bytes)`;
1014
1163
  }
1015
- // Ensure remote transport/runner
1016
- async ensureRemoteRunner() {
1017
- if (this.runner)
1018
- return this.runner;
1019
- let transport;
1020
- const kind = this.backend.kind;
1021
- const backend = this.backend;
1022
- const connectOptions = backend.connectOptions;
1023
- this.emit("transport", { state: "connecting", kind });
1024
- if (backend.kind === "remote-http") {
1025
- if (!sparkRemote.HttpPollingTransport)
1026
- throw new Error("HttpPollingTransport not available");
1027
- transport = new sparkRemote.HttpPollingTransport(backend.baseUrl);
1028
- await transport.connect(connectOptions);
1029
- }
1030
- else if (backend.kind === "remote-ws") {
1031
- if (!sparkRemote.WebSocketTransport)
1032
- throw new Error("WebSocketTransport not available");
1033
- transport = new sparkRemote.WebSocketTransport(backend.url);
1034
- await transport.connect(connectOptions);
1035
- }
1036
- else {
1037
- throw new Error("Remote backend not configured");
1038
- }
1039
- // Subscribe to custom events if handler provided
1040
- if (backend.onCustomEvent) {
1041
- transport.subscribe((event) => {
1042
- // Filter out standard runtime events, pass others to custom handler
1043
- const msg = event.message;
1044
- if (msg && typeof msg === "object" && "type" in msg) {
1045
- const type = msg.type;
1046
- // Standard runtime events: stats, value, error, invalidate
1047
- // Custom events are anything else (e.g., flow-opened, flow-latest)
1048
- if (!["stats", "value", "error", "invalidate"].includes(type)) {
1049
- backend.onCustomEvent?.(event);
1050
- }
1051
- }
1052
- });
1164
+ catch {
1165
+ return dataUrl.length > 64 ? dataUrl.slice(0, 64) + "…" : dataUrl;
1166
+ }
1167
+ }
1168
+ function resolveOutputDisplay(raw, declared) {
1169
+ if (sparkGraph.isTypedOutput(raw)) {
1170
+ return {
1171
+ typeId: sparkGraph.getTypedOutputTypeId(raw),
1172
+ value: sparkGraph.getTypedOutputValue(raw),
1173
+ };
1174
+ }
1175
+ let typeId = undefined;
1176
+ if (Array.isArray(declared)) {
1177
+ typeId = declared.length === 1 ? declared[0] : undefined;
1178
+ }
1179
+ else if (typeof declared === "string") {
1180
+ typeId = declared.includes("|") ? undefined : declared;
1181
+ }
1182
+ return { typeId, value: raw };
1183
+ }
1184
+ function formatDeclaredTypeSignature(declared) {
1185
+ if (Array.isArray(declared))
1186
+ return declared.join(" | ");
1187
+ return declared ?? "";
1188
+ }
1189
+ /**
1190
+ * Formats a handle ID for display in the UI.
1191
+ * For handles with format "prefix:middle:suffix:extra" (4 parts), displays only the middle part.
1192
+ * Otherwise returns the handle ID as-is.
1193
+ */
1194
+ function prettyHandle(id) {
1195
+ try {
1196
+ const parts = String(id).split(":");
1197
+ // If there are exactly 3 colons (4 parts), display only the second part
1198
+ if (parts.length === 4)
1199
+ return parts[1] || id;
1200
+ return id;
1201
+ }
1202
+ catch {
1203
+ return id;
1204
+ }
1205
+ }
1206
+ // Pre-format common structures for display; return undefined to defer to caller
1207
+ function preformatValueForDisplay(typeId, value, registry) {
1208
+ if (value === undefined || value === null)
1209
+ return "";
1210
+ // Unwrap typed outputs
1211
+ if (sparkGraph.isTypedOutput(value)) {
1212
+ return preformatValueForDisplay(sparkGraph.getTypedOutputTypeId(value), sparkGraph.getTypedOutputValue(value), registry);
1213
+ }
1214
+ // Enums
1215
+ if (typeId && typeId.startsWith("enum:") && registry) {
1216
+ const n = Number(value);
1217
+ const label = registry.enums.get(typeId)?.valueToLabel.get(n);
1218
+ if (label)
1219
+ return label;
1220
+ }
1221
+ // Use deep summarization for strings, arrays and nested objects to avoid huge HTML payloads
1222
+ const summarized = summarizeDeep(value);
1223
+ if (typeof summarized === "string")
1224
+ return summarized;
1225
+ // Resource-like objects with url/title (after summarization)
1226
+ if (summarized && typeof summarized === "object") {
1227
+ const urlMaybe = summarized.url;
1228
+ if (typeof urlMaybe === "string") {
1229
+ const title = summarized.title || "";
1230
+ const shortUrl = urlMaybe.length > 32 ? urlMaybe.slice(0, 32) + "…" : urlMaybe;
1231
+ return title ? `${title} (${shortUrl})` : shortUrl;
1053
1232
  }
1054
- const runner = new sparkRemote.RemoteRunner(transport);
1055
- this.runner = runner;
1056
- this.transport = transport;
1057
- this.valueCache.clear();
1058
- this.listenersBound = false;
1059
- this.emit("transport", { state: "connected", kind });
1060
- // Auto-fetch registry on first connection (only once)
1061
- if (!this.registryFetched && !this.registryFetching) {
1062
- // Log loading state (UI can listen to transport status for loading indication)
1063
- console.info("Loading registry from remote...");
1064
- this.fetchRegistry(runner).catch(() => {
1065
- // Error handling is done inside fetchRegistry
1066
- });
1233
+ }
1234
+ return undefined;
1235
+ }
1236
+ function summarizeDeep(value) {
1237
+ // Strings: summarize data URLs and trim extremely long strings
1238
+ if (typeof value === "string") {
1239
+ if (value.startsWith("data:"))
1240
+ return formatDataUrlAsLabel(value);
1241
+ return value.length > 512 ? value.slice(0, 512) + "…" : value;
1242
+ }
1243
+ // Typed output wrapper
1244
+ if (sparkGraph.isTypedOutput(value)) {
1245
+ return summarizeDeep(sparkGraph.getTypedOutputValue(value));
1246
+ }
1247
+ // Arrays
1248
+ if (Array.isArray(value)) {
1249
+ return value.map((v) => summarizeDeep(v));
1250
+ }
1251
+ // Objects
1252
+ if (value && typeof value === "object") {
1253
+ const obj = value;
1254
+ const out = {};
1255
+ for (const [k, v] of Object.entries(obj)) {
1256
+ // Special-case any 'url' field
1257
+ if (typeof v === "string" &&
1258
+ k.toLowerCase() === "url" &&
1259
+ v.startsWith("data:")) {
1260
+ out[k] = formatDataUrlAsLabel(v);
1261
+ continue;
1262
+ }
1263
+ out[k] = summarizeDeep(v);
1067
1264
  }
1068
- return runner;
1265
+ return out;
1069
1266
  }
1267
+ return value;
1268
+ }
1269
+
1270
+ // Shared UI constants for node layout to keep mapping and rendering in sync
1271
+ const NODE_HEADER_HEIGHT_PX = 24;
1272
+ const NODE_ROW_HEIGHT_PX = 22;
1273
+
1274
+ function computeEffectiveHandles(node, registry) {
1275
+ const desc = registry.nodes.get(node.typeId);
1276
+ const resolved = node.resolvedHandles || {};
1277
+ const inputs = { ...desc?.inputs, ...resolved.inputs };
1278
+ const outputs = { ...desc?.outputs, ...resolved.outputs };
1279
+ const inputDefaults = { ...desc?.inputDefaults, ...resolved.inputDefaults };
1280
+ return { inputs, outputs, inputDefaults };
1281
+ }
1282
+ function countVisibleHandles(handles) {
1283
+ const inputIds = Object.keys(handles.inputs).filter((k) => !sparkGraph.isInputPrivate(handles.inputs, k));
1284
+ const outputIds = Object.keys(handles.outputs);
1285
+ return { inputsCount: inputIds.length, outputsCount: outputIds.length };
1286
+ }
1287
+ function estimateNodeSize(args) {
1288
+ const { node, registry, showValues, overrides } = args;
1289
+ const { inputs, outputs } = computeEffectiveHandles(node, registry);
1290
+ // Count only non-private inputs for rows on left
1291
+ const { inputsCount, outputsCount } = countVisibleHandles({
1292
+ inputs,
1293
+ outputs,
1294
+ });
1295
+ const rows = Math.max(inputsCount, outputsCount);
1296
+ const baseWidth = showValues ? 320 : 240;
1297
+ const width = overrides?.width ?? baseWidth;
1298
+ const height = overrides?.height ?? NODE_HEADER_HEIGHT_PX + rows * NODE_ROW_HEIGHT_PX;
1299
+ return { width, height, inputsCount, outputsCount, rowCount: rows };
1300
+ }
1301
+ function layoutNode(args) {
1302
+ const { node, registry, showValues, overrides } = args;
1303
+ const { inputs, outputs } = computeEffectiveHandles(node, registry);
1304
+ const inputOrder = Object.keys(inputs).filter((k) => !sparkGraph.isInputPrivate(inputs, k));
1305
+ const outputOrder = Object.keys(outputs);
1306
+ const { width, height } = estimateNodeSize({
1307
+ node,
1308
+ registry,
1309
+ showValues,
1310
+ overrides,
1311
+ });
1312
+ const HEADER = NODE_HEADER_HEIGHT_PX;
1313
+ const ROW = NODE_ROW_HEIGHT_PX;
1314
+ const handles = [
1315
+ ...inputOrder.map((id, i) => ({
1316
+ id,
1317
+ type: "target",
1318
+ position: react.Position.Left,
1319
+ x: 0,
1320
+ y: HEADER + i * ROW,
1321
+ width: 1,
1322
+ height: ROW + 2,
1323
+ })),
1324
+ ...outputOrder.map((id, i) => ({
1325
+ id,
1326
+ type: "source",
1327
+ position: react.Position.Right,
1328
+ x: width - 1,
1329
+ y: HEADER + i * ROW,
1330
+ width: 1,
1331
+ height: ROW + 2,
1332
+ })),
1333
+ ];
1334
+ const handleLayout = [
1335
+ ...inputOrder.map((id, i) => ({
1336
+ id,
1337
+ type: "target",
1338
+ position: react.Position.Left,
1339
+ y: HEADER + i * ROW + ROW / 2,
1340
+ })),
1341
+ ...outputOrder.map((id, i) => ({
1342
+ id,
1343
+ type: "source",
1344
+ position: react.Position.Right,
1345
+ y: HEADER + i * ROW + ROW / 2,
1346
+ })),
1347
+ ];
1348
+ return { width, height, inputOrder, outputOrder, handles, handleLayout };
1070
1349
  }
1071
1350
 
1072
1351
  function useWorkbenchBridge(wb) {
@@ -1313,202 +1592,6 @@ function useQueryParamString(key, defaultValue) {
1313
1592
  return [val, set];
1314
1593
  }
1315
1594
 
1316
- function formatDataUrlAsLabel(dataUrl) {
1317
- try {
1318
- const semi = dataUrl.indexOf(";");
1319
- const comma = dataUrl.indexOf(",");
1320
- const mime = dataUrl.slice(5, semi > 0 ? semi : undefined).toUpperCase();
1321
- const b64 = comma >= 0 ? dataUrl.slice(comma + 1) : "";
1322
- const bytes = Math.floor((b64.length * 3) / 4);
1323
- return `${mime} Data (${bytes} bytes)`;
1324
- }
1325
- catch {
1326
- return dataUrl.length > 64 ? dataUrl.slice(0, 64) + "…" : dataUrl;
1327
- }
1328
- }
1329
- function resolveOutputDisplay(raw, declared) {
1330
- if (sparkGraph.isTypedOutput(raw)) {
1331
- return {
1332
- typeId: sparkGraph.getTypedOutputTypeId(raw),
1333
- value: sparkGraph.getTypedOutputValue(raw),
1334
- };
1335
- }
1336
- let typeId = undefined;
1337
- if (Array.isArray(declared)) {
1338
- typeId = declared.length === 1 ? declared[0] : undefined;
1339
- }
1340
- else if (typeof declared === "string") {
1341
- typeId = declared.includes("|") ? undefined : declared;
1342
- }
1343
- return { typeId, value: raw };
1344
- }
1345
- function formatDeclaredTypeSignature(declared) {
1346
- if (Array.isArray(declared))
1347
- return declared.join(" | ");
1348
- return declared ?? "";
1349
- }
1350
- /**
1351
- * Formats a handle ID for display in the UI.
1352
- * For handles with format "prefix:middle:suffix:extra" (4 parts), displays only the middle part.
1353
- * Otherwise returns the handle ID as-is.
1354
- */
1355
- function prettyHandle(id) {
1356
- try {
1357
- const parts = String(id).split(":");
1358
- // If there are exactly 3 colons (4 parts), display only the second part
1359
- if (parts.length === 4)
1360
- return parts[1] || id;
1361
- return id;
1362
- }
1363
- catch {
1364
- return id;
1365
- }
1366
- }
1367
- // Pre-format common structures for display; return undefined to defer to caller
1368
- function preformatValueForDisplay(typeId, value, registry) {
1369
- if (value === undefined || value === null)
1370
- return "";
1371
- // Unwrap typed outputs
1372
- if (sparkGraph.isTypedOutput(value)) {
1373
- return preformatValueForDisplay(sparkGraph.getTypedOutputTypeId(value), sparkGraph.getTypedOutputValue(value), registry);
1374
- }
1375
- // Enums
1376
- if (typeId && typeId.startsWith("enum:") && registry) {
1377
- const n = Number(value);
1378
- const label = registry.enums.get(typeId)?.valueToLabel.get(n);
1379
- if (label)
1380
- return label;
1381
- }
1382
- // Use deep summarization for strings, arrays and nested objects to avoid huge HTML payloads
1383
- const summarized = summarizeDeep(value);
1384
- if (typeof summarized === "string")
1385
- return summarized;
1386
- // Resource-like objects with url/title (after summarization)
1387
- if (summarized && typeof summarized === "object") {
1388
- const urlMaybe = summarized.url;
1389
- if (typeof urlMaybe === "string") {
1390
- const title = summarized.title || "";
1391
- const shortUrl = urlMaybe.length > 32 ? urlMaybe.slice(0, 32) + "…" : urlMaybe;
1392
- return title ? `${title} (${shortUrl})` : shortUrl;
1393
- }
1394
- }
1395
- return undefined;
1396
- }
1397
- function summarizeDeep(value) {
1398
- // Strings: summarize data URLs and trim extremely long strings
1399
- if (typeof value === "string") {
1400
- if (value.startsWith("data:"))
1401
- return formatDataUrlAsLabel(value);
1402
- return value.length > 512 ? value.slice(0, 512) + "…" : value;
1403
- }
1404
- // Typed output wrapper
1405
- if (sparkGraph.isTypedOutput(value)) {
1406
- return summarizeDeep(sparkGraph.getTypedOutputValue(value));
1407
- }
1408
- // Arrays
1409
- if (Array.isArray(value)) {
1410
- return value.map((v) => summarizeDeep(v));
1411
- }
1412
- // Objects
1413
- if (value && typeof value === "object") {
1414
- const obj = value;
1415
- const out = {};
1416
- for (const [k, v] of Object.entries(obj)) {
1417
- // Special-case any 'url' field
1418
- if (typeof v === "string" &&
1419
- k.toLowerCase() === "url" &&
1420
- v.startsWith("data:")) {
1421
- out[k] = formatDataUrlAsLabel(v);
1422
- continue;
1423
- }
1424
- out[k] = summarizeDeep(v);
1425
- }
1426
- return out;
1427
- }
1428
- return value;
1429
- }
1430
-
1431
- // Shared UI constants for node layout to keep mapping and rendering in sync
1432
- const NODE_HEADER_HEIGHT_PX = 24;
1433
- const NODE_ROW_HEIGHT_PX = 22;
1434
-
1435
- function computeEffectiveHandles(node, registry) {
1436
- const desc = registry.nodes.get(node.typeId);
1437
- const resolved = node.resolvedHandles || {};
1438
- const inputs = { ...desc?.inputs, ...resolved.inputs };
1439
- const outputs = { ...desc?.outputs, ...resolved.outputs };
1440
- const inputDefaults = { ...desc?.inputDefaults, ...resolved.inputDefaults };
1441
- return { inputs, outputs, inputDefaults };
1442
- }
1443
- function countVisibleHandles(handles) {
1444
- const inputIds = Object.keys(handles.inputs).filter((k) => !sparkGraph.isInputPrivate(handles.inputs, k));
1445
- const outputIds = Object.keys(handles.outputs);
1446
- return { inputsCount: inputIds.length, outputsCount: outputIds.length };
1447
- }
1448
- function estimateNodeSize(args) {
1449
- const { node, registry, showValues, overrides } = args;
1450
- const { inputs, outputs } = computeEffectiveHandles(node, registry);
1451
- // Count only non-private inputs for rows on left
1452
- const { inputsCount, outputsCount } = countVisibleHandles({
1453
- inputs,
1454
- outputs,
1455
- });
1456
- const rows = Math.max(inputsCount, outputsCount);
1457
- const baseWidth = showValues ? 320 : 240;
1458
- const width = overrides?.width ?? baseWidth;
1459
- const height = overrides?.height ?? NODE_HEADER_HEIGHT_PX + rows * NODE_ROW_HEIGHT_PX;
1460
- return { width, height, inputsCount, outputsCount, rowCount: rows };
1461
- }
1462
- function layoutNode(args) {
1463
- const { node, registry, showValues, overrides } = args;
1464
- const { inputs, outputs } = computeEffectiveHandles(node, registry);
1465
- const inputOrder = Object.keys(inputs).filter((k) => !sparkGraph.isInputPrivate(inputs, k));
1466
- const outputOrder = Object.keys(outputs);
1467
- const { width, height } = estimateNodeSize({
1468
- node,
1469
- registry,
1470
- showValues,
1471
- overrides,
1472
- });
1473
- const HEADER = NODE_HEADER_HEIGHT_PX;
1474
- const ROW = NODE_ROW_HEIGHT_PX;
1475
- const handles = [
1476
- ...inputOrder.map((id, i) => ({
1477
- id,
1478
- type: "target",
1479
- position: react.Position.Left,
1480
- x: 0,
1481
- y: HEADER + i * ROW,
1482
- width: 1,
1483
- height: ROW + 2,
1484
- })),
1485
- ...outputOrder.map((id, i) => ({
1486
- id,
1487
- type: "source",
1488
- position: react.Position.Right,
1489
- x: width - 1,
1490
- y: HEADER + i * ROW,
1491
- width: 1,
1492
- height: ROW + 2,
1493
- })),
1494
- ];
1495
- const handleLayout = [
1496
- ...inputOrder.map((id, i) => ({
1497
- id,
1498
- type: "target",
1499
- position: react.Position.Left,
1500
- y: HEADER + i * ROW + ROW / 2,
1501
- })),
1502
- ...outputOrder.map((id, i) => ({
1503
- id,
1504
- type: "source",
1505
- position: react.Position.Right,
1506
- y: HEADER + i * ROW + ROW / 2,
1507
- })),
1508
- ];
1509
- return { width, height, inputOrder, outputOrder, handles, handleLayout };
1510
- }
1511
-
1512
1595
  function toReactFlow(def, positions, registry, opts) {
1513
1596
  const EDGE_STYLE_MISSING = { stroke: "#f59e0b", strokeWidth: 2 }; // amber-500
1514
1597
  const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
@@ -1734,6 +1817,33 @@ function getNodeBorderClassNames(args) {
1734
1817
  : "";
1735
1818
  return [borderWidth, borderStyle, borderColor, ring].join(" ").trim();
1736
1819
  }
1820
+ /**
1821
+ * Shared utility to generate handle className based on validation and value state.
1822
+ * - Linked handles (with inbound edges) get black borders
1823
+ * - Handles with values (but not linked) get darker gray borders
1824
+ * - Handles with only defaults (no value, not linked) get lighter gray borders
1825
+ * - Validation errors (red/amber) take precedence over value-based styling.
1826
+ */
1827
+ function getHandleClassName(args) {
1828
+ const { kind, id, validation } = args;
1829
+ const vIssues = (kind === "input" ? validation.inputs : validation.outputs)?.filter((v) => v.handle === id) || [];
1830
+ const hasAny = vIssues.length > 0;
1831
+ const hasErr = vIssues.some((v) => v.level === "error");
1832
+ // Determine border color based on priority:
1833
+ // 1. Validation errors (red/amber) - highest priority
1834
+ // 2. Gray border
1835
+ let borderColor;
1836
+ if (hasAny && hasErr) {
1837
+ borderColor = "!border-red-500";
1838
+ }
1839
+ else if (hasAny) {
1840
+ borderColor = "!border-amber-500";
1841
+ }
1842
+ else {
1843
+ borderColor = "!border-gray-500 dark:!border-gray-400";
1844
+ }
1845
+ return cx("!w-3 !h-3 !bg-white !dark:bg-stone-900", borderColor, kind === "output" && "!rounded-none");
1846
+ }
1737
1847
 
1738
1848
  const WorkbenchContext = React.createContext(null);
1739
1849
  function useWorkbenchContext() {
@@ -2456,7 +2566,7 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
2456
2566
  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: handleCopyLogs, className: "p-2 border border-gray-300 rounded flex items-center justify-center", title: copied ? "Copied!" : "Copy logs as formatted JSON", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "p-2 border border-gray-300 rounded flex items-center justify-center", title: "Clear all events", children: jsxRuntime.jsx(react$1.TrashIcon, { size: 14 }) })] })] }), jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev) => (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-12 shrink-0 text-right text-gray-500 select-none", children: ev.no }), 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-12", children: renderPayload(ev.payload) })] }, `${ev.at}:${ev.no}`))) })] }));
2457
2567
  }
2458
2568
 
2459
- function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, contextPanel, setInput, }) {
2569
+ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, contextPanel, setInput, }) {
2460
2570
  const safeToString = (typeId, value) => {
2461
2571
  try {
2462
2572
  if (typeof toString === "function") {
@@ -2834,15 +2944,17 @@ function DefaultNodeContent({ data, isConnectable, }) {
2834
2944
  const status = data.status ?? { activeRuns: 0 };
2835
2945
  const validation = data.validation ?? {
2836
2946
  inputs: [],
2837
- outputs: []};
2947
+ outputs: [],
2948
+ issues: [],
2949
+ };
2838
2950
  const isRunning = !!status.activeRuns;
2839
2951
  const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
2840
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: cx("h-px", (isRunning || pct > 0) && "bg-blue-200 dark:bg-blue-900"), children: jsxRuntime.jsx("div", { className: cx("h-px transition-all", (isRunning || pct > 0) && "bg-blue-500"), style: { width: isRunning || pct > 0 ? `${pct}%` : 0 } }) }), jsxRuntime.jsx(NodeHandles, { data: data, isConnectable: isConnectable, getClassName: ({ kind, id }) => {
2841
- const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
2842
- const hasAny = vIssues.length > 0;
2843
- const hasErr = vIssues.some((v) => v.level === "error");
2844
- return cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", kind === "output" && "!rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500"));
2845
- }, renderLabel: ({ kind, id: handleId }) => {
2952
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: cx("h-px", (isRunning || pct > 0) && "bg-blue-200 dark:bg-blue-900"), children: jsxRuntime.jsx("div", { className: cx("h-px transition-all", (isRunning || pct > 0) && "bg-blue-500"), style: { width: isRunning || pct > 0 ? `${pct}%` : 0 } }) }), jsxRuntime.jsx(NodeHandles, { data: data, isConnectable: isConnectable, getClassName: ({ kind, id }) => getHandleClassName({
2953
+ kind,
2954
+ id,
2955
+ validation,
2956
+ inputConnected: data.inputConnected,
2957
+ }), renderLabel: ({ kind, id: handleId }) => {
2846
2958
  const entries = kind === "input" ? inputEntries : outputEntries;
2847
2959
  const entry = entries.find((e) => e.id === handleId);
2848
2960
  if (!entry)
@@ -3448,9 +3560,6 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3448
3560
  state: "local",
3449
3561
  });
3450
3562
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
3451
- selectedNode
3452
- ? registry.nodes.get(selectedNode.typeId)
3453
- : undefined;
3454
3563
  const effectiveHandles = selectedNode
3455
3564
  ? computeEffectiveHandles(selectedNode, registry)
3456
3565
  : { inputs: {}, outputs: {}, inputDefaults: {} };
@@ -3741,7 +3850,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3741
3850
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
3742
3851
  if (isLinked)
3743
3852
  return;
3744
- const typeId = effectiveHandles.inputs[handle];
3853
+ // If raw is undefined, pass it through to delete the input value
3854
+ if (raw === undefined) {
3855
+ runner.setInput(selectedNodeId, handle, undefined);
3856
+ return;
3857
+ }
3858
+ const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, handle);
3745
3859
  let value = raw;
3746
3860
  const parseArray = (s, map) => {
3747
3861
  const str = String(s).trim();
@@ -3950,7 +4064,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3950
4064
  const message = err instanceof Error ? err.message : String(err);
3951
4065
  alert(message);
3952
4066
  }
3953
- }, children: jsxRuntime.jsx(react$1.DownloadIcon, { size: 24 }) }), jsxRuntime.jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: jsxRuntime.jsx(react$1.UploadIcon, { size: 24 }) }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx(react$1.BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx(react$1.ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement, contextPanel: overrides?.contextPanel })] })] }));
4067
+ }, children: jsxRuntime.jsx(react$1.DownloadIcon, { size: 24 }) }), jsxRuntime.jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: jsxRuntime.jsx(react$1.UploadIcon, { size: 24 }) }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx(react$1.BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx(react$1.ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, contextPanel: overrides?.contextPanel })] })] }));
3954
4068
  }
3955
4069
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
3956
4070
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
@@ -4046,9 +4160,14 @@ exports.WorkbenchCanvas = WorkbenchCanvas;
4046
4160
  exports.WorkbenchContext = WorkbenchContext;
4047
4161
  exports.WorkbenchProvider = WorkbenchProvider;
4048
4162
  exports.WorkbenchStudio = WorkbenchStudio;
4163
+ exports.computeEffectiveHandles = computeEffectiveHandles;
4164
+ exports.countVisibleHandles = countVisibleHandles;
4165
+ exports.estimateNodeSize = estimateNodeSize;
4049
4166
  exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
4050
4167
  exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
4168
+ exports.getHandleClassName = getHandleClassName;
4051
4169
  exports.getNodeBorderClassNames = getNodeBorderClassNames;
4170
+ exports.layoutNode = layoutNode;
4052
4171
  exports.preformatValueForDisplay = preformatValueForDisplay;
4053
4172
  exports.prettyHandle = prettyHandle;
4054
4173
  exports.resolveOutputDisplay = resolveOutputDisplay;