@burdenoff/fluidgrids-fe-sdk 2026.1113.1 → 2026.1113.2

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 (127) hide show
  1. package/dist/{AiStudioPage-D3MAk-bs.js → AiStudioPage-5KYZ8r8-.js} +464 -7
  2. package/dist/{ApprovalInboxPage-Bb8H9ruw.js → ApprovalInboxPage-BsJX4osU.js} +4 -3
  3. package/dist/{ConnectionFormDialog-Bg9_OdHX.js → ConnectionFormDialog-Dr1-CTan.js} +2 -1
  4. package/dist/CreateFluidgridsTagDocument-Dtp9KPLJ.js +1055 -0
  5. package/dist/{DataSinkDashboardPage-Dc2QuJbL.js → DataSinkDashboardPage-8ZDn85e2.js} +1 -1
  6. package/dist/{DataTablesPage-Cv4veaKK.js → DataTablesPage-BBvABuxE.js} +5 -4
  7. package/dist/{EntityTagPicker-D8ws706D.js → EntityTagPicker-BppBSEJk.js} +2 -2
  8. package/dist/{ExecutionDetailPage-CTVGE-d-.js → ExecutionDetailPage-BXDF2eoT.js} +1444 -18
  9. package/dist/ExecutionsListDocument-CzdLPpj5.js +1259 -0
  10. package/dist/{ExecutionsListPage-J0sJPuoM.js → ExecutionsListPage--4BthM8L.js} +285 -11
  11. package/dist/FailureTriagePanel-B5WCSRFo.js +1252 -0
  12. package/dist/{FluidGridsProvider-C4FVNz0n.js → FluidGridsProvider-Dc_Jhtch.js} +8666 -8734
  13. package/dist/{HistoryPage-B9XcTPrO.js → HistoryPage-BvFk2m1m.js} +4 -3
  14. package/dist/{InsightsPage-DFOltjyj.js → InsightsPage-Dwmb02F6.js} +2682 -5
  15. package/dist/{MissingNodesInstallModal-D8H-YNCJ.js → MissingNodesInstallModal-Cze674hp.js} +2 -2
  16. package/dist/{NodesPage-YQ-hP60x.js → NodesPage-C5Dq-ru-.js} +3 -2
  17. package/dist/{OpenTemplateAsCodeButton-hH26HQCk.js → OpenTemplateAsCodeButton-8MZHih6I.js} +2 -2
  18. package/dist/{PaidTemplateConfirm-DoKWMBtR.js → PaidTemplateConfirm-0G-gv29n.js} +1 -1
  19. package/dist/{PublishCustomNodeAction-5XXnSA3H.js → PublishCustomNodeAction-OOY35dUv.js} +1 -1
  20. package/dist/QuotaExhaustedDialog-wvu_-WJ-.js +55 -0
  21. package/dist/{RunnerStatusPill-Ctg4TJcP.js → RunnerStatusPill-Bh7mEsPh.js} +2 -1
  22. package/dist/{RunnersPage-CAt4YDhB.js → RunnersPage-DHeuUDil.js} +2 -2
  23. package/dist/{SettingsPage-DEKPCVVP.js → SettingsPage-CfjXiGpb.js} +694 -14
  24. package/dist/{TagFilterSelect-CwM2-DU6.js → TagFilterSelect-CAShgvbS.js} +1 -1
  25. package/dist/{TaskQueuesPage-Dhi6SIT4.js → TaskQueuesPage--Mmn36b-.js} +3 -3
  26. package/dist/{TemplateCard-DVSd_4zJ.js → TemplateCard-BA_9em22.js} +644 -352
  27. package/dist/{TemplateConfigurationModal-BKi1z7z7.js → TemplateConfigurationModal-BT3PaR0O.js} +4 -3
  28. package/dist/{TemplateDetailPanel-DK1gF7P5.js → TemplateDetailPanel-BxFNnlXm.js} +2 -1
  29. package/dist/{TemplatePreview-DuSX2Q7H.js → TemplatePreview-Bagja6WN.js} +7 -7
  30. package/dist/{TemplatesPage-CAWHh61l.js → TemplatesPage-CAajpJ5w.js} +711 -6
  31. package/dist/{VariablesPage-D7CoatWf.js → VariablesPage-CgUBKhAe.js} +3 -3
  32. package/dist/{VersionDiffDialog-DGwysiyE.js → VersionDiffDialog-D9W5Fm56.js} +233 -129
  33. package/dist/VersionLifecycleBar-DuphLOT0.js +131 -0
  34. package/dist/{WorkflowBuilder-DAeoQi5E.js → WorkflowBuilder-DMpSPElu.js} +16 -13
  35. package/dist/{WorkflowBuilder-BJgqDboZ.js → WorkflowBuilder-v8h8skwG.js} +5 -5
  36. package/dist/WorkflowDefinitionDocument-DZLbTPUZ.js +321 -0
  37. package/dist/WorkflowEditLeaseStatusDocument-Dp62med3.js +627 -0
  38. package/dist/WorkflowProposalCard-zCdUpCk8.js +249 -0
  39. package/dist/WorkflowProposalDocument-BdUbsrd0.js +620 -0
  40. package/dist/{WorkflowSearchRedirect-CMEvd_AR.js → WorkflowSearchRedirect-G78lINAE.js} +1 -1
  41. package/dist/{WorkflowShareDialog-BgHRsDP0.js → WorkflowShareDialog-B7VOgr4-.js} +304 -3
  42. package/dist/WorkflowVersionsDocument-CfPMloRW.js +3195 -0
  43. package/dist/{WorkflowsList-CcQPxWKL.js → WorkflowsList-CPYzfE4K.js} +11 -9
  44. package/dist/agent-form-D0uLvzx4.js +718 -0
  45. package/dist/{agentTools-DKpNKztf.js → agentTools-DTXUqtfu.js} +4 -4
  46. package/dist/api-BZVyX5d2.js +2286 -0
  47. package/dist/api-BqA-f-AD.js +8064 -0
  48. package/dist/app-BJUvugPM.js +3813 -0
  49. package/dist/assert-rules-CPNrO44K.js +318 -0
  50. package/dist/attach-node-helpers-CNsYtLOR.js +32 -0
  51. package/dist/badge-CXfPhoMH.js +26 -0
  52. package/dist/{canvasNoteTools-C_AN79m0.js → canvasNoteTools-DmfZwtnL.js} +3 -3
  53. package/dist/{channel-CZ70oI81.js → channel-DtF1n_ab.js} +1 -1
  54. package/dist/client-Ctl4jmY3.js +127 -0
  55. package/dist/code-editor-diagnostics-Zr0cIcx1.js +46 -0
  56. package/dist/{code-editor-field-Dt6KNWwN.js → code-editor-field-Cv907s3W.js} +2 -2
  57. package/dist/{codec-client-CJ95Rmqo.js → codec-client-CAYl6j46.js} +2 -1
  58. package/dist/conditionBuilder-D8aLGL1w.js +1134 -0
  59. package/dist/config-X97zrIvn.js +600 -0
  60. package/dist/{definition-canvas-Bshn_dSP.js → definition-canvas-DL25_6NH.js} +1 -1
  61. package/dist/{dialog-Cjb4lB4D.js → dialog-BVd152GS.js} +3 -14
  62. package/dist/durable-nodes-DXCCQYWD.js +81 -0
  63. package/dist/errors-xPZ-IbQd.js +142 -0
  64. package/dist/executions.js +2 -2
  65. package/dist/{flow-builder-D3Hl2AFk.js → flow-builder-Dv5fWJ6F.js} +9301 -8189
  66. package/dist/{node-registry-refresh-Dy0uRg9f.js → global-client-DP_Wxe5t.js} +2 -27
  67. package/dist/global-operations-7pvXw_j9.js +145 -0
  68. package/dist/history.js +1 -1
  69. package/dist/{index-CWcP-tG1.d.ts → index-DtMUtXNq.d.ts} +247 -247
  70. package/dist/index.d.ts +1 -1
  71. package/dist/index.js +4403 -46
  72. package/dist/input-ChT6Q-Pr.js +17 -0
  73. package/dist/logs-panel-DtG93ocy.js +342 -0
  74. package/dist/mapping-colors-BIX_YeCQ.js +48 -0
  75. package/dist/{node-detail-view-bVLeYarT.js → node-detail-view-BKI_2kM6.js} +12 -11
  76. package/dist/{node-editor-tab-body-JHHVtQgR.js → node-editor-tab-body-Cc1MWEFR.js} +3 -3
  77. package/dist/{node-operation-selection-DAbS6jSY.js → node-operation-selection-Czeeb2D4.js} +2 -139
  78. package/dist/node-registry-refresh-s3okf83X.js +27 -0
  79. package/dist/nodes.js +1 -1
  80. package/dist/notesKey-5EaREBGA.js +12 -0
  81. package/dist/observability.d.ts +1 -1
  82. package/dist/observability.js +2 -2
  83. package/dist/{popover-Xx3gN7vg.js → popover-DDv7J1Kl.js} +717 -6
  84. package/dist/{quick-config-panel-BvVQXuIL.js → quick-config-panel-BPCRfqWI.js} +4 -4
  85. package/dist/read-only-context-Dt9N9SIG.js +18 -0
  86. package/dist/{schedule-panel-DCb40TPN.js → schedule-panel-C1KwfLCE.js} +14 -12
  87. package/dist/settings.js +1 -1
  88. package/dist/{shared-CMfn30xT.js → shared-Ctl9ZHo1.js} +2 -1
  89. package/dist/{sidebar-node-editor-DxCWFZG2.js → sidebar-node-editor-d9Rlq5pU.js} +4 -4
  90. package/dist/sticky-notes-23j-wL6Q.js +96 -0
  91. package/dist/template-code-dqRunGOv.js +184 -0
  92. package/dist/template-preview-C01KV277.js +29 -0
  93. package/dist/templates.js +2 -2
  94. package/dist/token-refresh-link-BjN7H4qA.js +291 -0
  95. package/dist/useAgentDefinitions-ZKX2N9aP.js +3101 -0
  96. package/dist/useConnections-BlB0gbOB.js +931 -0
  97. package/dist/useDurableParity-BgLH_SCF.js +2088 -0
  98. package/dist/{useEntityTags-Db4Pc8_M.js → useEntityTags-4iW6UTly.js} +227 -3
  99. package/dist/{useExecutions-DOpG1pUQ.js → useExecutions-Bm7q_UwN.js} +6 -3
  100. package/dist/useGlobalStoreClient-DMTrywdX.js +24 -0
  101. package/dist/{useHumanRequests-ZiR0sSzS.js → useHumanRequests-DwG2tQ3f.js} +2 -1
  102. package/dist/{useObservability-rQmwhoZZ.js → useObservability-DPATdbw7.js} +4 -2
  103. package/dist/{useRunHistory-COn6dl7l.js → useRunHistory-CGfjCsJ3.js} +1122 -4
  104. package/dist/{useVariables-2ddyJX4a.js → useVariables-DFdZofLQ.js} +2 -1
  105. package/dist/{useWorkflowCodeExport-ZoUJKRSK.js → useWorkflowCodeExport-D_mCuhKP.js} +289 -4
  106. package/dist/useWorkflowOrganization-r5FWNBs6.js +1690 -0
  107. package/dist/useWorkflowProposal-Bh2_qjmM.js +7478 -0
  108. package/dist/{useWorkflows-jOW60YmD.js → useWorkflows-aqVD7kGa.js} +694 -4
  109. package/dist/workflow-builder.js +5 -5
  110. package/dist/{workflow-code-transfer-l9wO34S0.js → workflow-code-transfer-5C0HUzae.js} +1 -53
  111. package/dist/workflow-host.js +4 -2
  112. package/dist/workflow-pages.d.ts +1 -1
  113. package/dist/workflow-pages.js +19 -19
  114. package/dist/workflow-preview.js +3 -2
  115. package/dist/workflows-list.js +1 -1
  116. package/package.json +1 -1
  117. package/dist/FailureTriagePanel-BbYFYeV_.js +0 -311
  118. package/dist/WorkflowProposalCard-DGMdZC4R.js +0 -1432
  119. package/dist/agent-form-Bfvm32j1.js +0 -83
  120. package/dist/api-DQtHXbIg.js +0 -224
  121. package/dist/api-MeAm6g7E.js +0 -55
  122. package/dist/client-C9XSzSXr.js +0 -1147
  123. package/dist/graphql-operation-types-C9qbvRa8.js +0 -53122
  124. package/dist/useAgentDefinitions-_hgNCK1A.js +0 -550
  125. package/dist/useConnections-Cs7LCWEM.js +0 -401
  126. package/dist/useDurableParity-CcLeCDfv.js +0 -504
  127. package/dist/useWorkflowOrganization-QxVePSkk.js +0 -282
@@ -1,1432 +0,0 @@
1
- "use client";
2
- import { _ as throwIfResultError, h as isForbiddenError, n as getFluidGridsClient, r as getFluidGridsClientSafe } from "./client-C9XSzSXr.js";
3
- import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
4
- import { $t as RejectWorkflowProposalDocument, En as RunWorkflowTestsDocument, Jt as ProposeWorkflowChangeDocument, Q as DeleteWorkflowTestCaseDocument, R as CreateWorkflowTestCaseDocument, Si as WorkflowTestRunsDocument, Yr as WorkflowProposalDocument, bi as WorkflowTestRunDocument, l as ApplyWorkflowProposalDocument, mr as UpdateWorkflowTestCaseDocument, qt as ProposeRunFixDocument, s as AgentTokenDeltaDocument, w as CancelWorkflowProposalDocument, yi as WorkflowTestCasesDocument, z as CreateWorkflowTestCaseFromRunDocument } from "./graphql-operation-types-C9qbvRa8.js";
5
- import { MI as WorkflowTestExpectedStatus, NI as WorkflowTestRunSource, PI as WorkflowTestRunStatus, jI as WorkflowTestAssertionOperator, jd as ErrorType } from "./graphql-types-v6CTGwA0.js";
6
- import { a as notifyFluidGridsAiCreditsUpdated } from "./aiCreditsBridge-BuE92tKo.js";
7
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
- import { Icon } from "@iconify/react";
9
- import { jsx, jsxs } from "react/jsx-runtime";
10
- import { Badge, Button } from "@burdenoff/fe-libs/ui";
11
-
12
- // src/modules/executions/utils/agent-token-stream.ts
13
- /** Longest reply kept per iteration; the PLAN step holds the full text. */
14
- const AGENT_TOKEN_STREAM_MAX_CHARS = 2e5;
15
- /** The key of one model turn: node execution and iteration. */
16
- function agentTokenStreamKey(delta) {
17
- return `${delta.nodeId}|${delta.nodeStateId ?? ""}|${delta.iteration}`;
18
- }
19
- function capped(text) {
20
- return text.length > 2e5 ? {
21
- text: text.slice(0, AGENT_TOKEN_STREAM_MAX_CHARS),
22
- truncated: true
23
- } : {
24
- text,
25
- truncated: false
26
- };
27
- }
28
- function isDelta(value) {
29
- if (typeof value !== "object" || value === null) return false;
30
- const delta = value;
31
- return typeof delta.nodeId === "string" && typeof delta.iteration === "number" && Number.isInteger(delta.iteration) && delta.iteration >= 0 && typeof delta.offset === "number" && Number.isInteger(delta.offset) && delta.offset >= 0 && typeof delta.delta === "string";
32
- }
33
- /** The text of a stream after one delta (see the module comment). */
34
- function applyText(current, delta) {
35
- const before = current?.text ?? "";
36
- if (delta.reset) return {
37
- text: delta.delta,
38
- gap: delta.offset > 0
39
- };
40
- if (delta.offset === before.length) return {
41
- text: before + delta.delta,
42
- gap: current?.gap ?? false
43
- };
44
- if (delta.offset < before.length) return {
45
- text: before.slice(0, delta.offset) + delta.delta,
46
- gap: current?.gap ?? false
47
- };
48
- return {
49
- text: before + delta.delta,
50
- gap: true
51
- };
52
- }
53
- /**
54
- * `current` with a batch of deltas applied, in order. Returns `current` itself
55
- * when nothing in the batch was usable, so callers can skip a re-render.
56
- */
57
- function applyAgentTokenDeltas(current, deltas, now = Date.now()) {
58
- let next = null;
59
- for (const raw of deltas) {
60
- if (!isDelta(raw)) continue;
61
- const nodeStateId = raw.nodeStateId ?? null;
62
- const key = agentTokenStreamKey({
63
- ...raw,
64
- nodeStateId
65
- });
66
- const held = (next ?? current).get(key);
67
- if (held?.done && !raw.reset) continue;
68
- next ??= new Map(current);
69
- const { text, gap } = applyText(held, raw);
70
- const limited = capped(text);
71
- next.set(key, {
72
- key,
73
- runId: raw.runId,
74
- nodeId: raw.nodeId,
75
- nodeStateId,
76
- iteration: raw.iteration,
77
- text: limited.text,
78
- done: raw.done === true,
79
- error: typeof raw.error === "string" && raw.error.length > 0 ? raw.error : null,
80
- gap,
81
- truncated: limited.truncated || (held?.truncated ?? false),
82
- order: held?.order ?? next.size,
83
- updatedAt: now
84
- });
85
- }
86
- return next ?? current;
87
- }
88
- /** The streams in the order they started (node execution, then iteration). */
89
- function sortedAgentTokenStreams(streams) {
90
- return [...streams.values()].sort((a, b) => a.order - b.order);
91
- }
92
- /** The streams of one AI Agent node execution, by iteration. */
93
- function agentTokenStreamsFor(streams, nodeStateId, nodeId) {
94
- return streams.filter((stream) => nodeStateId !== null && stream.nodeStateId !== null ? stream.nodeStateId === nodeStateId : stream.nodeId === nodeId).sort((a, b) => a.iteration - b.iteration);
95
- }
96
- /**
97
- * The state of a stream for the reader: a failed call, a finished one, one
98
- * still being written, or one the run stopped writing (cancelled, or the
99
- * worker went away) before the turn ended.
100
- */
101
- function agentLiveReplyState(stream, runLive) {
102
- if (stream.error) return "failed";
103
- if (stream.done) return "done";
104
- return runLive ? "streaming" : "stopped";
105
- }
106
- /**
107
- * The stream the builder's chat shows: the newest turn of the newest node
108
- * execution of an AI Agent node (`agentNodeIds`), or of any node when that set
109
- * is empty.
110
- */
111
- function latestAgentTokenStream(streams, agentNodeIds) {
112
- let latest = null;
113
- for (const stream of streams.values()) {
114
- if (agentNodeIds && agentNodeIds.size > 0) {
115
- if (!agentNodeIds.has(stream.nodeId)) continue;
116
- }
117
- if (!latest || stream.order > latest.order) latest = stream;
118
- }
119
- return latest;
120
- }
121
-
122
- // src/modules/executions/hooks/useAgentTokenStream.ts
123
- /**
124
- * useAgentTokenStream — a run's AI Agent model replies while the model is
125
- * still writing them (BOFF-9938), for the run page's Agent tab and the
126
- * builder's chat panel.
127
- *
128
- * Subscribes to `agentTokenDelta(runId)` while `enabled`, and folds every
129
- * batch into one stream per model turn (node execution and iteration, see
130
- * `agent-token-stream.ts`). The stream carries no history: a turn that started
131
- * before the subscription opened shows only what arrives after. Turning
132
- * `enabled` off closes the subscription but keeps the streams already held,
133
- * so a reader can keep showing them until the step trace has the final text.
134
- * A different `runId` starts from nothing.
135
- */
136
- const EMPTY_STREAMS = /* @__PURE__ */ new Map();
137
- /**
138
- * Follow a run's model replies: subscribe to `agentTokenDelta` and hand each
139
- * non-empty batch to `onDeltas`. Returns the function that closes the
140
- * subscription; nothing is called back after it ran. A failed or ended
141
- * subscription only reports `onStreaming(false)`: the step trace and the node
142
- * output still carry every reply, so the stream is an accelerator.
143
- */
144
- function followAgentTokenDeltas(client, runId, { onDeltas, onStreaming }) {
145
- let stopped = false;
146
- const subscription = client.subscribe({
147
- query: AgentTokenDeltaDocument,
148
- variables: { runId }
149
- }).subscribe({
150
- next: ({ data }) => {
151
- if (stopped) return;
152
- onStreaming(true);
153
- const deltas = data?.agentTokenDelta ?? [];
154
- if (deltas.length > 0) onDeltas(deltas, Date.now());
155
- },
156
- error: () => {
157
- if (!stopped) onStreaming(false);
158
- },
159
- complete: () => {
160
- if (!stopped) onStreaming(false);
161
- }
162
- });
163
- return () => {
164
- stopped = true;
165
- subscription.unsubscribe();
166
- onStreaming(false);
167
- };
168
- }
169
- function useAgentTokenStream(runId, { enabled }) {
170
- const [state, setState] = useState(null);
171
- const [streaming, setStreaming] = useState(false);
172
- useEffect(() => {
173
- const client = getFluidGridsClientSafe();
174
- if (!runId || !enabled || !client) return void 0;
175
- return followAgentTokenDeltas(client, runId, {
176
- onStreaming: setStreaming,
177
- onDeltas: (deltas, receivedAt) => setState((previous) => {
178
- const held = previous?.runId === runId ? previous.streams : EMPTY_STREAMS;
179
- const streams = applyAgentTokenDeltas(held, deltas, receivedAt);
180
- if (streams === held && previous?.runId === runId) return previous;
181
- return {
182
- runId,
183
- streams,
184
- lastDeltaAt: receivedAt
185
- };
186
- })
187
- });
188
- }, [runId, enabled]);
189
- const current = runId && state?.runId === runId ? state : null;
190
- const byKey = current?.streams ?? EMPTY_STREAMS;
191
- return {
192
- streams: useMemo(() => sortedAgentTokenStreams(byKey), [byKey]),
193
- byKey,
194
- streaming: enabled && !!runId && streaming,
195
- lastDeltaAt: current?.lastDeltaAt ?? null
196
- };
197
- }
198
-
199
- // src/builder/lib/assert-rules.ts
200
- /** Every operator, in the order the editors offer them. */
201
- const ASSERT_OPERATORS = [
202
- {
203
- wire: "equals",
204
- value: "EQUALS",
205
- operand: "expected",
206
- label: "equals"
207
- },
208
- {
209
- wire: "notEquals",
210
- value: "NOT_EQUALS",
211
- operand: "expected",
212
- label: "does not equal"
213
- },
214
- {
215
- wire: "contains",
216
- value: "CONTAINS",
217
- operand: "expected",
218
- label: "contains"
219
- },
220
- {
221
- wire: "notContains",
222
- value: "NOT_CONTAINS",
223
- operand: "expected",
224
- label: "does not contain"
225
- },
226
- {
227
- wire: "matches",
228
- value: "MATCHES",
229
- operand: "expected",
230
- label: "matches regex"
231
- },
232
- {
233
- wire: "jsonSchema",
234
- value: "JSON_SCHEMA",
235
- operand: "expected",
236
- label: "matches JSON Schema"
237
- },
238
- {
239
- wire: "between",
240
- value: "BETWEEN",
241
- operand: "range",
242
- label: "is between"
243
- },
244
- {
245
- wire: "itemCount",
246
- value: "ITEM_COUNT",
247
- operand: "range",
248
- label: "has item count"
249
- },
250
- {
251
- wire: "exists",
252
- value: "EXISTS",
253
- operand: "none",
254
- label: "exists"
255
- },
256
- {
257
- wire: "notExists",
258
- value: "NOT_EXISTS",
259
- operand: "none",
260
- label: "does not exist"
261
- }
262
- ];
263
- /** The service's cap on a `matches` pattern. */
264
- const MAX_REGEX_LENGTH = 500;
265
- /** Rules per node (Assert node or test-case expectation). */
266
- const MAX_RULES = 50;
267
- /**
268
- * The spec of an operator given in either spelling (`itemCount`,
269
- * `ITEM_COUNT`), or null when it is not one.
270
- */
271
- function operatorSpec(value) {
272
- if (typeof value !== "string") return null;
273
- return ASSERT_OPERATORS.find((spec) => spec.wire === value || spec.value === value) ?? null;
274
- }
275
- function isRecord$1(value) {
276
- return typeof value === "object" && value !== null && !Array.isArray(value);
277
- }
278
- function optionalNumber(value) {
279
- if (value === void 0 || value === null) return void 0;
280
- return typeof value === "number" && Number.isFinite(value) ? value : false;
281
- }
282
- /**
283
- * Checks one rule and normalises it to the wire form, or reports the first
284
- * problem. `index` is the rule's position, for the message.
285
- */
286
- function normaliseAssertRule(raw, index) {
287
- if (!isRecord$1(raw)) return { problem: {
288
- code: "notObject",
289
- index
290
- } };
291
- const spec = operatorSpec(raw.operator);
292
- if (!spec) return { problem: {
293
- code: "unknownOperator",
294
- index,
295
- operator: String(raw.operator ?? "")
296
- } };
297
- const rule = { operator: spec.wire };
298
- if (raw.path !== void 0 && raw.path !== null && raw.path !== "") {
299
- if (typeof raw.path !== "string") return { problem: {
300
- code: "pathInvalid",
301
- index
302
- } };
303
- rule.path = raw.path;
304
- }
305
- if (raw.message !== void 0 && raw.message !== null && raw.message !== "") {
306
- if (typeof raw.message !== "string") return { problem: {
307
- code: "messageInvalid",
308
- index
309
- } };
310
- rule.message = raw.message;
311
- }
312
- if (spec.operand === "expected") {
313
- if (!("expected" in raw)) return { problem: {
314
- code: "expectedRequired",
315
- index
316
- } };
317
- if (spec.wire === "matches") {
318
- if (typeof raw.expected !== "string") return { problem: {
319
- code: "regexInvalid",
320
- index
321
- } };
322
- if (raw.expected.length > 500) return { problem: {
323
- code: "regexTooLong",
324
- index
325
- } };
326
- try {
327
- new RegExp(raw.expected);
328
- } catch {
329
- return { problem: {
330
- code: "regexInvalid",
331
- index
332
- } };
333
- }
334
- }
335
- if (spec.wire === "jsonSchema" && !isRecord$1(raw.expected)) return { problem: {
336
- code: "schemaInvalid",
337
- index
338
- } };
339
- rule.expected = raw.expected;
340
- }
341
- if (spec.operand === "range") {
342
- const min = optionalNumber(raw.min);
343
- const max = optionalNumber(raw.max);
344
- if (min === false || max === false) return { problem: {
345
- code: "rangeInvalid",
346
- index
347
- } };
348
- if (min === void 0 && max === void 0) return { problem: {
349
- code: "rangeRequired",
350
- index
351
- } };
352
- if (min !== void 0 && max !== void 0 && min > max) return { problem: {
353
- code: "rangeInvalid",
354
- index
355
- } };
356
- if (min !== void 0) rule.min = min;
357
- if (max !== void 0) rule.max = max;
358
- }
359
- return { rule };
360
- }
361
- /** Checks a list of rules (1 to {@link MAX_RULES}); the first problem wins. */
362
- function normaliseAssertRules(raw) {
363
- if (raw.length === 0) return { problem: { code: "empty" } };
364
- if (raw.length > 50) return { problem: {
365
- code: "tooMany",
366
- count: raw.length
367
- } };
368
- const rules = [];
369
- for (const [index, item] of raw.entries()) {
370
- const result = normaliseAssertRule(item, index);
371
- if ("problem" in result) return result;
372
- rules.push(result.rule);
373
- }
374
- return { rules };
375
- }
376
- /** An i18n key and its English fallback for a rule problem. */
377
- function describeAssertRuleProblem(problem) {
378
- const at = "index" in problem ? `Rule ${problem.index + 1}: ` : "";
379
- switch (problem.code) {
380
- case "notObject": return {
381
- key: "fluidgrids.builder.assertRules.notObject",
382
- fallback: `${at}a rule must be an object.`
383
- };
384
- case "unknownOperator": return {
385
- key: "fluidgrids.builder.assertRules.unknownOperator",
386
- fallback: `${at}unknown operator "${problem.operator}". Use ${ASSERT_OPERATORS.map((spec) => spec.wire).join(", ")}.`
387
- };
388
- case "expectedRequired": return {
389
- key: "fluidgrids.builder.assertRules.expectedRequired",
390
- fallback: `${at}this operator needs an expected value.`
391
- };
392
- case "rangeRequired": return {
393
- key: "fluidgrids.builder.assertRules.rangeRequired",
394
- fallback: `${at}set a minimum, a maximum or both.`
395
- };
396
- case "rangeInvalid": return {
397
- key: "fluidgrids.builder.assertRules.rangeInvalid",
398
- fallback: `${at}minimum and maximum must be numbers, with the minimum not above the maximum.`
399
- };
400
- case "regexInvalid": return {
401
- key: "fluidgrids.builder.assertRules.regexInvalid",
402
- fallback: `${at}the expected value is not a valid regular expression.`
403
- };
404
- case "regexTooLong": return {
405
- key: "fluidgrids.builder.assertRules.regexTooLong",
406
- fallback: `${at}a regular expression may have at most ${500} characters.`
407
- };
408
- case "schemaInvalid": return {
409
- key: "fluidgrids.builder.assertRules.schemaInvalid",
410
- fallback: `${at}the expected value must be a JSON Schema object.`
411
- };
412
- case "pathInvalid": return {
413
- key: "fluidgrids.builder.assertRules.pathInvalid",
414
- fallback: `${at}the path must be text.`
415
- };
416
- case "messageInvalid": return {
417
- key: "fluidgrids.builder.assertRules.messageInvalid",
418
- fallback: `${at}the message must be text.`
419
- };
420
- case "tooMany": return {
421
- key: "fluidgrids.builder.assertRules.tooMany",
422
- fallback: `At most ${50} rules (found ${problem.count}).`
423
- };
424
- case "empty": return {
425
- key: "fluidgrids.builder.assertRules.empty",
426
- fallback: "Add at least one rule."
427
- };
428
- }
429
- }
430
- /**
431
- * The rules held in an Assert node's `rules` setting. The manifest declares it
432
- * as text, so it is usually a JSON string; an array is accepted as well. A
433
- * value that does not parse yields null, so the editor can fall back to the
434
- * raw text instead of discarding it.
435
- */
436
- function readAssertRulesSetting(value) {
437
- if (value === void 0 || value === null || value === "") return [];
438
- if (Array.isArray(value)) return value;
439
- if (typeof value !== "string") return null;
440
- try {
441
- const parsed = JSON.parse(value);
442
- return Array.isArray(parsed) ? parsed : null;
443
- } catch {
444
- return null;
445
- }
446
- }
447
- function emptyAssertRuleRow() {
448
- return {
449
- path: "",
450
- operator: "equals",
451
- expected: "",
452
- min: "",
453
- max: "",
454
- message: ""
455
- };
456
- }
457
- function rowNumber(value) {
458
- return typeof value === "number" && Number.isFinite(value) ? String(value) : "";
459
- }
460
- function rowExpected(operator, value) {
461
- if (value === void 0) return "";
462
- if (operator === "matches" && typeof value === "string") return value;
463
- return JSON.stringify(value);
464
- }
465
- /** The editor rows of stored rules (either operator spelling). */
466
- function rowsFromRules(rules) {
467
- return rules.map((raw) => {
468
- const rule = isRecord$1(raw) ? raw : {};
469
- const operator = operatorSpec(rule.operator)?.wire ?? (typeof rule.operator === "string" ? rule.operator : "equals");
470
- return {
471
- path: typeof rule.path === "string" ? rule.path : "",
472
- operator,
473
- expected: rowExpected(operator, rule.expected),
474
- min: rowNumber(rule.min),
475
- max: rowNumber(rule.max),
476
- message: typeof rule.message === "string" ? rule.message : ""
477
- };
478
- });
479
- }
480
- function parseExpected(operator, text) {
481
- if (operator === "matches") return text;
482
- try {
483
- return JSON.parse(text);
484
- } catch {
485
- return text;
486
- }
487
- }
488
- function parseRowNumber(text) {
489
- const trimmed = text.trim();
490
- if (trimmed === "") return void 0;
491
- const value = Number(trimmed);
492
- return Number.isFinite(value) ? value : trimmed;
493
- }
494
- /**
495
- * The stored form of an editor row. Only the operands its operator uses are
496
- * kept, so switching an operator does not leave a stale `expected` behind.
497
- */
498
- function ruleFromRow(row) {
499
- const spec = operatorSpec(row.operator);
500
- const rule = { operator: row.operator };
501
- if (row.path.trim()) rule.path = row.path.trim();
502
- if (spec?.operand === "expected" && (row.expected !== "" || spec.wire === "matches")) rule.expected = parseExpected(spec.wire, row.expected);
503
- if (spec?.operand === "range") {
504
- const min = parseRowNumber(row.min);
505
- const max = parseRowNumber(row.max);
506
- if (min !== void 0) rule.min = min;
507
- if (max !== void 0) rule.max = max;
508
- }
509
- if (row.message.trim()) rule.message = row.message.trim();
510
- return rule;
511
- }
512
-
513
- // src/builder/lib/workflow-tests.ts
514
- /**
515
- * Workflow test suites (BOFF-9960): the editable form of a saved test case,
516
- * its conversion to the GraphQL input, and the suite-status helpers the Tests
517
- * panel and "Save as test case" share.
518
- *
519
- * A person edits the JSON parts of a case as text. `draftToInput` parses and
520
- * checks them the way wspace-workflow-svc does (shapes, counts, the 256 KiB
521
- * cap) so a mistake shows next to its field instead of as a refused save. The
522
- * service still enforces every limit itself.
523
- */
524
- /** JSON bytes per case (trigger data, pinned outputs, mocks, expectations). */
525
- const MAX_CASE_JSON_BYTES = 262144;
526
- /** Mocks, pinned nodes and checked nodes per case. */
527
- const MAX_CASE_ENTRIES = 100;
528
- /** Name length. */
529
- const MAX_CASE_NAME = 200;
530
- function emptyTestCaseDraft() {
531
- return {
532
- name: "",
533
- description: "",
534
- enabled: true,
535
- expectedStatus: "COMPLETED",
536
- triggerNodeId: "",
537
- triggerData: "{}",
538
- pinnedOutputs: "{}",
539
- connectionMocks: "[]",
540
- expectations: "{}"
541
- };
542
- }
543
- function pretty(value) {
544
- return JSON.stringify(value, null, 2);
545
- }
546
- function isRecord(value) {
547
- return typeof value === "object" && value !== null && !Array.isArray(value);
548
- }
549
- /** The rules of an expectation as the wire (and the editor) spells them. */
550
- function wireRule(rule) {
551
- const wire = { operator: operatorSpec(rule.operator)?.wire ?? String(rule.operator) };
552
- if (rule.path) wire.path = rule.path;
553
- if (rule.expected !== null && rule.expected !== void 0) wire.expected = rule.expected;
554
- if (rule.min !== null && rule.min !== void 0) wire.min = rule.min;
555
- if (rule.max !== null && rule.max !== void 0) wire.max = rule.max;
556
- if (rule.message) wire.message = rule.message;
557
- return wire;
558
- }
559
- /** The editable form of a saved case. */
560
- function draftFromTestCase(testCase) {
561
- const expectations = {};
562
- for (const expectation of testCase.expectations) expectations[expectation.nodeId] = expectation.rules.map(wireRule);
563
- const mocks = testCase.connectionMocks.map((mock) => {
564
- const entry = { nodeId: mock.nodeId };
565
- if (mock.operation) entry.operation = mock.operation;
566
- if (mock.error) entry.error = mock.error.errorType ? {
567
- message: mock.error.message,
568
- errorType: mock.error.errorType
569
- } : { message: mock.error.message };
570
- else entry.response = mock.response ?? {};
571
- return entry;
572
- });
573
- return {
574
- name: testCase.name,
575
- description: testCase.description ?? "",
576
- enabled: testCase.enabled,
577
- expectedStatus: testCase.expectedStatus,
578
- triggerNodeId: testCase.triggerNodeId ?? "",
579
- triggerData: pretty(testCase.triggerData ?? {}),
580
- pinnedOutputs: pretty(testCase.pinnedOutputs ?? {}),
581
- connectionMocks: pretty(mocks),
582
- expectations: pretty(expectations)
583
- };
584
- }
585
- /**
586
- * The pinned outputs of the open canvas in the test-case shape, without the
587
- * pin metadata (`pinnedAt`, `pinnedBy`).
588
- */
589
- function pinnedOutputsFromCanvas(pinned) {
590
- const outputs = {};
591
- for (const [nodeId, entry] of Object.entries(pinned ?? {})) if (entry && isRecord(entry.data)) outputs[nodeId] = { data: entry.data };
592
- return outputs;
593
- }
594
- function parseJson(text, field, label) {
595
- const source = text.trim();
596
- if (source === "") return { value: void 0 };
597
- try {
598
- return { value: JSON.parse(source) };
599
- } catch (err) {
600
- return { error: {
601
- field,
602
- key: "fluidgrids.builder.workflowTests.invalidJson",
603
- fallback: `${label} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
604
- } };
605
- }
606
- }
607
- function fieldError(field, key, fallback) {
608
- return { error: {
609
- field,
610
- key: `fluidgrids.builder.workflowTests.${key}`,
611
- fallback
612
- } };
613
- }
614
- function parseTriggerData(text) {
615
- const parsed = parseJson(text, "triggerData", "Trigger data");
616
- if ("error" in parsed) return parsed;
617
- if (parsed.value === void 0) return { value: {} };
618
- if (!isRecord(parsed.value)) return fieldError("triggerData", "triggerDataShape", "Trigger data must be a JSON object.");
619
- return { value: parsed.value };
620
- }
621
- function parsePinnedOutputs(text) {
622
- const parsed = parseJson(text, "pinnedOutputs", "Pinned outputs");
623
- if ("error" in parsed) return parsed;
624
- if (parsed.value === void 0) return { value: {} };
625
- if (!isRecord(parsed.value)) return fieldError("pinnedOutputs", "pinnedShape", "Pinned outputs must be an object of { \"nodeId\": { \"data\": { ... } } }.");
626
- const entries = Object.entries(parsed.value);
627
- if (entries.length > 100) return fieldError("pinnedOutputs", "pinnedTooMany", `At most ${100} pinned nodes.`);
628
- const outputs = {};
629
- for (const [nodeId, entry] of entries) {
630
- if (!isRecord(entry) || !isRecord(entry.data)) return fieldError("pinnedOutputs", "pinnedShape", `Pinned output "${nodeId}" must be { "data": { ... } }.`);
631
- outputs[nodeId] = { data: entry.data };
632
- }
633
- return { value: outputs };
634
- }
635
- const ERROR_TYPES = new Set(Object.values(ErrorType));
636
- function isErrorType(value) {
637
- return ERROR_TYPES.has(value);
638
- }
639
- function parseConnectionMocks(text) {
640
- const parsed = parseJson(text, "connectionMocks", "Connection mocks");
641
- if ("error" in parsed) return parsed;
642
- if (parsed.value === void 0) return { value: [] };
643
- if (!Array.isArray(parsed.value)) return fieldError("connectionMocks", "mocksShape", "Connection mocks must be an array of { \"nodeId\", \"response\" } or { \"nodeId\", \"error\" }.");
644
- if (parsed.value.length > 100) return fieldError("connectionMocks", "mocksTooMany", `At most ${100} connection mocks.`);
645
- const mocks = [];
646
- for (const [index, raw] of parsed.value.entries()) {
647
- const at = `Mock ${index + 1}`;
648
- if (!isRecord(raw) || typeof raw.nodeId !== "string" || !raw.nodeId) return fieldError("connectionMocks", "mockNodeRequired", `${at} needs a "nodeId".`);
649
- if (raw.operation !== void 0 && raw.operation !== null && typeof raw.operation !== "string") return fieldError("connectionMocks", "mockOperationInvalid", `${at}: "operation" must be text.`);
650
- const hasResponse = raw.response !== void 0 && raw.response !== null;
651
- if (hasResponse === (raw.error !== void 0 && raw.error !== null)) return fieldError("connectionMocks", "mockResponseOrError", `${at} must set exactly one of "response" and "error".`);
652
- const mock = { nodeId: raw.nodeId };
653
- if (typeof raw.operation === "string" && raw.operation) mock.operation = raw.operation;
654
- if (hasResponse) mock.response = jsonScalar(raw.response);
655
- else {
656
- const error = raw.error;
657
- if (!isRecord(error) || typeof error.message !== "string") return fieldError("connectionMocks", "mockErrorShape", `${at}: "error" must be { "message": "...", "errorType"?: "..." }.`);
658
- const errorType = error.errorType;
659
- if (errorType !== void 0 && errorType !== null) {
660
- if (typeof errorType !== "string" || !isErrorType(errorType)) return fieldError("connectionMocks", "mockErrorType", `${at}: "errorType" must be one of ${[...ERROR_TYPES].join(", ")}.`);
661
- mock.error = {
662
- message: error.message,
663
- errorType
664
- };
665
- } else mock.error = { message: error.message };
666
- }
667
- mocks.push(mock);
668
- }
669
- return { value: mocks };
670
- }
671
- /**
672
- * A JSON value for a GraphQL `JSON` field. Codegen maps the scalar to an
673
- * object type, but the service accepts any JSON value (a rule's `expected` is
674
- * often a string or a number, a mock's `response` an array), so the parsed
675
- * value is passed through unchanged.
676
- */
677
- function jsonScalar(value) {
678
- return value;
679
- }
680
- /** A normalised rule as the GraphQL input (UPPER_SNAKE operator). */
681
- function ruleInput(rule) {
682
- const spec = operatorSpec(rule.operator);
683
- if (!spec) return [];
684
- const input = { operator: spec.value };
685
- if (rule.path !== void 0) input.path = rule.path;
686
- if (rule.expected !== void 0) input.expected = jsonScalar(rule.expected);
687
- if (rule.min !== void 0) input.min = rule.min;
688
- if (rule.max !== void 0) input.max = rule.max;
689
- if (rule.message !== void 0) input.message = rule.message;
690
- return [input];
691
- }
692
- function expectationEntries(value) {
693
- if (isRecord(value)) return Object.entries(value);
694
- if (Array.isArray(value)) {
695
- const entries = [];
696
- for (const item of value) {
697
- if (!isRecord(item)) return null;
698
- entries.push([item.nodeId, item.rules]);
699
- }
700
- return entries;
701
- }
702
- return null;
703
- }
704
- function parseExpectations(text) {
705
- const parsed = parseJson(text, "expectations", "Expectations");
706
- if ("error" in parsed) return parsed;
707
- if (parsed.value === void 0) return { value: [] };
708
- const entries = expectationEntries(parsed.value);
709
- if (!entries) return fieldError("expectations", "expectationsShape", "Expectations must be an object of { \"nodeId\": [rules] }.");
710
- if (entries.length > 100) return fieldError("expectations", "expectationsTooMany", `At most ${100} checked nodes.`);
711
- const expectations = [];
712
- for (const [nodeId, rules] of entries) {
713
- if (typeof nodeId !== "string" || !nodeId || !Array.isArray(rules)) return fieldError("expectations", "expectationsShape", "Expectations must be an object of { \"nodeId\": [rules] }.");
714
- const result = normaliseAssertRules(rules);
715
- if ("problem" in result) {
716
- const problem = describeAssertRuleProblem(result.problem);
717
- return { error: {
718
- field: "expectations",
719
- key: problem.key,
720
- fallback: `${nodeId}: ${problem.fallback}`
721
- } };
722
- }
723
- expectations.push({
724
- nodeId,
725
- rules: result.rules.flatMap(ruleInput)
726
- });
727
- }
728
- return { value: expectations };
729
- }
730
- /**
731
- * The GraphQL input for a draft, or the first problem found. Updating a case
732
- * replaces every field, so the whole input is always sent.
733
- */
734
- function draftToInput(draft, sortOrder) {
735
- const name = draft.name.trim();
736
- if (!name) return fieldError("name", "nameRequired", "Give the test case a name.");
737
- if (name.length > 200) return fieldError("name", "nameTooLong", `A name may have at most ${200} characters.`);
738
- const triggerData = parseTriggerData(draft.triggerData);
739
- if ("error" in triggerData) return triggerData;
740
- const pinnedOutputs = parsePinnedOutputs(draft.pinnedOutputs);
741
- if ("error" in pinnedOutputs) return pinnedOutputs;
742
- const connectionMocks = parseConnectionMocks(draft.connectionMocks);
743
- if ("error" in connectionMocks) return connectionMocks;
744
- const expectations = parseExpectations(draft.expectations);
745
- if ("error" in expectations) return expectations;
746
- const bytes = new TextEncoder().encode(JSON.stringify([
747
- triggerData.value,
748
- pinnedOutputs.value,
749
- connectionMocks.value,
750
- expectations.value
751
- ])).length;
752
- if (bytes > 262144) return fieldError("size", "tooLarge", `The case holds ${Math.ceil(bytes / 1024)} KiB of JSON; the limit is ${MAX_CASE_JSON_BYTES / 1024} KiB.`);
753
- const input = {
754
- name,
755
- description: draft.description.trim() || null,
756
- triggerNodeId: draft.triggerNodeId || null,
757
- triggerData: triggerData.value,
758
- pinnedOutputs: pinnedOutputs.value,
759
- connectionMocks: connectionMocks.value,
760
- expectedStatus: draft.expectedStatus,
761
- expectations: expectations.value,
762
- enabled: draft.enabled
763
- };
764
- if (sortOrder !== void 0) input.sortOrder = sortOrder;
765
- return { input };
766
- }
767
- /** Whether a suite (or a case in it) is still running. */
768
- function isTestRunOpen(status) {
769
- return status === "PENDING" || status === "RUNNING";
770
- }
771
- /** How a suite or case status is shown. */
772
- function testStatusTone(status) {
773
- switch (status) {
774
- case "PASSED": return "success";
775
- case "FAILED": return "error";
776
- case "ERROR": return "warning";
777
- default: return "pending";
778
- }
779
- }
780
- /** Semantic-token classes for a status tone. */
781
- const TEST_STATUS_TONE_CLASS = {
782
- success: "border-[var(--color-status-success-border)] bg-[var(--color-status-success-bg-subtle)] text-[var(--color-status-success-text)]",
783
- error: "border-[var(--color-status-error-border)] bg-[var(--color-status-error-bg-subtle)] text-[var(--color-status-error-text)]",
784
- warning: "border-[var(--color-status-warning-border)] bg-[var(--color-status-warning-bg-subtle)] text-[var(--color-status-warning-text)]",
785
- pending: "border-border bg-muted text-muted-foreground"
786
- };
787
- /** The i18n key and English label of a suite or case status. */
788
- function testStatusLabel(status) {
789
- const fallback = {
790
- ["PENDING"]: "Pending",
791
- ["RUNNING"]: "Running",
792
- ["PASSED"]: "Passed",
793
- ["FAILED"]: "Failed",
794
- ["ERROR"]: "Error"
795
- };
796
- return {
797
- key: `fluidgrids.builder.workflowTests.status.${status.toLowerCase()}`,
798
- fallback: fallback[status]
799
- };
800
- }
801
- /** "3 passed · 1 failed · 1 running" for a suite run. */
802
- function testRunSummaryText(summary) {
803
- const parts = [`${summary.passed} passed`];
804
- if (summary.failed) parts.push(`${summary.failed} failed`);
805
- if (summary.errored) parts.push(`${summary.errored} errored`);
806
- if (summary.pending) parts.push(`${summary.pending} running`);
807
- return `${parts.join(" · ")} of ${summary.total}`;
808
- }
809
- /** The ids of the enabled cases, in their order. */
810
- function enabledCaseIds(cases) {
811
- return cases.filter((testCase) => testCase.enabled).map((c) => c.id);
812
- }
813
- /** The sort order a new case takes: after every existing case. */
814
- function nextSortOrder(cases) {
815
- return cases.reduce((max, c) => Math.max(max, c.sortOrder + 1), 0);
816
- }
817
- /** Run statuses after which a run can be saved as a test case. */
818
- const SAVEABLE_RUN_STATUSES = /* @__PURE__ */ new Set(["COMPLETED", "FAILED"]);
819
- /** Whether a finished run of a canvas workflow can become a test case. */
820
- function canSaveRunAsTestCase(run) {
821
- return SAVEABLE_RUN_STATUSES.has(run.status) && !!run.workflowKey && !run.isDurable;
822
- }
823
-
824
- // src/builder/hooks/use-workflow-tests.ts
825
- /**
826
- * useWorkflowTests — the saved test cases of one workflow, its recent suite
827
- * runs and the actions on them (BOFF-9960).
828
- *
829
- * Every write refetches what it changed, so the panel shows what the server
830
- * holds. While a suite run is open (PENDING or RUNNING) it is polled with
831
- * `workflowTestRun` until it settles; the cases are then read again for their
832
- * last result. Nothing is sent while `enabled` is false (the
833
- * `fluidgrids.workflow-tests` flag off), so a workspace without the feature
834
- * never meets its FEATURE_DISABLED refusals.
835
- */
836
- /** How often an open suite run is read again. */
837
- const TEST_RUN_POLL_INTERVAL_MS = 2e3;
838
- /** Suite runs listed in the panel. */
839
- const TEST_RUN_HISTORY_LIMIT = 10;
840
- function messageOf$1(error) {
841
- return error instanceof Error ? error.message : String(error);
842
- }
843
- /** Puts a run first in the list, replacing an older copy of it. */
844
- function upsertRun(runs, run) {
845
- return [run, ...runs.filter((item) => item.id !== run.id)].slice(0, 10);
846
- }
847
- function useWorkflowTests(workflowKey, enabled) {
848
- const [cases, setCases] = useState([]);
849
- const [runs, setRuns] = useState([]);
850
- const [activeRunId, setActiveRunId] = useState(null);
851
- const [loading, setLoading] = useState(false);
852
- const [error, setError] = useState(null);
853
- const [forbidden, setForbidden] = useState(false);
854
- const [pollTick, setPollTick] = useState(0);
855
- const keyRef = useRef(workflowKey);
856
- const refetch = useCallback(async () => {
857
- if (!enabled || !workflowKey) return;
858
- setLoading(true);
859
- setError(null);
860
- try {
861
- const client = getFluidGridsClient();
862
- const [caseResult, runResult] = await Promise.all([client.query({
863
- query: WorkflowTestCasesDocument,
864
- variables: { workflowKey },
865
- fetchPolicy: "network-only"
866
- }), client.query({
867
- query: WorkflowTestRunsDocument,
868
- variables: {
869
- workflowKey,
870
- limit: 10
871
- },
872
- fetchPolicy: "network-only"
873
- })]);
874
- throwIfResultError(caseResult);
875
- throwIfResultError(runResult);
876
- if (keyRef.current !== workflowKey) return;
877
- setCases(caseResult.data?.workflowTestCases ?? []);
878
- setRuns(runResult.data?.workflowTestRuns ?? []);
879
- setForbidden(false);
880
- } catch (err) {
881
- if (keyRef.current !== workflowKey) return;
882
- if (isForbiddenError(err)) setForbidden(true);
883
- else setError(messageOf$1(err));
884
- } finally {
885
- if (keyRef.current === workflowKey) setLoading(false);
886
- }
887
- }, [enabled, workflowKey]);
888
- useEffect(() => {
889
- keyRef.current = workflowKey;
890
- setCases([]);
891
- setRuns([]);
892
- setActiveRunId(null);
893
- setForbidden(false);
894
- setError(null);
895
- }, [workflowKey]);
896
- const activeRun = (activeRunId ? runs.find((run) => run.id === activeRunId) : runs[0]) ?? null;
897
- const followedId = activeRun && isTestRunOpen(activeRun.status) ? activeRun.id : null;
898
- useEffect(() => {
899
- if (!enabled || !followedId) return;
900
- let cancelled = false;
901
- const key = workflowKey;
902
- const timer = setTimeout(() => {
903
- (async () => {
904
- try {
905
- const result = await getFluidGridsClient().query({
906
- query: WorkflowTestRunDocument,
907
- variables: { id: followedId },
908
- fetchPolicy: "network-only"
909
- });
910
- throwIfResultError(result);
911
- const run = result.data?.workflowTestRun;
912
- if (cancelled || keyRef.current !== key || !run) return;
913
- setRuns((current) => upsertRun(current, run));
914
- if (!isTestRunOpen(run.status)) await refetchCases(key);
915
- } catch (err) {
916
- if (!cancelled && keyRef.current === key) setError(messageOf$1(err));
917
- } finally {
918
- if (!cancelled) setPollTick((tick) => tick + 1);
919
- }
920
- })();
921
- }, TEST_RUN_POLL_INTERVAL_MS);
922
- async function refetchCases(forKey) {
923
- const result = await getFluidGridsClient().query({
924
- query: WorkflowTestCasesDocument,
925
- variables: { workflowKey: forKey },
926
- fetchPolicy: "network-only"
927
- });
928
- throwIfResultError(result);
929
- if (!cancelled && keyRef.current === forKey) setCases(result.data?.workflowTestCases ?? []);
930
- }
931
- return () => {
932
- cancelled = true;
933
- clearTimeout(timer);
934
- };
935
- }, [
936
- enabled,
937
- followedId,
938
- workflowKey,
939
- pollTick
940
- ]);
941
- return {
942
- cases,
943
- runs,
944
- activeRun,
945
- loading,
946
- error,
947
- forbidden,
948
- refetch,
949
- createCase: useCallback(async (input) => {
950
- const result = await getFluidGridsClient().mutate({
951
- mutation: CreateWorkflowTestCaseDocument,
952
- variables: {
953
- workflowKey,
954
- input
955
- }
956
- });
957
- throwIfResultError(result);
958
- const created = result.data?.createWorkflowTestCase;
959
- if (!created) throw new Error("The test case was not saved.");
960
- await refetch();
961
- return created;
962
- }, [workflowKey, refetch]),
963
- updateCase: useCallback(async (id, input) => {
964
- const result = await getFluidGridsClient().mutate({
965
- mutation: UpdateWorkflowTestCaseDocument,
966
- variables: {
967
- id,
968
- input
969
- }
970
- });
971
- throwIfResultError(result);
972
- const updated = result.data?.updateWorkflowTestCase;
973
- if (!updated) throw new Error("The test case was not saved.");
974
- await refetch();
975
- return updated;
976
- }, [refetch]),
977
- deleteCase: useCallback(async (id) => {
978
- const result = await getFluidGridsClient().mutate({
979
- mutation: DeleteWorkflowTestCaseDocument,
980
- variables: { id }
981
- });
982
- throwIfResultError(result);
983
- await refetch();
984
- }, [refetch]),
985
- runTests: useCallback(async (options = {}) => {
986
- const result = await getFluidGridsClient().mutate({
987
- mutation: RunWorkflowTestsDocument,
988
- variables: { input: {
989
- workflowKey,
990
- source: "UI",
991
- ...options.version ? { version: options.version } : {},
992
- ...options.caseIds ? { caseIds: options.caseIds } : {}
993
- } }
994
- });
995
- throwIfResultError(result);
996
- const run = result.data?.runWorkflowTests;
997
- if (!run) throw new Error("The test run did not start.");
998
- setRuns((current) => upsertRun(current, run));
999
- setActiveRunId(run.id);
1000
- return run;
1001
- }, [workflowKey])
1002
- };
1003
- }
1004
- /**
1005
- * Saves a finished run as a test case of its workflow (used by the run detail
1006
- * page, outside the builder).
1007
- */
1008
- async function saveRunAsTestCase(runId, name, description) {
1009
- const result = await getFluidGridsClient().mutate({
1010
- mutation: CreateWorkflowTestCaseFromRunDocument,
1011
- variables: {
1012
- runId,
1013
- name,
1014
- ...description ? { description } : {}
1015
- }
1016
- });
1017
- throwIfResultError(result);
1018
- const created = result.data?.createWorkflowTestCaseFromRun;
1019
- if (!created) throw new Error("The test case was not saved.");
1020
- return created;
1021
- }
1022
-
1023
- // src/hooks/useWorkflowProposal.ts
1024
- /**
1025
- * useWorkflowProposal — drives one AI workflow proposal (BOFF-3662): start it
1026
- * (author, run fix or next node), poll `workflowProposal(id)` while it is
1027
- * PROCESSING, then apply, reject or cancel it.
1028
- *
1029
- * A proposal never changes a workflow by itself. Applying it creates a new
1030
- * DRAFT version (or a new DRAFT workflow); the server marks it STALE when the
1031
- * workflow changed after the proposal was made.
1032
- */
1033
- /** How often a PROCESSING proposal is polled. */
1034
- const PROPOSAL_POLL_INTERVAL_MS = 3e3;
1035
- /** Stop polling after this long; the server closes a stuck proposal after 20 minutes. */
1036
- const PROPOSAL_MAX_WAIT_MS = 36e4;
1037
- function messageOf(error) {
1038
- return error instanceof Error ? error.message : String(error);
1039
- }
1040
- /** Whether a proposal status means the model is still working. */
1041
- function isProposalRunning(status) {
1042
- return status === "PROCESSING";
1043
- }
1044
- function useWorkflowProposal() {
1045
- const [proposal, setProposal] = useState(null);
1046
- const [isWorking, setIsWorking] = useState(false);
1047
- const [isDeciding, setIsDeciding] = useState(false);
1048
- const [error, setError] = useState(null);
1049
- const [timedOut, setTimedOut] = useState(false);
1050
- const runRef = useRef(0);
1051
- const timerRef = useRef(null);
1052
- const clearTimer = useCallback(() => {
1053
- if (timerRef.current) clearTimeout(timerRef.current);
1054
- timerRef.current = null;
1055
- }, []);
1056
- useEffect(() => () => {
1057
- runRef.current++;
1058
- clearTimer();
1059
- }, [clearTimer]);
1060
- const reset = useCallback(() => {
1061
- runRef.current++;
1062
- clearTimer();
1063
- setProposal(null);
1064
- setIsWorking(false);
1065
- setIsDeciding(false);
1066
- setError(null);
1067
- setTimedOut(false);
1068
- }, [clearTimer]);
1069
- const poll = useCallback(async (run, id, startedAt) => {
1070
- if (run !== runRef.current) return;
1071
- try {
1072
- const result = throwIfResultError(await getFluidGridsClient().query({
1073
- query: WorkflowProposalDocument,
1074
- variables: { id },
1075
- fetchPolicy: "network-only"
1076
- }));
1077
- if (run !== runRef.current) return;
1078
- const next = result.data?.workflowProposal ?? null;
1079
- if (next) setProposal(next);
1080
- if (isProposalRunning(next ? String(next.status) : void 0)) {
1081
- if (Date.now() - startedAt < 36e4) {
1082
- timerRef.current = setTimeout(() => void poll(run, id, startedAt), PROPOSAL_POLL_INTERVAL_MS);
1083
- return;
1084
- }
1085
- setTimedOut(true);
1086
- } else notifyFluidGridsAiCreditsUpdated();
1087
- setIsWorking(false);
1088
- } catch (pollError) {
1089
- if (run !== runRef.current) return;
1090
- setError(messageOf(pollError));
1091
- setIsWorking(false);
1092
- }
1093
- }, []);
1094
- const start = useCallback(async (input) => {
1095
- const run = ++runRef.current;
1096
- clearTimer();
1097
- setProposal(null);
1098
- setError(null);
1099
- setTimedOut(false);
1100
- setIsWorking(true);
1101
- try {
1102
- const client = getFluidGridsClient();
1103
- let started;
1104
- if (input.kind === "runFix") started = throwIfResultError(await client.mutate({
1105
- mutation: ProposeRunFixDocument,
1106
- variables: { input: {
1107
- runId: input.runId,
1108
- ...input.prompt ? { prompt: input.prompt } : {}
1109
- } }
1110
- })).data?.proposeRunFix;
1111
- else started = throwIfResultError(await client.mutate({
1112
- mutation: ProposeWorkflowChangeDocument,
1113
- variables: { input: {
1114
- prompt: input.prompt,
1115
- ...input.workflowKey ? { workflowKey: input.workflowKey } : {},
1116
- ...input.baseVersion ? { baseVersion: input.baseVersion } : {},
1117
- ...input.template ? { template: input.template } : {}
1118
- } }
1119
- })).data?.proposeWorkflowChange;
1120
- if (!started?.id) throw new Error("The proposal did not start");
1121
- if (run !== runRef.current) return;
1122
- setProposal(started);
1123
- const id = started.id;
1124
- timerRef.current = setTimeout(() => void poll(run, id, Date.now()), PROPOSAL_POLL_INTERVAL_MS);
1125
- } catch (startError) {
1126
- if (run !== runRef.current) return;
1127
- setError(messageOf(startError));
1128
- setIsWorking(false);
1129
- }
1130
- }, [clearTimer, poll]);
1131
- const open = useCallback(async (id) => {
1132
- const run = ++runRef.current;
1133
- clearTimer();
1134
- setError(null);
1135
- setTimedOut(false);
1136
- setIsWorking(true);
1137
- await poll(run, id, Date.now());
1138
- }, [clearTimer, poll]);
1139
- const decide = useCallback(async (action) => {
1140
- const id = proposal?.id;
1141
- if (!id) return null;
1142
- const run = runRef.current;
1143
- setIsDeciding(true);
1144
- setError(null);
1145
- try {
1146
- const client = getFluidGridsClient();
1147
- let next;
1148
- if (action === "apply") next = throwIfResultError(await client.mutate({
1149
- mutation: ApplyWorkflowProposalDocument,
1150
- variables: { id }
1151
- })).data?.applyWorkflowProposal;
1152
- else if (action === "reject") next = throwIfResultError(await client.mutate({
1153
- mutation: RejectWorkflowProposalDocument,
1154
- variables: { id }
1155
- })).data?.rejectWorkflowProposal;
1156
- else {
1157
- clearTimer();
1158
- next = throwIfResultError(await client.mutate({
1159
- mutation: CancelWorkflowProposalDocument,
1160
- variables: { id }
1161
- })).data?.cancelWorkflowProposal;
1162
- setIsWorking(false);
1163
- }
1164
- if (run === runRef.current && next) setProposal(next);
1165
- return next ?? null;
1166
- } catch (decideError) {
1167
- if (run === runRef.current) setError(messageOf(decideError));
1168
- return null;
1169
- } finally {
1170
- if (run === runRef.current) setIsDeciding(false);
1171
- }
1172
- }, [clearTimer, proposal?.id]);
1173
- return {
1174
- proposal,
1175
- isWorking,
1176
- isDeciding,
1177
- error,
1178
- timedOut,
1179
- start,
1180
- open,
1181
- apply: useCallback(() => decide("apply"), [decide]),
1182
- reject: useCallback(async () => {
1183
- await decide("reject");
1184
- }, [decide]),
1185
- cancel: useCallback(async () => {
1186
- await decide("cancel");
1187
- }, [decide]),
1188
- reset
1189
- };
1190
- }
1191
-
1192
- // src/components/shared/WorkflowProposalCard.tsx
1193
- /**
1194
- * WorkflowProposalCard — review an AI workflow proposal before it changes
1195
- * anything (BOFF-3662).
1196
- *
1197
- * Shows what the model proposes (explanation, a node and connection diff
1198
- * against the version it was made from, validation errors and the settings the
1199
- * user still has to fill in) with Apply / Reject while it is PROPOSED, a
1200
- * Cancel while it is still running, and once applied a link to the new draft
1201
- * plus a one-click revert to the version it was made from (a restore, which
1202
- * creates another version and never deletes one).
1203
- */
1204
- const STATUS_TONE = {
1205
- PROCESSING: "secondary",
1206
- PROPOSED: "default",
1207
- APPLIED: "default",
1208
- REJECTED: "outline",
1209
- STALE: "outline",
1210
- CANCELLED: "outline",
1211
- FAILED: "destructive"
1212
- };
1213
- function edgeLabel(edge) {
1214
- return `${edge.sourceHandle ? `${edge.source} (${edge.sourceHandle})` : edge.source} → ${edge.target}`;
1215
- }
1216
- function ChangeList({ title, icon, items, testId }) {
1217
- if (items.length === 0) return null;
1218
- return /* @__PURE__ */ jsxs("div", {
1219
- "data-testid": testId,
1220
- children: [/* @__PURE__ */ jsxs("div", {
1221
- className: "flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary",
1222
- children: [
1223
- /* @__PURE__ */ jsx(Icon, {
1224
- icon,
1225
- width: 14
1226
- }),
1227
- title,
1228
- " (",
1229
- items.length,
1230
- ")"
1231
- ]
1232
- }), /* @__PURE__ */ jsx("ul", {
1233
- className: "mt-1 space-y-0.5 text-sm text-text-primary",
1234
- children: items.map((item) => /* @__PURE__ */ jsx("li", {
1235
- className: "truncate",
1236
- children: item
1237
- }, item))
1238
- })]
1239
- });
1240
- }
1241
- function nodeLine(node, withFields) {
1242
- const base = `${node.label} · ${node.type}`;
1243
- return withFields && node.fields.length > 0 ? `${base} — ${node.fields.join(", ")}` : base;
1244
- }
1245
- /**
1246
- * Validation errors that are not already part of the message shown above
1247
- * them. Comparison ignores case and surrounding whitespace and punctuation.
1248
- */
1249
- function visibleErrors(errors, shownMessage) {
1250
- const shown = normalise(shownMessage ?? "");
1251
- const seen = /* @__PURE__ */ new Set();
1252
- const result = [];
1253
- for (const error of errors) {
1254
- const key = normalise(error);
1255
- if (!key || seen.has(key)) continue;
1256
- seen.add(key);
1257
- if (shown && shown.includes(key)) continue;
1258
- result.push(error);
1259
- }
1260
- return result;
1261
- }
1262
- function normalise(text) {
1263
- return text.trim().toLowerCase().replace(/^[\s.:;,]+|[\s.:;,]+$/g, "");
1264
- }
1265
- function WorkflowProposalCard({ proposal, isDeciding = false, onOpenWorkflow, onApply, onReject, onCancel, onRevert }) {
1266
- const tr = useTr();
1267
- const status = String(proposal.status);
1268
- const diff = proposal.diff;
1269
- const warnings = proposal.validation?.warnings ?? [];
1270
- const showMessage = Boolean(proposal.message && status !== "PROPOSED" && status !== "PROCESSING" && status !== "APPLIED");
1271
- const errors = visibleErrors(proposal.validation?.valid === false ? proposal.validation.errors : [], showMessage ? proposal.message : null);
1272
- const revertTarget = proposal.baseVersion && proposal.workflowKey && proposal.appliedWorkflowKey === proposal.workflowKey ? {
1273
- workflowKey: proposal.workflowKey,
1274
- version: proposal.baseVersion
1275
- } : null;
1276
- return /* @__PURE__ */ jsxs("section", {
1277
- className: "rounded-lg border border-border-secondary bg-background-secondary p-4",
1278
- "data-testid": "workflow-proposal-card",
1279
- "data-status": status,
1280
- children: [
1281
- /* @__PURE__ */ jsxs("header", {
1282
- className: "flex flex-wrap items-center gap-2",
1283
- children: [
1284
- /* @__PURE__ */ jsx(Icon, {
1285
- icon: "mdi:creation-outline",
1286
- width: 18,
1287
- className: "text-text-secondary"
1288
- }),
1289
- /* @__PURE__ */ jsx("span", {
1290
- className: "text-sm font-medium text-text-primary",
1291
- children: proposal.name ?? tr("fluidgrids.ai.proposal.title", "AI proposal")
1292
- }),
1293
- /* @__PURE__ */ jsx(Badge, {
1294
- variant: STATUS_TONE[status] ?? "outline",
1295
- "data-testid": "workflow-proposal-status",
1296
- children: status
1297
- }),
1298
- proposal.baseVersion && /* @__PURE__ */ jsx("span", {
1299
- className: "text-xs text-text-secondary",
1300
- children: tr("fluidgrids.ai.proposal.against", "against version {{version}}", { version: proposal.baseVersion })
1301
- })
1302
- ]
1303
- }),
1304
- status === "PROCESSING" && /* @__PURE__ */ jsxs("div", {
1305
- className: "mt-3 flex items-center gap-2 text-sm text-text-secondary",
1306
- children: [
1307
- /* @__PURE__ */ jsx(Icon, {
1308
- icon: "mdi:loading",
1309
- width: 16,
1310
- className: "animate-spin"
1311
- }),
1312
- tr("fluidgrids.ai.proposal.working", "Working ({{stage}})…", { stage: proposal.progress?.stage ?? "prompting" }),
1313
- onCancel && /* @__PURE__ */ jsx(Button, {
1314
- size: "sm",
1315
- variant: "ghost",
1316
- onClick: onCancel,
1317
- disabled: isDeciding,
1318
- children: tr("fluidgrids.ai.proposal.cancel", "Cancel")
1319
- })
1320
- ]
1321
- }),
1322
- proposal.explanation && /* @__PURE__ */ jsx("p", {
1323
- className: "mt-3 text-sm text-text-primary",
1324
- "data-testid": "workflow-proposal-explanation",
1325
- children: proposal.explanation
1326
- }),
1327
- showMessage && /* @__PURE__ */ jsx("p", {
1328
- className: "mt-2 text-sm text-text-secondary",
1329
- "data-testid": "workflow-proposal-message",
1330
- children: proposal.message
1331
- }),
1332
- diff && /* @__PURE__ */ jsxs("div", {
1333
- className: "mt-3 space-y-2",
1334
- "data-testid": "workflow-proposal-diff",
1335
- children: [
1336
- /* @__PURE__ */ jsx("p", {
1337
- className: "text-sm font-medium text-text-primary",
1338
- children: diff.summary
1339
- }),
1340
- /* @__PURE__ */ jsx(ChangeList, {
1341
- testId: "workflow-proposal-added",
1342
- icon: "mdi:plus-circle-outline",
1343
- title: tr("fluidgrids.ai.proposal.added", "Added nodes"),
1344
- items: diff.addedNodes.map((node) => nodeLine(node, false))
1345
- }),
1346
- /* @__PURE__ */ jsx(ChangeList, {
1347
- testId: "workflow-proposal-changed",
1348
- icon: "mdi:pencil-circle-outline",
1349
- title: tr("fluidgrids.ai.proposal.changed", "Changed nodes"),
1350
- items: diff.changedNodes.map((node) => nodeLine(node, true))
1351
- }),
1352
- /* @__PURE__ */ jsx(ChangeList, {
1353
- testId: "workflow-proposal-removed",
1354
- icon: "mdi:minus-circle-outline",
1355
- title: tr("fluidgrids.ai.proposal.removed", "Removed nodes"),
1356
- items: diff.removedNodes.map((node) => nodeLine(node, false))
1357
- }),
1358
- /* @__PURE__ */ jsx(ChangeList, {
1359
- testId: "workflow-proposal-edges",
1360
- icon: "mdi:vector-line",
1361
- title: tr("fluidgrids.ai.proposal.connections", "Connection changes"),
1362
- items: [...diff.addedEdges.map((edge) => `+ ${edgeLabel(edge)}`), ...diff.removedEdges.map((edge) => `− ${edgeLabel(edge)}`)]
1363
- })
1364
- ]
1365
- }),
1366
- errors.length > 0 && /* @__PURE__ */ jsx("ul", {
1367
- className: "mt-3 space-y-1 text-sm text-[var(--color-status-error-text)]",
1368
- "data-testid": "workflow-proposal-errors",
1369
- children: errors.map((message) => /* @__PURE__ */ jsx("li", { children: message }, message))
1370
- }),
1371
- warnings.length > 0 && /* @__PURE__ */ jsxs("div", {
1372
- className: "mt-3",
1373
- "data-testid": "workflow-proposal-warnings",
1374
- children: [/* @__PURE__ */ jsx("div", {
1375
- className: "text-xs font-medium uppercase tracking-wide text-text-secondary",
1376
- children: tr("fluidgrids.ai.proposal.todo", "Still to set up")
1377
- }), /* @__PURE__ */ jsx("ul", {
1378
- className: "mt-1 space-y-0.5 text-sm text-text-secondary",
1379
- children: warnings.map((warning) => /* @__PURE__ */ jsx("li", { children: warning }, warning))
1380
- })]
1381
- }),
1382
- status === "PROPOSED" && (onApply || onReject) && /* @__PURE__ */ jsxs("div", {
1383
- className: "mt-4 flex flex-wrap items-center gap-2",
1384
- children: [
1385
- onApply && /* @__PURE__ */ jsx(Button, {
1386
- size: "sm",
1387
- onClick: onApply,
1388
- disabled: isDeciding,
1389
- "data-testid": "workflow-proposal-apply",
1390
- children: proposal.baseVersion ? tr("fluidgrids.ai.proposal.applyVersion", "Apply as a new draft version") : tr("fluidgrids.ai.proposal.applyNew", "Create as a draft workflow")
1391
- }),
1392
- onReject && /* @__PURE__ */ jsx(Button, {
1393
- size: "sm",
1394
- variant: "outline",
1395
- onClick: onReject,
1396
- disabled: isDeciding,
1397
- children: tr("fluidgrids.ai.proposal.reject", "Reject")
1398
- }),
1399
- /* @__PURE__ */ jsx("span", {
1400
- className: "text-xs text-text-secondary",
1401
- children: tr("fluidgrids.ai.proposal.neverActivates", "Nothing goes live: the result is a draft you can review, run as a test and activate yourself.")
1402
- })
1403
- ]
1404
- }),
1405
- status === "APPLIED" && proposal.appliedWorkflowKey && proposal.appliedVersion && /* @__PURE__ */ jsxs("div", {
1406
- className: "mt-4 space-y-2",
1407
- children: [/* @__PURE__ */ jsxs("div", {
1408
- className: "flex flex-wrap items-center gap-2",
1409
- children: [onOpenWorkflow && /* @__PURE__ */ jsx(Button, {
1410
- size: "sm",
1411
- variant: "outline",
1412
- "data-testid": "workflow-proposal-open",
1413
- onClick: () => onOpenWorkflow(proposal.appliedWorkflowKey ?? "", proposal.appliedVersion ?? ""),
1414
- children: tr("fluidgrids.ai.proposal.open", "Open draft {{version}}", { version: proposal.appliedVersion })
1415
- }), onRevert && revertTarget && /* @__PURE__ */ jsx(Button, {
1416
- size: "sm",
1417
- variant: "ghost",
1418
- "data-testid": "workflow-proposal-revert",
1419
- disabled: isDeciding,
1420
- onClick: () => onRevert(revertTarget.workflowKey, revertTarget.version),
1421
- children: tr("fluidgrids.ai.proposal.revert", "Revert to {{version}}", { version: revertTarget.version })
1422
- })]
1423
- }), !onRevert && proposal.restoreHint && /* @__PURE__ */ jsx("p", {
1424
- className: "text-xs text-text-secondary",
1425
- children: proposal.restoreHint
1426
- })]
1427
- })
1428
- ]
1429
- });
1430
- }
1431
-
1432
- export { rowsFromRules as C, agentTokenStreamsFor as D, agentLiveReplyState as E, latestAgentTokenStream as O, readAssertRulesSetting as S, useAgentTokenStream as T, ASSERT_OPERATORS as _, TEST_STATUS_TONE_CLASS as a, normaliseAssertRules as b, draftToInput as c, isTestRunOpen as d, nextSortOrder as f, testStatusTone as g, testStatusLabel as h, useWorkflowTests as i, emptyTestCaseDraft as l, testRunSummaryText as m, useWorkflowProposal as n, canSaveRunAsTestCase as o, pinnedOutputsFromCanvas as p, saveRunAsTestCase as r, draftFromTestCase as s, WorkflowProposalCard as t, enabledCaseIds as u, describeAssertRuleProblem as v, ruleFromRow as w, operatorSpec as x, emptyAssertRuleRow as y };