@bian-womp/spark-workbench 0.2.47 → 0.2.49
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 +308 -161
- package/lib/cjs/index.cjs.map +1 -1
- 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/context/WorkbenchContext.d.ts +2 -2
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +7 -3
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/IGraphRunner.d.ts +6 -14
- package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +4 -3
- package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +12 -3
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +310 -163
- package/lib/esm/index.js.map +1 -1
- 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/context/WorkbenchContext.d.ts +2 -2
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +7 -3
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/IGraphRunner.d.ts +6 -14
- package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/LocalGraphRunner.d.ts +4 -3
- package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +12 -3
- 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
|
-
import { GraphBuilder,
|
|
1
|
+
import { GraphBuilder, createEngine, StepEngine, PullEngine, BatchedEngine, isTypedOutput, getTypedOutputValue, getTypedOutputTypeId, isInputPrivate, getInputTypeId, createSimpleGraphRegistry, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry } from '@bian-womp/spark-graph';
|
|
2
2
|
import { RuntimeApiClient } from '@bian-womp/spark-remote';
|
|
3
3
|
import { Position, Handle, useUpdateNodeInternals, useReactFlow, ReactFlowProvider, ReactFlow, Background, BackgroundVariant, MiniMap, Controls } from '@xyflow/react';
|
|
4
4
|
import React, { useCallback, useState, useRef, useEffect, useMemo, createContext, useContext, useImperativeHandle } from 'react';
|
|
5
5
|
import cx from 'classnames';
|
|
6
6
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
7
|
-
import { XCircleIcon, WarningCircleIcon, CopyIcon, TrashIcon, XIcon, ArrowClockwiseIcon,
|
|
7
|
+
import { XCircleIcon, WarningCircleIcon, CopyIcon, TrashIcon, XIcon, ArrowClockwiseIcon, ClockClockwiseIcon, StopIcon, PlayIcon, PlugsConnectedIcon, WifiHighIcon, WifiSlashIcon, PlayPauseIcon, LightningIcon, TreeStructureIcon, CornersOutIcon, DownloadSimpleIcon, DownloadIcon, UploadIcon, BugBeetleIcon, ListBulletsIcon } from '@phosphor-icons/react';
|
|
8
8
|
import isEqual from 'lodash/isEqual';
|
|
9
9
|
|
|
10
10
|
class DefaultUIExtensionRegistry {
|
|
@@ -332,10 +332,12 @@ class AbstractGraphRunner {
|
|
|
332
332
|
this.backend = backend;
|
|
333
333
|
this.listeners = new Map();
|
|
334
334
|
this.stagedInputs = {};
|
|
335
|
+
this.runnerId = "";
|
|
335
336
|
}
|
|
336
337
|
launch(def, opts) {
|
|
338
|
+
// Auto-stop if engine is already running
|
|
337
339
|
if (this.engine) {
|
|
338
|
-
|
|
340
|
+
this.stop();
|
|
339
341
|
}
|
|
340
342
|
}
|
|
341
343
|
setInput(nodeId, handle, value) {
|
|
@@ -387,6 +389,39 @@ class AbstractGraphRunner {
|
|
|
387
389
|
async whenIdle() {
|
|
388
390
|
await this.engine?.whenIdle();
|
|
389
391
|
}
|
|
392
|
+
stop() {
|
|
393
|
+
if (!this.engine)
|
|
394
|
+
return;
|
|
395
|
+
// Dispose engine (cleans up timers, listeners, etc.)
|
|
396
|
+
this.engine.dispose();
|
|
397
|
+
this.engine = undefined;
|
|
398
|
+
// Emit status but keep runtime alive
|
|
399
|
+
if (this.runningKind) {
|
|
400
|
+
this.runningKind = undefined;
|
|
401
|
+
this.emit("status", { running: false, engine: undefined });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async switchEngine(opts) {
|
|
405
|
+
if (!this.engine || !this.runtime) {
|
|
406
|
+
throw new Error("No engine running to switch from");
|
|
407
|
+
}
|
|
408
|
+
// Wait for current engine to be idle
|
|
409
|
+
await this.whenIdle();
|
|
410
|
+
// Capture current state
|
|
411
|
+
const currentInputs = { ...this.stagedInputs };
|
|
412
|
+
// Stop current engine
|
|
413
|
+
this.stop();
|
|
414
|
+
// Ensure runtime is in a clean state (resumed)
|
|
415
|
+
this.runtime.resume();
|
|
416
|
+
// Create and launch new engine (to be implemented by subclasses)
|
|
417
|
+
await this.createAndLaunchEngine(opts);
|
|
418
|
+
// Re-apply staged inputs to new engine
|
|
419
|
+
for (const [nodeId, map] of Object.entries(currentInputs)) {
|
|
420
|
+
if (this.engine) {
|
|
421
|
+
this.engine.setInputs(nodeId, map);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
390
425
|
on(event, handler) {
|
|
391
426
|
if (!this.listeners.has(event))
|
|
392
427
|
this.listeners.set(event, new Set());
|
|
@@ -418,6 +453,8 @@ class AbstractGraphRunner {
|
|
|
418
453
|
}
|
|
419
454
|
}
|
|
420
455
|
|
|
456
|
+
// Counter for generating readable runner IDs
|
|
457
|
+
let localRunnerCounter = 0;
|
|
421
458
|
class LocalGraphRunner extends AbstractGraphRunner {
|
|
422
459
|
constructor(registry) {
|
|
423
460
|
super(registry, { kind: "local" });
|
|
@@ -436,7 +473,11 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
436
473
|
this.getEnvironment = () => {
|
|
437
474
|
return this.runtime?.getEnvironment?.();
|
|
438
475
|
};
|
|
439
|
-
this
|
|
476
|
+
// Generate readable ID for this runner instance (e.g., local-001, local-002)
|
|
477
|
+
localRunnerCounter++;
|
|
478
|
+
this.runnerId = `local-${String(localRunnerCounter).padStart(3, "0")}`;
|
|
479
|
+
console.info(`[LocalGraphRunner] Created runner with ID: ${this.runnerId}`);
|
|
480
|
+
this.emit("transport", { runnerId: this.runnerId, state: "local" });
|
|
440
481
|
}
|
|
441
482
|
build(def) {
|
|
442
483
|
const builder = new GraphBuilder(this.registry);
|
|
@@ -458,37 +499,30 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
458
499
|
this.build(def);
|
|
459
500
|
if (!this.runtime)
|
|
460
501
|
throw new Error("Runtime not built");
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
break;
|
|
480
|
-
case "step":
|
|
481
|
-
this.engine = new StepEngine(rt);
|
|
482
|
-
break;
|
|
483
|
-
default:
|
|
484
|
-
throw new Error("Unknown engine kind");
|
|
485
|
-
}
|
|
502
|
+
// Use the async method to create engine
|
|
503
|
+
this.createAndLaunchEngine(opts).catch((err) => {
|
|
504
|
+
console.error("Failed to launch engine:", err);
|
|
505
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
506
|
+
this.emit("error", {
|
|
507
|
+
kind: "system",
|
|
508
|
+
message: errorMessage,
|
|
509
|
+
err: err instanceof Error ? err : new Error(errorMessage),
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
async createAndLaunchEngine(opts) {
|
|
514
|
+
if (!this.runtime)
|
|
515
|
+
throw new Error("Runtime not built");
|
|
516
|
+
// Use shared engine factory
|
|
517
|
+
this.engine = createEngine(this.runtime, opts);
|
|
518
|
+
if (!this.engine)
|
|
519
|
+
throw new Error("Failed to create engine");
|
|
486
520
|
this.engine.on("value", (e) => this.emit("value", e));
|
|
487
521
|
this.engine.on("error", (e) => this.emit("error", e));
|
|
488
522
|
this.engine.on("invalidate", (e) => this.emit("invalidate", e));
|
|
489
523
|
this.engine.on("stats", (e) => this.emit("stats", e));
|
|
490
|
-
this.engine.launch(opts
|
|
491
|
-
this.runningKind = opts
|
|
524
|
+
this.engine.launch(opts?.invalidate);
|
|
525
|
+
this.runningKind = opts?.engine ?? "push";
|
|
492
526
|
this.emit("status", { running: true, engine: this.runningKind });
|
|
493
527
|
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
494
528
|
this.engine.setInputs(nodeId, map);
|
|
@@ -496,18 +530,18 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
496
530
|
}
|
|
497
531
|
async step() {
|
|
498
532
|
const eng = this.engine;
|
|
499
|
-
if (eng instanceof StepEngine)
|
|
533
|
+
if (eng && eng instanceof StepEngine)
|
|
500
534
|
await eng.step();
|
|
501
535
|
}
|
|
502
536
|
async computeNode(nodeId) {
|
|
503
537
|
const eng = this.engine;
|
|
504
|
-
if (eng instanceof PullEngine)
|
|
538
|
+
if (eng && eng instanceof PullEngine)
|
|
505
539
|
await eng.computeNode(nodeId);
|
|
506
540
|
}
|
|
507
|
-
flush() {
|
|
541
|
+
async flush() {
|
|
508
542
|
const eng = this.engine;
|
|
509
|
-
if (eng instanceof BatchedEngine)
|
|
510
|
-
eng.flush();
|
|
543
|
+
if (eng && eng instanceof BatchedEngine)
|
|
544
|
+
await eng.flush();
|
|
511
545
|
}
|
|
512
546
|
getOutputs(def) {
|
|
513
547
|
const out = {};
|
|
@@ -595,10 +629,12 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
595
629
|
dispose() {
|
|
596
630
|
super.dispose();
|
|
597
631
|
this.runtime = undefined;
|
|
598
|
-
this.emit("transport", { state: "local" });
|
|
632
|
+
this.emit("transport", { runnerId: this.runnerId, state: "local" });
|
|
599
633
|
}
|
|
600
634
|
}
|
|
601
635
|
|
|
636
|
+
// Counter for generating readable runner IDs
|
|
637
|
+
let remoteRunnerCounter = 0;
|
|
602
638
|
class RemoteGraphRunner extends AbstractGraphRunner {
|
|
603
639
|
/**
|
|
604
640
|
* Fetch full registry description from remote and register it locally.
|
|
@@ -737,13 +773,20 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
737
773
|
setupClientSubscriptions(client) {
|
|
738
774
|
// Subscribe to transport status changes
|
|
739
775
|
// Convert RuntimeApiClient.TransportStatus to IGraphRunner.TransportStatus
|
|
776
|
+
// Only emit status if it matches this runner's ID
|
|
740
777
|
this.transportStatusUnsubscribe = client.onTransportStatus((status) => {
|
|
778
|
+
if (status.runnerId && status.runnerId !== this.runnerId)
|
|
779
|
+
return;
|
|
741
780
|
// Map remote-unix to undefined since RemoteGraphRunner doesn't support it
|
|
742
781
|
const mappedKind = status.kind === "remote-unix" ? undefined : status.kind;
|
|
743
|
-
|
|
782
|
+
const transportStatus = {
|
|
744
783
|
state: status.state,
|
|
745
784
|
kind: mappedKind,
|
|
746
|
-
|
|
785
|
+
runnerId: this.runnerId,
|
|
786
|
+
};
|
|
787
|
+
// Track current status
|
|
788
|
+
this.currentTransportStatus = transportStatus;
|
|
789
|
+
this.emit("transport", transportStatus);
|
|
747
790
|
});
|
|
748
791
|
}
|
|
749
792
|
// Ensure remote client
|
|
@@ -764,6 +807,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
764
807
|
// Create client with custom event handler if provided
|
|
765
808
|
const client = new RuntimeApiClient(clientConfig, {
|
|
766
809
|
onCustomEvent: backend.onCustomEvent,
|
|
810
|
+
runnerId: this.runnerId,
|
|
767
811
|
});
|
|
768
812
|
// Setup event subscriptions
|
|
769
813
|
this.setupClientSubscriptions(client);
|
|
@@ -796,6 +840,15 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
796
840
|
this.registryFetching = false;
|
|
797
841
|
this.MAX_REGISTRY_FETCH_ATTEMPTS = 3;
|
|
798
842
|
this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
|
|
843
|
+
// Generate readable ID for this runner instance (e.g., remote-001, remote-002)
|
|
844
|
+
remoteRunnerCounter++;
|
|
845
|
+
this.runnerId = `remote-${String(remoteRunnerCounter).padStart(3, "0")}`;
|
|
846
|
+
console.info(`[RemoteGraphRunner] Created runner with ID: ${this.runnerId}`);
|
|
847
|
+
// Initialize transport status as "connecting" - will be updated when connection completes
|
|
848
|
+
this.currentTransportStatus = {
|
|
849
|
+
state: "connecting",
|
|
850
|
+
kind: backend.kind,
|
|
851
|
+
};
|
|
799
852
|
// Auto-handle registry-changed invalidations from remote
|
|
800
853
|
// We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
|
|
801
854
|
this.ensureClient().then(async (client) => {
|
|
@@ -903,30 +956,36 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
903
956
|
catch {
|
|
904
957
|
console.error("Failed to hydrate remote inputs/outputs");
|
|
905
958
|
}
|
|
906
|
-
|
|
907
|
-
if (!this.listenersBound) {
|
|
908
|
-
eng.on("value", (e) => {
|
|
909
|
-
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
910
|
-
io: e.io,
|
|
911
|
-
value: e.value,
|
|
912
|
-
runtimeTypeId: e.runtimeTypeId,
|
|
913
|
-
});
|
|
914
|
-
this.emit("value", e);
|
|
915
|
-
});
|
|
916
|
-
eng.on("error", (e) => this.emit("error", e));
|
|
917
|
-
eng.on("invalidate", (e) => this.emit("invalidate", e));
|
|
918
|
-
eng.on("stats", (e) => this.emit("stats", e));
|
|
919
|
-
this.listenersBound = true;
|
|
920
|
-
}
|
|
921
|
-
this.engine = eng;
|
|
922
|
-
this.engine.launch(opts.invalidate);
|
|
923
|
-
this.runningKind = "push";
|
|
924
|
-
this.emit("status", { running: true, engine: this.runningKind });
|
|
925
|
-
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
926
|
-
this.engine.setInputs(nodeId, map);
|
|
927
|
-
}
|
|
959
|
+
await this.createAndLaunchEngine(opts);
|
|
928
960
|
});
|
|
929
961
|
}
|
|
962
|
+
async createAndLaunchEngine(opts) {
|
|
963
|
+
const client = await this.ensureClient();
|
|
964
|
+
// Configure and launch engine on the backend
|
|
965
|
+
await client.launch(opts);
|
|
966
|
+
// Get the remote engine proxy and wire up event listeners
|
|
967
|
+
const eng = client.getEngine();
|
|
968
|
+
if (!this.listenersBound) {
|
|
969
|
+
eng.on("value", (e) => {
|
|
970
|
+
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
971
|
+
io: e.io,
|
|
972
|
+
value: e.value,
|
|
973
|
+
runtimeTypeId: e.runtimeTypeId,
|
|
974
|
+
});
|
|
975
|
+
this.emit("value", e);
|
|
976
|
+
});
|
|
977
|
+
eng.on("error", (e) => this.emit("error", e));
|
|
978
|
+
eng.on("invalidate", (e) => this.emit("invalidate", e));
|
|
979
|
+
eng.on("stats", (e) => this.emit("stats", e));
|
|
980
|
+
this.listenersBound = true;
|
|
981
|
+
}
|
|
982
|
+
this.engine = eng;
|
|
983
|
+
this.runningKind = opts?.engine ?? "push";
|
|
984
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
985
|
+
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
986
|
+
this.engine.setInputs(nodeId, map);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
930
989
|
/**
|
|
931
990
|
* Launch using an existing backend runtime that has already been built and hydrated.
|
|
932
991
|
* This is used when resuming from a snapshot where the backend has already applied
|
|
@@ -942,34 +1001,47 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
942
1001
|
// has already been built and hydrated via ApplySnapshotFull.
|
|
943
1002
|
// Calling build() would create a new runtime and lose the restored state.
|
|
944
1003
|
this.lastDef = def;
|
|
945
|
-
|
|
946
|
-
const eng = client.getEngine();
|
|
947
|
-
if (!this.listenersBound) {
|
|
948
|
-
eng.on("value", (e) => {
|
|
949
|
-
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
950
|
-
io: e.io,
|
|
951
|
-
value: e.value,
|
|
952
|
-
runtimeTypeId: e.runtimeTypeId,
|
|
953
|
-
});
|
|
954
|
-
this.emit("value", e);
|
|
955
|
-
});
|
|
956
|
-
eng.on("error", (e) => this.emit("error", e));
|
|
957
|
-
eng.on("invalidate", (e) => this.emit("invalidate", e));
|
|
958
|
-
eng.on("stats", (e) => this.emit("stats", e));
|
|
959
|
-
this.listenersBound = true;
|
|
960
|
-
}
|
|
961
|
-
this.engine = eng;
|
|
962
|
-
this.engine.launch(opts.invalidate);
|
|
963
|
-
this.runningKind = "push";
|
|
964
|
-
this.emit("status", { running: true, engine: this.runningKind });
|
|
965
|
-
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
966
|
-
this.engine.setInputs(nodeId, map);
|
|
967
|
-
}
|
|
1004
|
+
await this.createAndLaunchEngine(opts);
|
|
968
1005
|
});
|
|
969
1006
|
}
|
|
970
|
-
async
|
|
971
|
-
|
|
972
|
-
|
|
1007
|
+
async switchEngine(opts) {
|
|
1008
|
+
if (!this.engine) {
|
|
1009
|
+
throw new Error("No engine running to switch from");
|
|
1010
|
+
}
|
|
1011
|
+
// Wait for current engine to be idle
|
|
1012
|
+
await this.whenIdle();
|
|
1013
|
+
// Capture current state
|
|
1014
|
+
const currentInputs = { ...this.stagedInputs };
|
|
1015
|
+
// For remote runners, we cannot call this.stop() because it sends a Dispose
|
|
1016
|
+
// command that destroys the graphRuntime on the backend. Instead, we rely on
|
|
1017
|
+
// the backend's launch() method to dispose the old engine and create a new one.
|
|
1018
|
+
// Reconfigure engine on the backend (this will dispose old engine and create new one)
|
|
1019
|
+
const client = await this.ensureClient();
|
|
1020
|
+
await client.launch(opts);
|
|
1021
|
+
// Get the remote engine proxy (should be the same RemoteEngine instance)
|
|
1022
|
+
const eng = client.getEngine();
|
|
1023
|
+
// Update local state to reflect new engine kind
|
|
1024
|
+
// Note: The RemoteEngine instance itself doesn't change, but the backend engine does
|
|
1025
|
+
this.engine = eng;
|
|
1026
|
+
this.runningKind = opts?.engine ?? "push";
|
|
1027
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
1028
|
+
// Re-apply staged inputs to new engine
|
|
1029
|
+
for (const [nodeId, map] of Object.entries(currentInputs)) {
|
|
1030
|
+
this.engine.setInputs(nodeId, map);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
async step() {
|
|
1034
|
+
const client = await this.ensureClient();
|
|
1035
|
+
await client.step();
|
|
1036
|
+
}
|
|
1037
|
+
async computeNode(nodeId) {
|
|
1038
|
+
const client = await this.ensureClient();
|
|
1039
|
+
await client.computeNode(nodeId);
|
|
1040
|
+
}
|
|
1041
|
+
async flush() {
|
|
1042
|
+
const client = await this.ensureClient();
|
|
1043
|
+
await client.flush();
|
|
1044
|
+
}
|
|
973
1045
|
triggerExternal(nodeId, event) {
|
|
974
1046
|
this.ensureClient().then(async (client) => {
|
|
975
1047
|
try {
|
|
@@ -1117,6 +1189,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1117
1189
|
if (this.disposed)
|
|
1118
1190
|
return;
|
|
1119
1191
|
this.disposed = true;
|
|
1192
|
+
console.info(`[RemoteGraphRunner] Disposing runner with ID: ${this.runnerId}`);
|
|
1120
1193
|
super.dispose();
|
|
1121
1194
|
// Clear client promise if any
|
|
1122
1195
|
this.clientPromise = undefined;
|
|
@@ -1136,10 +1209,30 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1136
1209
|
console.warn("[RemoteGraphRunner] Error disposing client:", err);
|
|
1137
1210
|
});
|
|
1138
1211
|
}
|
|
1139
|
-
|
|
1212
|
+
const disconnectedStatus = {
|
|
1140
1213
|
state: "disconnected",
|
|
1141
1214
|
kind: this.backend.kind,
|
|
1142
|
-
|
|
1215
|
+
runnerId: this.runnerId,
|
|
1216
|
+
};
|
|
1217
|
+
this.currentTransportStatus = disconnectedStatus;
|
|
1218
|
+
this.emit("transport", disconnectedStatus);
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Override on() to emit current transport status immediately when a new listener subscribes.
|
|
1222
|
+
* This ensures listeners don't miss the current status when they attach after connection.
|
|
1223
|
+
*/
|
|
1224
|
+
on(event, handler) {
|
|
1225
|
+
const unsubscribe = super.on(event, handler);
|
|
1226
|
+
// If subscribing to transport events and we have a current status, emit it immediately
|
|
1227
|
+
if (event === "transport" && this.currentTransportStatus) {
|
|
1228
|
+
// Use setTimeout to ensure this happens after the listener is registered
|
|
1229
|
+
// This prevents issues if the handler modifies state synchronously
|
|
1230
|
+
setTimeout(() => {
|
|
1231
|
+
// Type assertion is safe here because we checked event === "transport"
|
|
1232
|
+
handler(this.currentTransportStatus);
|
|
1233
|
+
}, 0);
|
|
1234
|
+
}
|
|
1235
|
+
return unsubscribe;
|
|
1143
1236
|
}
|
|
1144
1237
|
}
|
|
1145
1238
|
|
|
@@ -2325,16 +2418,28 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2325
2418
|
offRunnerRegistry();
|
|
2326
2419
|
offRunnerTransport();
|
|
2327
2420
|
};
|
|
2421
|
+
}, [runner, wb, setRegistry]);
|
|
2422
|
+
const isRunning = useCallback(() => runner.isRunning(), [runner]);
|
|
2423
|
+
const engineKind = useCallback(() => runner.getRunningEngine(), [runner]);
|
|
2424
|
+
const start = useCallback((engine) => {
|
|
2425
|
+
try {
|
|
2426
|
+
runner.launch(wb.export(), { engine });
|
|
2427
|
+
}
|
|
2428
|
+
catch { }
|
|
2328
2429
|
}, [runner, wb]);
|
|
2430
|
+
const stop = useCallback(() => runner.stop(), [runner]);
|
|
2431
|
+
const step = useCallback(() => runner.step(), [runner]);
|
|
2432
|
+
const flush = useCallback(() => runner.flush(), [runner]);
|
|
2329
2433
|
// Push incremental updates into running engine without full reload
|
|
2434
|
+
const isGraphRunning = isRunning();
|
|
2330
2435
|
useEffect(() => {
|
|
2331
|
-
if (
|
|
2436
|
+
if (isGraphRunning) {
|
|
2332
2437
|
try {
|
|
2333
2438
|
runner.update(def);
|
|
2334
2439
|
}
|
|
2335
2440
|
catch { }
|
|
2336
2441
|
}
|
|
2337
|
-
}, [runner, def, graphTick]);
|
|
2442
|
+
}, [runner, isGraphRunning, def, graphTick]);
|
|
2338
2443
|
const validationByNode = useMemo(() => {
|
|
2339
2444
|
const inputs = {};
|
|
2340
2445
|
const outputs = {};
|
|
@@ -2398,17 +2503,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2398
2503
|
}
|
|
2399
2504
|
return { errors, issues };
|
|
2400
2505
|
}, [validation]);
|
|
2401
|
-
const isRunning = useCallback(() => runner.isRunning(), [runner]);
|
|
2402
|
-
const engineKind = useCallback(() => runner.getRunningEngine(), [runner]);
|
|
2403
|
-
const start = useCallback((engine) => {
|
|
2404
|
-
try {
|
|
2405
|
-
runner.launch(wb.export(), { engine });
|
|
2406
|
-
}
|
|
2407
|
-
catch { }
|
|
2408
|
-
}, [runner, wb]);
|
|
2409
|
-
const stop = useCallback(() => runner.dispose(), [runner]);
|
|
2410
|
-
const step = useCallback(() => runner.step(), [runner]);
|
|
2411
|
-
const flush = useCallback(() => runner.flush(), [runner]);
|
|
2412
2506
|
const value = useMemo(() => ({
|
|
2413
2507
|
wb,
|
|
2414
2508
|
runner,
|
|
@@ -2614,6 +2708,42 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2614
2708
|
outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
|
|
2615
2709
|
}
|
|
2616
2710
|
: { inputs: [], outputs: [] };
|
|
2711
|
+
// Render edge status indicator
|
|
2712
|
+
const renderEdgeStatus = useCallback(() => {
|
|
2713
|
+
if (!selectedEdge)
|
|
2714
|
+
return null;
|
|
2715
|
+
const status = edgeStatus?.[selectedEdge.id];
|
|
2716
|
+
if (status?.activeRuns > 0) {
|
|
2717
|
+
return (jsxs("div", { className: "mt-1 text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1", children: [jsxs("div", { className: "font-semibold", children: ["Running (", status.activeRuns, ")"] }), jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "Note: Edge runIds are not available in stats events" })] }));
|
|
2718
|
+
}
|
|
2719
|
+
return null;
|
|
2720
|
+
}, [selectedEdge, edgeStatus]);
|
|
2721
|
+
// Render linked input display value
|
|
2722
|
+
const renderLinkedInputDisplay = useCallback((typeId, current) => {
|
|
2723
|
+
const displayStr = safeToString(typeId, current);
|
|
2724
|
+
const isLong = displayStr.length > 50;
|
|
2725
|
+
const truncated = isLong ? truncateValue(displayStr) : displayStr;
|
|
2726
|
+
return (jsxs("div", { className: "flex items-center gap-1", children: [jsx("span", { className: "truncate", children: truncated }), isLong && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsx(CopyIcon, { size: 14 }) }))] }));
|
|
2727
|
+
}, [safeToString, truncateValue, copyToClipboard]);
|
|
2728
|
+
// Render output validation issues badge
|
|
2729
|
+
const renderOutputValidationBadge = useCallback((handle) => {
|
|
2730
|
+
const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === handle);
|
|
2731
|
+
if (outIssues.length === 0)
|
|
2732
|
+
return null;
|
|
2733
|
+
const outErr = outIssues.some((m) => m.level === "error");
|
|
2734
|
+
const outTitle = outIssues
|
|
2735
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
2736
|
+
.join("; ");
|
|
2737
|
+
return (jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
|
|
2738
|
+
}, [selectedNodeHandleValidation]);
|
|
2739
|
+
// Render output display value
|
|
2740
|
+
const renderOutputDisplay = useCallback((outputValue, effectiveHandle) => {
|
|
2741
|
+
const { typeId, value } = resolveOutputDisplay(outputValue, effectiveHandle);
|
|
2742
|
+
const displayStr = safeToString(typeId, value);
|
|
2743
|
+
const isLong = displayStr.length > 50;
|
|
2744
|
+
const truncated = isLong ? truncateValue(displayStr) : displayStr;
|
|
2745
|
+
return (jsxs("div", { className: "flex items-center gap-1", children: [jsx("span", { className: "truncate", children: truncated }), isLong && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsx(CopyIcon, { size: 14 }) }))] }));
|
|
2746
|
+
}, [safeToString, truncateValue, copyToClipboard]);
|
|
2617
2747
|
// Local drafts and originals for commit-on-blur/enter behavior
|
|
2618
2748
|
const [drafts, setDrafts] = useState({});
|
|
2619
2749
|
const [originals, setOriginals] = useState({});
|
|
@@ -2671,10 +2801,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2671
2801
|
return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsx("div", { className: "break-words", children: err.message }), jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsx("div", { className: "break-words", children: err.message })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Registry Error" }), jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2672
2802
|
e.stopPropagation();
|
|
2673
2803
|
deleteEdgeById(m.data?.edgeId);
|
|
2674
|
-
}, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxs("div", { children: [jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), (() => {
|
|
2675
|
-
const status = edgeStatus?.[selectedEdge.id];
|
|
2676
|
-
return status?.activeRuns > 0 ? (jsxs("div", { className: "mt-1 text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1", children: [jsxs("div", { className: "font-semibold", children: ["Running (", status.activeRuns, ")"] }), jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "Note: Edge runIds are not available in stats events" })] })) : null;
|
|
2677
|
-
})(), jsx("div", { className: "mt-1", children: jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2804
|
+
}, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxs("div", { children: [jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), renderEdgeStatus(), jsx("div", { className: "mt-1", children: jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2678
2805
|
e.stopPropagation();
|
|
2679
2806
|
deleteEdgeById(selectedEdge.id);
|
|
2680
2807
|
}, title: "Delete this edge", children: "Delete edge" }) }), jsxs("div", { className: "flex items-center gap-2 mt-1", children: [jsxs("label", { className: "w-20 flex flex-col", children: [jsx("span", { children: "Type" }), jsx("span", { className: "text-gray-500 text-[11px]", children: "DataTypeId" })] }), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: selectedEdge.typeId ?? "", onChange: (e) => {
|
|
@@ -2753,14 +2880,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2753
2880
|
? `Default: ${placeholder}`
|
|
2754
2881
|
: "(select)" }), registry.enums
|
|
2755
2882
|
.get(typeId)
|
|
2756
|
-
?.options.map((opt) => (jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsx(XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsx("div", { className: "flex items-center gap-1 flex-1", children: jsx("div", { className: "flex-1 min-w-0", children: (
|
|
2757
|
-
const displayStr = safeToString(typeId, current);
|
|
2758
|
-
const isLong = displayStr.length > 50;
|
|
2759
|
-
const truncated = isLong
|
|
2760
|
-
? truncateValue(displayStr)
|
|
2761
|
-
: displayStr;
|
|
2762
|
-
return (jsxs("div", { className: "flex items-center gap-1", children: [jsx("span", { className: "truncate", children: truncated }), isLong && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsx(CopyIcon, { size: 14 }) }))] }));
|
|
2763
|
-
})() }) })) : (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", placeholder: placeholder
|
|
2883
|
+
?.options.map((opt) => (jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsx(XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsx("div", { className: "flex items-center gap-1 flex-1", children: jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", placeholder: placeholder
|
|
2764
2884
|
? `Default: ${placeholder}`
|
|
2765
2885
|
: undefined, value: displayValue, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
|
|
2766
2886
|
if (e.key === "Enter")
|
|
@@ -2768,24 +2888,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2768
2888
|
if (e.key === "Escape")
|
|
2769
2889
|
revert();
|
|
2770
2890
|
}, ...commonProps }), hasValue && !isLinked && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsx(XCircleIcon, { size: 16 }) }))] }))] }, h));
|
|
2771
|
-
}))] }), jsxs("div", { children: [jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-20 flex flex-col", children: [jsx("span", { children: prettyHandle(h) }), jsx("span", { className: "text-gray-500 text-[11px]", children: outputTypesMap[selectedNodeId]?.[h] ?? "" })] }), jsx("div", { className: "flex-1 min-w-0", children: (() => {
|
|
2772
|
-
const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]);
|
|
2773
|
-
const displayStr = safeToString(typeId, value);
|
|
2774
|
-
const isLong = displayStr.length > 50;
|
|
2775
|
-
const truncated = isLong
|
|
2776
|
-
? truncateValue(displayStr)
|
|
2777
|
-
: displayStr;
|
|
2778
|
-
return (jsxs("div", { className: "flex items-center gap-1", children: [jsx("span", { className: "truncate", children: truncated }), isLong && (jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsx(CopyIcon, { size: 14 }) }))] }));
|
|
2779
|
-
})() }), (() => {
|
|
2780
|
-
const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
|
|
2781
|
-
if (outIssues.length === 0)
|
|
2782
|
-
return null;
|
|
2783
|
-
const outErr = outIssues.some((m) => m.level === "error");
|
|
2784
|
-
const outTitle = outIssues
|
|
2785
|
-
.map((v) => `${v.code}: ${v.message}`)
|
|
2786
|
-
.join("; ");
|
|
2787
|
-
return (jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
|
|
2788
|
-
})()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2891
|
+
}))] }), jsxs("div", { children: [jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-20 flex flex-col", children: [jsx("span", { children: prettyHandle(h) }), jsx("span", { className: "text-gray-500 text-[11px]", children: outputTypesMap[selectedNodeId]?.[h] ?? "" })] }), jsx("div", { className: "flex-1 min-w-0", children: renderOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]) }), renderOutputValidationBadge(h)] }, h))))] }), selectedNodeValidation.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2789
2892
|
e.stopPropagation();
|
|
2790
2893
|
deleteEdgeById(m.data?.edgeId);
|
|
2791
2894
|
}, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) })] }), debug && (jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
|
|
@@ -3540,6 +3643,44 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3540
3643
|
? computeEffectiveHandles(selectedNode, registry)
|
|
3541
3644
|
: { inputs: {}, outputs: {}, inputDefaults: {} };
|
|
3542
3645
|
const [exampleState, setExampleState] = useState(example ?? "");
|
|
3646
|
+
const isGraphRunning = runner.isRunning();
|
|
3647
|
+
const engineKind = runner.getRunningEngine();
|
|
3648
|
+
// Render Start/Stop button based on transport and runner state
|
|
3649
|
+
const renderStartStopButton = useCallback(() => {
|
|
3650
|
+
// Check if transport is connecting/retrying
|
|
3651
|
+
const isConnecting = transportStatus.state === "connecting" ||
|
|
3652
|
+
transportStatus.state === "retrying";
|
|
3653
|
+
// Only allow Start/Stop when transport is connected or local
|
|
3654
|
+
const canControl = transportStatus.state === "connected" ||
|
|
3655
|
+
transportStatus.state === "local";
|
|
3656
|
+
if (isConnecting) {
|
|
3657
|
+
return (jsxs("button", { className: "border rounded px-2 py-1.5 text-gray-500 border-gray-400 flex items-center gap-1 disabled:opacity-50", disabled: true, title: "Connecting to backend...", children: [jsx(ClockClockwiseIcon, { size: 16, className: "animate-spin" }), jsx("span", { className: "font-medium ml-1", children: "Connecting..." })] }));
|
|
3658
|
+
}
|
|
3659
|
+
if (isGraphRunning) {
|
|
3660
|
+
return (jsxs("button", { className: "border rounded px-2 py-1.5 text-red-700 border-red-600 flex items-center gap-1 disabled:opacity-50 disabled:text-gray-400 disabled:border-gray-300", onClick: () => runner.stop(), disabled: !canControl, title: canControl ? "Stop engine" : "Waiting for connection", children: [jsx(StopIcon, { size: 16, weight: "fill" }), jsx("span", { className: "font-medium ml-1", children: "Stop" })] }));
|
|
3661
|
+
}
|
|
3662
|
+
return (jsxs("button", { className: "border rounded px-2 py-1.5 text-green-700 border-green-600 flex items-center gap-1 disabled:text-gray-400 disabled:border-gray-300 disabled:opacity-50", onClick: (evt) => {
|
|
3663
|
+
const kind = engine;
|
|
3664
|
+
if (!kind)
|
|
3665
|
+
return alert("Select an engine first.");
|
|
3666
|
+
if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
|
|
3667
|
+
return;
|
|
3668
|
+
try {
|
|
3669
|
+
runner.launch(wb.export(), {
|
|
3670
|
+
engine: kind,
|
|
3671
|
+
invalidate: evt.shiftKey,
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
catch (err) {
|
|
3675
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3676
|
+
alert(message);
|
|
3677
|
+
}
|
|
3678
|
+
}, disabled: !engine || !canControl, title: !engine
|
|
3679
|
+
? "Select an engine first"
|
|
3680
|
+
: !canControl
|
|
3681
|
+
? "Waiting for connection"
|
|
3682
|
+
: "Start engine", children: [jsx(PlayIcon, { size: 16, weight: "fill" }), jsx("span", { className: "font-medium ml-1", children: "Start" })] }));
|
|
3683
|
+
}, [transportStatus, isGraphRunning, runner, engine, wb]);
|
|
3543
3684
|
const defaultExamples = useMemo(() => [
|
|
3544
3685
|
{
|
|
3545
3686
|
id: "simple",
|
|
@@ -3588,7 +3729,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3588
3729
|
return backendKind === "local";
|
|
3589
3730
|
});
|
|
3590
3731
|
// Expose init callback with setInitialGraph helper
|
|
3591
|
-
// Note: This runs whenever runner changes
|
|
3732
|
+
// Note: This runs whenever runner changes
|
|
3592
3733
|
useEffect(() => {
|
|
3593
3734
|
if (!onInit)
|
|
3594
3735
|
return;
|
|
@@ -3764,7 +3905,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3764
3905
|
applyExample(example);
|
|
3765
3906
|
}, [example, wb]);
|
|
3766
3907
|
useEffect(() => {
|
|
3767
|
-
const off = runner.on("transport",
|
|
3908
|
+
const off = runner.on("transport", setTransportStatus);
|
|
3768
3909
|
return () => off();
|
|
3769
3910
|
}, [runner]);
|
|
3770
3911
|
// Track registry readiness for remote backends
|
|
@@ -3785,7 +3926,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3785
3926
|
useEffect(() => {
|
|
3786
3927
|
if (!engine)
|
|
3787
3928
|
return;
|
|
3788
|
-
if (
|
|
3929
|
+
if (isGraphRunning)
|
|
3789
3930
|
return;
|
|
3790
3931
|
// Only auto-launch for local backend; require explicit Start for remote
|
|
3791
3932
|
if (backendKind !== "local")
|
|
@@ -3802,7 +3943,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3802
3943
|
catch {
|
|
3803
3944
|
// ignore
|
|
3804
3945
|
}
|
|
3805
|
-
}, [engine, runner, wb, backendKind]);
|
|
3946
|
+
}, [engine, runner, isGraphRunning, wb, backendKind]);
|
|
3806
3947
|
// Registry is automatically fetched by RemoteGraphRunner when it connects
|
|
3807
3948
|
// Run auto layout after registry is hydrated (for remote backends)
|
|
3808
3949
|
useEffect(() => {
|
|
@@ -3988,30 +4129,34 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3988
4129
|
return overrides.toElement(baseToElement, { registry });
|
|
3989
4130
|
return baseToElement;
|
|
3990
4131
|
}, [overrides, baseToElement, registry]);
|
|
3991
|
-
return (jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [
|
|
3992
|
-
? "Stop engine before switching example"
|
|
3993
|
-
: undefined, children: [jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
|
|
3994
|
-
? "Stop engine before switching backend"
|
|
3995
|
-
: undefined, children: [jsx("option", { value: "local", children: "Local" }), jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && !!onHttpBaseUrlChange && (jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && !!onWsUrlChange && (jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
|
|
4132
|
+
return (jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [isGraphRunning ? (jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", engineKind] })) : (jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsx(PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsx(ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsx(WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsx(WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsx(ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: isGraphRunning, title: isGraphRunning ? "Stop engine before switching example" : undefined, children: [jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: isGraphRunning, title: isGraphRunning ? "Stop engine before switching backend" : undefined, children: [jsx("option", { value: "local", children: "Local" }), jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && !!onHttpBaseUrlChange && (jsx("input", { className: "border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && !!onWsUrlChange && (jsx("input", { className: "border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: engineKind ?? engine ?? "", onChange: async (e) => {
|
|
3996
4133
|
const kind = e.target.value || undefined;
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4134
|
+
const currentEngine = runner.getRunningEngine();
|
|
4135
|
+
// If engine is running and user selected a different engine, switch it
|
|
4136
|
+
if (runner.isRunning() &&
|
|
4137
|
+
currentEngine &&
|
|
4138
|
+
kind &&
|
|
4139
|
+
kind !== currentEngine) {
|
|
4140
|
+
try {
|
|
4141
|
+
await runner.switchEngine({
|
|
4142
|
+
engine: kind,
|
|
4143
|
+
batched: { flushIntervalMs: 0 },
|
|
4144
|
+
hybrid: { windowMs: 250, batchThreshold: 3 },
|
|
4145
|
+
});
|
|
4146
|
+
onEngineChange?.(kind);
|
|
4147
|
+
}
|
|
4148
|
+
catch (err) {
|
|
4149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4150
|
+
alert(`Failed to switch engine: ${message}`);
|
|
4151
|
+
// Reset dropdown to current engine
|
|
4152
|
+
e.target.value = currentEngine;
|
|
4153
|
+
}
|
|
4009
4154
|
}
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4155
|
+
else {
|
|
4156
|
+
// Normal change when not running
|
|
4157
|
+
onEngineChange?.(kind);
|
|
4013
4158
|
}
|
|
4014
|
-
},
|
|
4159
|
+
}, children: [jsx("option", { value: "", children: "Select Engine\u2026" }), jsx("option", { value: "push", children: "Push" }), jsx("option", { value: "batched", children: "Batched" }), jsx("option", { value: "pull", children: "Pull" }), jsx("option", { value: "hybrid", children: "Hybrid" }), jsx("option", { value: "step", children: "Step" })] }), engineKind === "step" && (jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.step(), disabled: !isGraphRunning, title: "Step", children: jsx(PlayPauseIcon, { size: 24 }) })), engineKind === "batched" && (jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.flush(), disabled: !isGraphRunning, title: "Flush", children: jsx(LightningIcon, { size: 24 }) })), renderStartStopButton(), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: runAutoLayout, children: jsx(TreeStructureIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: jsx(CornersOutIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: downloadGraph, children: jsx(DownloadSimpleIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: async () => {
|
|
4015
4160
|
try {
|
|
4016
4161
|
const def = wb.export();
|
|
4017
4162
|
const positions = wb.getPositions();
|
|
@@ -4040,7 +4185,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
4040
4185
|
const message = err instanceof Error ? err.message : String(err);
|
|
4041
4186
|
alert(message);
|
|
4042
4187
|
}
|
|
4043
|
-
}, children: jsx(DownloadIcon, { size: 24 }) }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "
|
|
4188
|
+
}, children: jsx(DownloadIcon, { size: 24 }) }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: triggerUpload, children: jsx(UploadIcon, { size: 24 }) }), jsxs("label", { className: "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: "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 })] })] }));
|
|
4044
4189
|
}
|
|
4045
4190
|
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
|
|
4046
4191
|
const [registry, setRegistry] = useState(createSimpleGraphRegistry());
|
|
@@ -4104,6 +4249,7 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
4104
4249
|
}
|
|
4105
4250
|
};
|
|
4106
4251
|
}, [runner]);
|
|
4252
|
+
const isGraphRunning = runner.isRunning();
|
|
4107
4253
|
// Track UI registration version to trigger nodeTypes recomputation
|
|
4108
4254
|
const [uiVersion, setUiVersion] = useState(0);
|
|
4109
4255
|
// Allow external UI registration (e.g., node renderers) with access to wb
|
|
@@ -4114,11 +4260,12 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
4114
4260
|
setUiVersion((v) => v + 1);
|
|
4115
4261
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4116
4262
|
}, [wb, runner, overrides]);
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4263
|
+
const onBackendKindChangeWithDispose = useCallback(() => (v) => {
|
|
4264
|
+
if (isGraphRunning)
|
|
4265
|
+
runner.dispose();
|
|
4266
|
+
onBackendKindChange(v);
|
|
4267
|
+
}, [isGraphRunning]);
|
|
4268
|
+
return (jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: onBackendKindChangeWithDispose, 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 }) }));
|
|
4122
4269
|
}
|
|
4123
4270
|
|
|
4124
4271
|
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 };
|