@burdenoff/fluidgrids-fe-sdk 2026.1101.1 → 2026.1101.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/{ExecutionDetailPage-4LPCNWM3.js → ExecutionDetailPage-Q77U6YJO.js} +11 -8
  2. package/dist/{ExecutionsListPage-HNPUKYD7.js → ExecutionsListPage-X4H4FVLN.js} +6 -5
  3. package/dist/{WorkflowBuilder-QFSCYCUC.js → WorkflowBuilder-RJPRUMQR.js} +11 -8
  4. package/dist/{WorkflowsList-RV4BRJO7.js → WorkflowsList-R2JJK7BL.js} +7 -6
  5. package/dist/agentTools-MISTH7JI.js +349 -0
  6. package/dist/{chunk-77HIU3CX.js → chunk-26HGYPBC.js} +1 -1
  7. package/dist/{chunk-76UUEMTN.js → chunk-37VUN74D.js} +4 -4
  8. package/dist/{chunk-QPZCJKII.js → chunk-3ERT6FQK.js} +3 -3
  9. package/dist/chunk-472HJ3S3.js +3929 -0
  10. package/dist/{chunk-WNZV4SLU.js → chunk-4QHCXTHW.js} +4 -4
  11. package/dist/chunk-AASHQI77.js +126 -0
  12. package/dist/chunk-CGXQVZGY.js +260 -0
  13. package/dist/chunk-CRDG2SQQ.js +185 -0
  14. package/dist/chunk-DQ2ZCXYX.js +21875 -0
  15. package/dist/chunk-F7DD7BOB.js +1164 -0
  16. package/dist/chunk-GTEBQQ7J.js +24 -0
  17. package/dist/{chunk-ALFMJSD6.js → chunk-H7XPXONT.js} +1 -1
  18. package/dist/{chunk-UO5DUKYN.js → chunk-HKKSXOFO.js} +8 -7
  19. package/dist/chunk-IA2ZCHNQ.js +1336 -0
  20. package/dist/{chunk-3M36XWAH.js → chunk-IGRIN7NL.js} +8 -7
  21. package/dist/chunk-JDLQ6QIB.js +283 -0
  22. package/dist/{chunk-AD7BICOT.js → chunk-JKBCLBHY.js} +1 -1
  23. package/dist/{chunk-6JX5UPDW.js → chunk-JNZCXFLD.js} +1 -1
  24. package/dist/{chunk-SEFOT2LD.js → chunk-LWLHPVIA.js} +1 -1
  25. package/dist/{chunk-JWROEOG5.js → chunk-LYTR6IQO.js} +1 -38
  26. package/dist/chunk-MR2B67NW.js +338 -0
  27. package/dist/{chunk-RIHYPPMQ.js → chunk-NLYQ3XDE.js} +1 -1
  28. package/dist/{chunk-6ILIBJFH.js → chunk-NZJWKVER.js} +7904 -7943
  29. package/dist/chunk-OCJTTHTS.js +46 -0
  30. package/dist/chunk-OSQ3OCR3.js +1193 -0
  31. package/dist/{chunk-VJL3DFJT.js → chunk-REO4S5MY.js} +2 -2
  32. package/dist/{chunk-XKQMCPSB.js → chunk-T2UUYYVJ.js} +4 -4
  33. package/dist/{chunk-BXR6BWXM.js → chunk-W7MTSATB.js} +5 -5
  34. package/dist/{chunk-A47PSD7A.js → chunk-WF6GMBR4.js} +7 -4
  35. package/dist/chunk-WYCB6OQL.js +19709 -0
  36. package/dist/{chunk-YOLPHVOE.js → chunk-YKFR7BZJ.js} +1 -1
  37. package/dist/{chunk-26NENGXA.js → chunk-YSXNJGCY.js} +4 -4
  38. package/dist/code-editor-field-JKCUR6BI.js +3 -0
  39. package/dist/executions.js +12 -9
  40. package/dist/history.js +4 -3
  41. package/dist/index.js +41 -32
  42. package/dist/node-detail-view-PCYFQX62.js +2034 -0
  43. package/dist/observability.js +5 -4
  44. package/dist/quick-config-panel-BRZZHOSI.js +402 -0
  45. package/dist/settings.js +6 -5
  46. package/dist/templates.js +10 -7
  47. package/dist/workflow-builder.js +12 -9
  48. package/dist/workflows-list.js +7 -6
  49. package/package.json +4 -1
  50. package/dist/chunk-NMEI7TQW.js +0 -2628
  51. package/dist/chunk-S7XV65DB.js +0 -50350
@@ -1,8 +1,8 @@
1
- import { ActionPoliciesTab } from './chunk-AD7BICOT.js';
2
- import { LogStreamingTab } from './chunk-RIHYPPMQ.js';
3
- import { AgentsTab } from './chunk-YOLPHVOE.js';
1
+ import { AgentsTab } from './chunk-YKFR7BZJ.js';
2
+ import { ActionPoliciesTab } from './chunk-JKBCLBHY.js';
3
+ import { LogStreamingTab } from './chunk-NLYQ3XDE.js';
4
4
  import { FluidGridsDocsLink } from './chunk-4VJ3MDOV.js';
5
- import { useFlowStore, useFluidGridsFlag, FLUIDGRIDS_FLAGS, useSaveStatusStore, findSaveConflict, useDurableParityEnabled, throwIfResultError, describeOperationError, useConnections } from './chunk-6ILIBJFH.js';
5
+ import { useFlowStore, useFluidGridsFlag, FLUIDGRIDS_FLAGS, useSaveStatusStore, findSaveConflict, useDurableParityEnabled, throwIfResultError, describeOperationError, useConnections } from './chunk-NZJWKVER.js';
6
6
  import { useFluidGridsPermission, FLUIDGRIDS_PERMISSIONS } from './chunk-ZUVFD5KV.js';
7
7
  import { getGlobalStoreClient } from './chunk-PXIFUMLA.js';
8
8
  import { getFluidGridsConfig } from './chunk-6AMNEX2Y.js';
@@ -0,0 +1,126 @@
1
+ import { useBuilderReadOnly, toCodeMirrorDiagnostics } from './chunk-OCJTTHTS.js';
2
+ import { useMemo, useRef, useEffect, useState } from 'react';
3
+ import CodeMirror, { EditorView } from '@uiw/react-codemirror';
4
+ import { lintGutter, setDiagnostics } from '@codemirror/lint';
5
+ import { cn } from '@burdenoff/fe-libs/utils';
6
+ import { jsx } from 'react/jsx-runtime';
7
+
8
+ function useLanguageExtension(language) {
9
+ const [extension, setExtension] = useState(null);
10
+ useEffect(() => {
11
+ let cancelled = false;
12
+ const loaded = language === "python" ? import('@codemirror/lang-python').then((m) => m.python()) : import('@codemirror/lang-javascript').then(
13
+ (m) => m.javascript({ typescript: language === "typescript" })
14
+ );
15
+ void loaded.then((ext) => {
16
+ if (!cancelled) setExtension(ext);
17
+ });
18
+ return () => {
19
+ cancelled = true;
20
+ };
21
+ }, [language]);
22
+ return extension;
23
+ }
24
+ function normalizeCodeLanguage(value) {
25
+ return typeof value === "string" && value.toLowerCase().startsWith("py") ? "python" : "javascript";
26
+ }
27
+ function useIsDarkTheme() {
28
+ const [dark, setDark] = useState(
29
+ () => typeof document !== "undefined" && document.documentElement.classList.contains("dark")
30
+ );
31
+ useEffect(() => {
32
+ const root = document.documentElement;
33
+ const observer = new MutationObserver(
34
+ () => setDark(root.classList.contains("dark"))
35
+ );
36
+ observer.observe(root, { attributes: true, attributeFilter: ["class"] });
37
+ return () => observer.disconnect();
38
+ }, []);
39
+ return dark;
40
+ }
41
+ function CodeEditorField({
42
+ value,
43
+ language,
44
+ placeholder,
45
+ label,
46
+ onChange,
47
+ readOnly: readOnlyProp = false,
48
+ diagnostics,
49
+ minHeight = "180px",
50
+ maxHeight = "420px",
51
+ className,
52
+ "data-testid": testId
53
+ }) {
54
+ const dark = useIsDarkTheme();
55
+ const withLint = diagnostics !== void 0;
56
+ const builderReadOnly = useBuilderReadOnly();
57
+ const readOnly = readOnlyProp || builderReadOnly;
58
+ const languageExtension = useLanguageExtension(language);
59
+ const extensions = useMemo(
60
+ () => [
61
+ ...languageExtension ? [languageExtension] : [],
62
+ // CodeMirror's contenteditable is the control assistive tech focuses; name it.
63
+ ...label ? [EditorView.contentAttributes.of({ "aria-label": label })] : [],
64
+ ...withLint ? [lintGutter()] : []
65
+ ],
66
+ [languageExtension, label, withLint]
67
+ );
68
+ const viewRef = useRef(null);
69
+ useEffect(() => {
70
+ const view = viewRef.current;
71
+ if (!view || diagnostics === void 0) return;
72
+ view.dispatch(
73
+ setDiagnostics(
74
+ view.state,
75
+ toCodeMirrorDiagnostics(view.state.doc.toString(), diagnostics)
76
+ )
77
+ );
78
+ }, [diagnostics]);
79
+ return /* @__PURE__ */ jsx(
80
+ "div",
81
+ {
82
+ "data-testid": testId,
83
+ className: cn(
84
+ "overflow-hidden rounded-md border border-input text-sm transition-colors focus-within:border-primary",
85
+ className
86
+ ),
87
+ onKeyDown: (e) => e.stopPropagation(),
88
+ onMouseDown: (e) => e.stopPropagation(),
89
+ children: /* @__PURE__ */ jsx(
90
+ CodeMirror,
91
+ {
92
+ value,
93
+ onChange: readOnly ? void 0 : onChange,
94
+ extensions,
95
+ theme: dark ? "dark" : "light",
96
+ placeholder,
97
+ minHeight,
98
+ maxHeight,
99
+ editable: !readOnly,
100
+ readOnly,
101
+ onCreateEditor: (view) => {
102
+ viewRef.current = view;
103
+ if (diagnostics?.length)
104
+ view.dispatch(
105
+ setDiagnostics(
106
+ view.state,
107
+ toCodeMirrorDiagnostics(view.state.doc.toString(), diagnostics)
108
+ )
109
+ );
110
+ },
111
+ basicSetup: {
112
+ lineNumbers: true,
113
+ foldGutter: true,
114
+ highlightActiveLine: !readOnly,
115
+ autocompletion: !readOnly,
116
+ bracketMatching: true,
117
+ closeBrackets: !readOnly,
118
+ indentOnInput: !readOnly
119
+ }
120
+ }
121
+ )
122
+ }
123
+ );
124
+ }
125
+
126
+ export { CodeEditorField, normalizeCodeLanguage };
@@ -0,0 +1,260 @@
1
+ import { throwIfResultError } from './chunk-NZJWKVER.js';
2
+ import { useTr } from './chunk-UULVIT25.js';
3
+ import { getFluidGridsClient } from './chunk-NTWG3CJX.js';
4
+ import { useState, useRef, useCallback, useEffect } from 'react';
5
+ import { gql } from '@apollo/client';
6
+ import { cn } from '@burdenoff/fe-libs/utils';
7
+ import { jsx, jsxs } from 'react/jsx-runtime';
8
+ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@burdenoff/fe-libs/ui';
9
+
10
+ var RUNNER_FIELDS = gql`
11
+ fragment SandboxRunnerFields on SandboxRunner {
12
+ id
13
+ name
14
+ status
15
+ hostname
16
+ os
17
+ arch
18
+ driver
19
+ version
20
+ capacity
21
+ privateNetwork {
22
+ target
23
+ ports
24
+ nodes
25
+ }
26
+ tokenPrefix
27
+ lastSeenAt
28
+ connectedAt
29
+ createdBy
30
+ createdAt
31
+ }
32
+ `;
33
+ var LIST_RUNNERS = gql`
34
+ ${RUNNER_FIELDS}
35
+ query SandboxRunners {
36
+ sandboxRunners(context: {}) {
37
+ ...SandboxRunnerFields
38
+ }
39
+ }
40
+ `;
41
+ var CREATE_RUNNER = gql`
42
+ ${RUNNER_FIELDS}
43
+ mutation CreateSandboxRunner($name: String!) {
44
+ createSandboxRunner(input: { name: $name, context: {} }) {
45
+ token
46
+ bridgeUrl
47
+ runner {
48
+ ...SandboxRunnerFields
49
+ }
50
+ }
51
+ }
52
+ `;
53
+ var ROTATE_RUNNER_TOKEN = gql`
54
+ ${RUNNER_FIELDS}
55
+ mutation RotateSandboxRunnerToken($id: ID!) {
56
+ rotateSandboxRunnerToken(id: $id, context: {}) {
57
+ token
58
+ bridgeUrl
59
+ runner {
60
+ ...SandboxRunnerFields
61
+ }
62
+ }
63
+ }
64
+ `;
65
+ var DELETE_RUNNER = gql`
66
+ mutation DeleteSandboxRunner($id: ID!) {
67
+ deleteSandboxRunner(id: $id, context: {})
68
+ }
69
+ `;
70
+ var RUNNER_INSTALL_SCRIPT_URL = "https://www.fluidgrids.ai/downloads/runner/install.sh";
71
+ function runnerInstallCommand(bridgeUrl) {
72
+ const base = `curl -fsSL ${RUNNER_INSTALL_SCRIPT_URL} | sh`;
73
+ return bridgeUrl && bridgeUrl !== DEFAULT_BRIDGE_URL ? `${base} -s -- --url ${bridgeUrl}` : base;
74
+ }
75
+ var DEFAULT_BRIDGE_URL = "wss://graphqlworkspaces.burdenoff.com/runner-bridge";
76
+ function useRunners(options) {
77
+ const [runners, setRunners] = useState([]);
78
+ const [loading, setLoading] = useState(!options?.skip);
79
+ const [error, setError] = useState(null);
80
+ const mounted = useRef(true);
81
+ const fetchRunners = useCallback(async () => {
82
+ if (options?.skip) return;
83
+ try {
84
+ const result = await getFluidGridsClient().query({ query: LIST_RUNNERS, fetchPolicy: "network-only" });
85
+ if (!mounted.current) return;
86
+ setRunners(result.data?.sandboxRunners ?? []);
87
+ setError(null);
88
+ } catch (err) {
89
+ if (mounted.current) setError(err);
90
+ } finally {
91
+ if (mounted.current) setLoading(false);
92
+ }
93
+ }, [options?.skip]);
94
+ useEffect(() => {
95
+ mounted.current = true;
96
+ void fetchRunners();
97
+ const pollMs = options?.pollMs ?? 1e4;
98
+ const timer = pollMs > 0 ? setInterval(fetchRunners, pollMs) : null;
99
+ return () => {
100
+ mounted.current = false;
101
+ if (timer) clearInterval(timer);
102
+ };
103
+ }, [fetchRunners, options?.pollMs]);
104
+ return { runners, loading, error, refetch: fetchRunners };
105
+ }
106
+ function useRunnerActions() {
107
+ const [busy, setBusy] = useState(false);
108
+ const run = useCallback(async (fn) => {
109
+ setBusy(true);
110
+ try {
111
+ return await fn();
112
+ } finally {
113
+ setBusy(false);
114
+ }
115
+ }, []);
116
+ const createRunner = useCallback(
117
+ (name) => run(async () => {
118
+ const result = throwIfResultError(
119
+ await getFluidGridsClient().mutate({ mutation: CREATE_RUNNER, variables: { name } })
120
+ );
121
+ if (!result.data?.createSandboxRunner)
122
+ throw new Error("No runner returned");
123
+ return result.data.createSandboxRunner;
124
+ }),
125
+ [run]
126
+ );
127
+ const rotateToken = useCallback(
128
+ (id) => run(async () => {
129
+ const result = throwIfResultError(
130
+ await getFluidGridsClient().mutate({ mutation: ROTATE_RUNNER_TOKEN, variables: { id } })
131
+ );
132
+ if (!result.data?.rotateSandboxRunnerToken)
133
+ throw new Error("No runner returned");
134
+ return result.data.rotateSandboxRunnerToken;
135
+ }),
136
+ [run]
137
+ );
138
+ const deleteRunner = useCallback(
139
+ (id) => run(async () => {
140
+ const result = throwIfResultError(
141
+ await getFluidGridsClient().mutate({
142
+ mutation: DELETE_RUNNER,
143
+ variables: { id }
144
+ })
145
+ );
146
+ return result.data?.deleteSandboxRunner === true;
147
+ }),
148
+ [run]
149
+ );
150
+ return { createRunner, rotateToken, deleteRunner, busy };
151
+ }
152
+ var STYLES = {
153
+ ONLINE: "border-status-success-border bg-status-success-bg-subtle text-status-success-text",
154
+ OFFLINE: "border-border-default bg-bg-sunken text-text-muted",
155
+ NEVER_CONNECTED: "border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text"
156
+ };
157
+ function RunnerStatusPill({
158
+ status,
159
+ className
160
+ }) {
161
+ const tr = useTr();
162
+ const label = status === "ONLINE" ? tr("fluidgrids.runners.online", "Online") : status === "OFFLINE" ? tr("fluidgrids.runners.offline", "Offline") : tr("fluidgrids.runners.neverConnected", "Not connected yet");
163
+ return /* @__PURE__ */ jsx(
164
+ "span",
165
+ {
166
+ className: cn(
167
+ "inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium",
168
+ STYLES[status],
169
+ className
170
+ ),
171
+ "data-testid": "runner-status",
172
+ "data-status": status,
173
+ children: label
174
+ }
175
+ );
176
+ }
177
+ var CLOUD = "__fluidgrids_cloud__";
178
+ function RunOnSection({
179
+ value,
180
+ onChange,
181
+ sectionLabelClassName
182
+ }) {
183
+ const tr = useTr();
184
+ const { runners, loading } = useRunners({ pollMs: 15e3 });
185
+ const selected = value ? runners.find((r) => r.id === value.runnerId) : void 0;
186
+ return /* @__PURE__ */ jsxs("section", { className: "space-y-2", "data-testid": "ndv-settings-run-on", children: [
187
+ /* @__PURE__ */ jsx("div", { className: sectionLabelClassName, children: tr("fluidgrids.builder.nodeSettings.runOn", "Run on") }),
188
+ /* @__PURE__ */ jsxs(
189
+ Select,
190
+ {
191
+ value: value?.runnerId ?? CLOUD,
192
+ onValueChange: (v) => onChange(v === CLOUD ? null : { kind: "runner", runnerId: v }),
193
+ children: [
194
+ /* @__PURE__ */ jsx(
195
+ SelectTrigger,
196
+ {
197
+ className: "w-full",
198
+ "data-testid": "ndv-settings-run-on-select",
199
+ children: /* @__PURE__ */ jsx(SelectValue, {})
200
+ }
201
+ ),
202
+ /* @__PURE__ */ jsxs(
203
+ SelectContent,
204
+ {
205
+ style: { zIndex: 1100 },
206
+ "data-testid": "ndv-settings-run-on-options",
207
+ children: [
208
+ /* @__PURE__ */ jsx(SelectItem, { value: CLOUD, "data-testid": "run-on-cloud", children: tr(
209
+ "fluidgrids.builder.nodeSettings.runOnCloud",
210
+ "FluidGrids Cloud (default)"
211
+ ) }),
212
+ runners.map((runner) => /* @__PURE__ */ jsxs(
213
+ SelectItem,
214
+ {
215
+ value: runner.id,
216
+ "data-testid": "run-on-runner-option",
217
+ children: [
218
+ runner.name,
219
+ runner.status !== "ONLINE" ? ` (${runner.status === "OFFLINE" ? tr("fluidgrids.runners.offline", "Offline") : tr("fluidgrids.runners.neverConnected", "Not connected yet")})` : ""
220
+ ]
221
+ },
222
+ runner.id
223
+ )),
224
+ value && !loading && !selected && /* @__PURE__ */ jsx(SelectItem, { value: value.runnerId, disabled: true, children: tr(
225
+ "fluidgrids.builder.nodeSettings.runOnMissing",
226
+ "Removed runner"
227
+ ) })
228
+ ]
229
+ }
230
+ )
231
+ ]
232
+ }
233
+ ),
234
+ value ? /* @__PURE__ */ jsxs("div", { className: "space-y-1 text-xs text-text-secondary", children: [
235
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
236
+ selected && /* @__PURE__ */ jsx(RunnerStatusPill, { status: selected.status }),
237
+ /* @__PURE__ */ jsx("span", { children: tr(
238
+ "fluidgrids.builder.nodeSettings.runOnRunnerHint",
239
+ "This node runs on your runner, in a local sandbox. If the runner is offline the node fails \u2014 it never falls back to FluidGrids Cloud. The node's input and the credentials it uses are sent to that machine."
240
+ ) })
241
+ ] }),
242
+ selected && /* @__PURE__ */ jsx("p", { "data-testid": "run-on-private-network", children: (selected.privateNetwork ?? []).length === 0 ? tr(
243
+ "fluidgrids.builder.nodeSettings.runOnPublicOnly",
244
+ "On this runner, nodes reach the public internet only. Private destinations are opened on the runner machine (fluidgrids-runner network allow)."
245
+ ) : tr(
246
+ "fluidgrids.builder.nodeSettings.runOnPrivateNetwork",
247
+ "This runner opens {{count}} private destination(s) \u2014 see Runners for which nodes may use them.",
248
+ { count: (selected.privateNetwork ?? []).length }
249
+ ) })
250
+ ] }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-text-secondary", children: runners.length === 0 && !loading ? tr(
251
+ "fluidgrids.builder.nodeSettings.runOnNoRunners",
252
+ "Runs in FluidGrids Cloud. Add a runner under Workflows \u2192 Runners to run nodes on your own machine."
253
+ ) : tr(
254
+ "fluidgrids.builder.nodeSettings.runOnCloudHint",
255
+ "Runs in FluidGrids Cloud."
256
+ ) })
257
+ ] });
258
+ }
259
+
260
+ export { RUNNER_INSTALL_SCRIPT_URL, RunOnSection, RunnerStatusPill, runnerInstallCommand, useRunnerActions, useRunners };
@@ -0,0 +1,185 @@
1
+ import { STICKY_NOTE_TYPE, isNodeTrigger, isLocked, useEditLeaseStore, useSaveStatusStore, canAutosave, editStateOf, workflowFingerprint, snapshotForSave, leaveNeedsConfirmation, runPreparationOf, useFlowStore, resolveInputHandles } from './chunk-NZJWKVER.js';
2
+ import { getConnectedEdges, useReactFlow } from '@xyflow/react';
3
+ import { useState, useCallback } from 'react';
4
+
5
+ function findEdges(node, connectedEdges) {
6
+ const outgoingEdges = connectedEdges.filter(
7
+ (edge) => edge.source === node.id
8
+ );
9
+ const incomingEdges = connectedEdges.filter(
10
+ (edge) => edge.target === node.id
11
+ );
12
+ return { outgoingEdges, incomingEdges };
13
+ }
14
+ function checkFlowTerminalNode(outgoingEdges, incomingEdges, isTrigger, type) {
15
+ if (isTrigger) {
16
+ return outgoingEdges.length >= 1;
17
+ }
18
+ switch (type) {
19
+ case "end" /* END */:
20
+ return incomingEdges.length >= 1;
21
+ default:
22
+ return false;
23
+ }
24
+ }
25
+ function checkLoneNode(outgoingEdges, incomingEdges, isTrigger) {
26
+ if (isTrigger) {
27
+ return outgoingEdges.length === 0;
28
+ }
29
+ return incomingEdges.length === 0;
30
+ }
31
+ function isFlowComplete(nodes, edges) {
32
+ const connectedEdges = getConnectedEdges(nodes, edges);
33
+ let isTriggerConnected = false;
34
+ let hasTriggerNode = false;
35
+ let hasEndNode = false;
36
+ let isEndConnected = false;
37
+ const nodesWithEmptyTarget = [];
38
+ for (const node of nodes) {
39
+ if (node.type === STICKY_NOTE_TYPE) continue;
40
+ const { outgoingEdges, incomingEdges } = findEdges(node, connectedEdges);
41
+ const isTrigger = isNodeTrigger(node);
42
+ if (isTrigger) {
43
+ hasTriggerNode = true;
44
+ if (checkFlowTerminalNode(
45
+ outgoingEdges,
46
+ incomingEdges,
47
+ true,
48
+ node.type
49
+ )) {
50
+ isTriggerConnected = true;
51
+ }
52
+ } else if (node.type === "end" /* END */) {
53
+ hasEndNode = true;
54
+ isEndConnected = checkFlowTerminalNode(
55
+ outgoingEdges,
56
+ incomingEdges,
57
+ false,
58
+ "end" /* END */
59
+ );
60
+ }
61
+ if (checkLoneNode(outgoingEdges, incomingEdges, isTrigger))
62
+ nodesWithEmptyTarget.push(node);
63
+ }
64
+ const hasAnyLoneNode = nodesWithEmptyTarget.length > 0;
65
+ const isEndValid = !hasEndNode || isEndConnected;
66
+ return hasTriggerNode && isTriggerConnected && isEndValid && !hasAnyLoneNode;
67
+ }
68
+ function useFlowValidator(onValidate) {
69
+ const [isValidating, setIsValidating] = useState(false);
70
+ const { getNodes, getEdges } = useReactFlow();
71
+ const validate = useCallback(async () => {
72
+ setIsValidating(true);
73
+ await new Promise((resolve) => setTimeout(resolve, 300));
74
+ onValidate?.(isFlowComplete(getNodes(), getEdges()));
75
+ setIsValidating(false);
76
+ }, [getNodes, getEdges, onValidate]);
77
+ return [isValidating, validate];
78
+ }
79
+
80
+ // src/builder/lib/save-guards.ts
81
+ function editingLeaseLocked() {
82
+ return isLocked(useEditLeaseStore.getState().phase);
83
+ }
84
+ function autosaveAllowedFor(workflow) {
85
+ return useSaveStatusStore.getState().autosaveEnabled && canAutosave(workflow);
86
+ }
87
+ function editStateFor(state) {
88
+ return editStateOf(
89
+ useSaveStatusStore.getState().baseline,
90
+ {
91
+ fingerprint: workflowFingerprint(state.workflow),
92
+ generation: state.loadGeneration,
93
+ editRevision: state.editRevision
94
+ },
95
+ Date.now()
96
+ );
97
+ }
98
+ function departingSave(state) {
99
+ const status = useSaveStatusStore.getState();
100
+ if (status.autosavePaused || editingLeaseLocked()) return null;
101
+ if (editStateFor(state) !== "dirty") return null;
102
+ if (!autosaveAllowedFor(state.workflow)) return null;
103
+ const { nodes, edges } = state.workflow;
104
+ if (!isFlowComplete(nodes, edges)) return null;
105
+ return snapshotForSave(state.workflow, state.loadGeneration);
106
+ }
107
+ function touchesSavedFields(state, previous) {
108
+ return state.workflow.nodes !== previous.workflow.nodes || state.workflow.edges !== previous.workflow.edges || state.workflow.name !== previous.workflow.name || state.workflow.alias !== previous.workflow.alias || state.workflow.status !== previous.workflow.status || state.loadGeneration !== previous.loadGeneration;
109
+ }
110
+ function unsavedState() {
111
+ const flow = useFlowStore.getState();
112
+ const status = useSaveStatusStore.getState();
113
+ return {
114
+ // Checked fresh: the dirty flag trails the last edit by a moment.
115
+ dirty: status.dirty || editStateFor(flow) === "dirty",
116
+ // A paused autosave (a refused save), or a page someone else is editing
117
+ // from, does not save on the way out either.
118
+ autosaveAllowed: autosaveAllowedFor(flow.workflow) && !status.autosavePaused && !editingLeaseLocked(),
119
+ incomplete: status.incomplete || !isFlowComplete(flow.workflow.nodes, flow.workflow.edges),
120
+ failed: status.failure !== null
121
+ };
122
+ }
123
+ function leavingLosesChanges() {
124
+ return leaveNeedsConfirmation(unsavedState());
125
+ }
126
+ function runPreparation() {
127
+ return runPreparationOf(unsavedState());
128
+ }
129
+ function savedEverythingBeforeLeaving() {
130
+ return editStateFor(useFlowStore.getState()) !== "dirty";
131
+ }
132
+ function watchLeavingLosesChanges(onChange, delayMs) {
133
+ let current;
134
+ let timer;
135
+ const report = (losesChanges) => {
136
+ if (losesChanges === current) return;
137
+ current = losesChanges;
138
+ onChange(losesChanges);
139
+ };
140
+ const settle = () => {
141
+ timer = void 0;
142
+ report(leavingLosesChanges());
143
+ };
144
+ const settleLater = () => {
145
+ if (timer) clearTimeout(timer);
146
+ timer = setTimeout(settle, delayMs);
147
+ };
148
+ const settleNow = () => {
149
+ if (timer) clearTimeout(timer);
150
+ settle();
151
+ };
152
+ settle();
153
+ const unsubscribeFlow = useFlowStore.subscribe((state, previous) => {
154
+ if (!touchesSavedFields(state, previous)) return;
155
+ if (state.loadGeneration === previous.loadGeneration) report(true);
156
+ settleLater();
157
+ });
158
+ const unsubscribeStatus = useSaveStatusStore.subscribe(settleLater);
159
+ const unsubscribeLease = useEditLeaseStore.subscribe((state, previous) => {
160
+ if (isLocked(state.phase) !== isLocked(previous.phase)) settleNow();
161
+ });
162
+ return () => {
163
+ unsubscribeFlow();
164
+ unsubscribeStatus();
165
+ unsubscribeLease();
166
+ if (timer) clearTimeout(timer);
167
+ };
168
+ }
169
+
170
+ // src/builder/hooks/attach-node-helpers.ts
171
+ var ATTACH_GAP_X = 120;
172
+ var DEFAULT_NODE_WIDTH = 288;
173
+ function computeAttachPosition(source) {
174
+ if (!source) return void 0;
175
+ const width = source.measured?.width ?? DEFAULT_NODE_WIDTH;
176
+ return {
177
+ x: source.position.x + width + ATTACH_GAP_X,
178
+ y: source.position.y
179
+ };
180
+ }
181
+ function firstInputHandleId(data) {
182
+ return resolveInputHandles(data)[0]?.id ?? "target";
183
+ }
184
+
185
+ export { autosaveAllowedFor, computeAttachPosition, departingSave, editStateFor, editingLeaseLocked, firstInputHandleId, isFlowComplete, leavingLosesChanges, runPreparation, savedEverythingBeforeLeaving, touchesSavedFields, useFlowValidator, watchLeavingLosesChanges };