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