@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 +536 -417
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/index.d.ts +2 -1
- package/lib/cjs/src/index.d.ts.map +1 -1
- package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts +1 -2
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/cjs/src/misc/mapping.d.ts +19 -0
- package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +32 -17
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +533 -419
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/index.d.ts +2 -1
- package/lib/esm/src/index.d.ts.map +1 -1
- package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts +1 -2
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/esm/src/misc/mapping.d.ts +19 -0
- package/lib/esm/src/misc/mapping.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +32 -17
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/package.json +4 -4
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 {
|
|
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.
|
|
614
|
-
const eng =
|
|
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.
|
|
868
|
+
this.ensureClient().then(async (client) => {
|
|
679
869
|
try {
|
|
680
|
-
await
|
|
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.
|
|
691
|
-
await
|
|
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
|
|
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 =
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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.
|
|
978
|
+
this.ensureClient().then(async (client) => {
|
|
755
979
|
try {
|
|
756
|
-
await
|
|
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
|
|
986
|
+
const client = await this.ensureClient();
|
|
763
987
|
try {
|
|
764
|
-
return await
|
|
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
|
|
995
|
+
const client = await this.ensureClient();
|
|
772
996
|
try {
|
|
773
|
-
return await
|
|
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.
|
|
1005
|
+
this.hydrateSnapshotFull(payload);
|
|
782
1006
|
// Then sync with backend
|
|
783
|
-
const
|
|
784
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
815
|
-
if (
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
//
|
|
826
|
-
//
|
|
827
|
-
//
|
|
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
|
-
|
|
893
|
-
this.
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
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
|
-
|
|
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,
|
|
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
|