@burdenoff/fluidgrids-fe-sdk 2026.1112.8 → 2026.1112.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AiStudioPage-CtgPSPyc.js → AiStudioPage-DfoV1Olc.js} +1 -1
- package/dist/ApprovalInboxPage-Bb8H9ruw.js +1317 -0
- package/dist/{DataTablesPage-2xTLe_jk.js → DataTablesPage-Zz23QZQp.js} +3 -1
- package/dist/Disclosure-DQiUKrqd.js +41 -0
- package/dist/{ExecutionDetailPage-CL-w9c3L.js → ExecutionDetailPage-CcUxFexZ.js} +3 -2
- package/dist/{ExecutionsListPage-Bf6Cvsnz.js → ExecutionsListPage-JpByw6nA.js} +3 -1
- package/dist/{MissingNodesInstallModal-BCbhddk4.js → MissingNodesInstallModal-olEc52p2.js} +1 -1
- package/dist/{OpenTemplateAsCodeButton-BgvgDaVe.js → OpenTemplateAsCodeButton-Dh3ee2OR.js} +1 -1
- package/dist/PaidTemplateConfirm-DjmVi_m-.js +28 -0
- package/dist/{RunActionsMenu-DmO14hEE.js → RunActionsMenu-BE_1r3bF.js} +1 -50
- package/dist/{RunnersPage-CvAPHdfU.js → RunnersPage-CAt4YDhB.js} +2 -1
- package/dist/TechnicalDetails-D4xGy201.js +19 -0
- package/dist/{TemplateCard-XBc0qEA4.js → TemplateCard--WYJZlA_.js} +81 -25
- package/dist/{TemplateConfigurationModal-Dl_7RaCt.js → TemplateConfigurationModal-cd0-QVMZ.js} +2 -2
- package/dist/{TemplateDetailPanel-0Cc6jguU.js → TemplateDetailPanel-C-2JuXg6.js} +1 -1
- package/dist/TemplatePreview-BCnGjMuN.js +646 -0
- package/dist/{TemplatesPage-wClKZLO4.js → TemplatesPage-u7bXJYfv.js} +375 -136
- package/dist/{VariablesPage-BtoLjmUA.js → VariablesPage-CPL34WBc.js} +2 -358
- package/dist/{WorkflowBuilder-qRGFtXJG.js → WorkflowBuilder-BWxBmBoP.js} +2 -2
- package/dist/{WorkflowBuilder-CK6c0eRp.js → WorkflowBuilder-DxAAPHsn.js} +3 -3
- package/dist/{agentTools-BkQsycAp.js → agentTools-JHtYGgd7.js} +2 -2
- package/dist/{canvasNoteTools-U7j1z7GR.js → canvasNoteTools-B2ouyIbj.js} +1 -1
- package/dist/{HumanRequestCard-lbWDpWd7.js → channel-CZ70oI81.js} +5 -228
- package/dist/executions.js +2 -2
- package/dist/{flow-builder-9jDNwkID.js → flow-builder-DzKPrW2r.js} +33 -24
- package/dist/{index-rj-TznII.d.ts → index-CGCAcRKo.d.ts} +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +19 -19
- package/dist/{node-detail-view-C1yupyvL.js → node-detail-view-DNilUGcx.js} +2 -2
- package/dist/{node-editor-tab-body-BtxSQPtX.js → node-editor-tab-body-XBedxkpN.js} +1 -1
- package/dist/observability.d.ts +1 -1
- package/dist/{popover-BaYffpTw.js → popover-BC4r84K0.js} +1 -1
- package/dist/{quick-config-panel-BvlhLiQp.js → quick-config-panel-BPmrhIOb.js} +2 -2
- package/dist/{schedule-panel-D5VdzW2c.js → schedule-panel-DJN1TDQH.js} +2 -2
- package/dist/{sidebar-node-editor-BU4rJwvm.js → sidebar-node-editor-DKdfcd2r.js} +2 -2
- package/dist/templates.js +2 -2
- package/dist/useVariables-cI8uLwye.js +361 -0
- package/dist/workflow-builder.js +2 -2
- package/dist/workflow-pages.d.ts +1 -1
- package/dist/workflow-pages.js +10 -10
- package/package.json +1 -1
- package/dist/ApprovalInboxPage-QAJim81n.js +0 -559
- package/dist/TemplatePreview-CRLUX5yI.js +0 -240
|
@@ -0,0 +1,1317 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-hDbaFkMy.js";
|
|
3
|
+
import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
|
|
4
|
+
import { n as useNavigationV2 } from "./useNavigationV2-C2WeqN7d.js";
|
|
5
|
+
import { _ as throwIfResultError, h as isForbiddenError, m as describeOperationError, n as getFluidGridsClient } from "./client-C9XSzSXr.js";
|
|
6
|
+
import { a as useRecipientLabel, i as notAnswerableText, n as ApprovalApprovers, r as answeredToastText, t as channelLabel } from "./channel-CZ70oI81.js";
|
|
7
|
+
import { n as useRespondToHumanRequest, t as usePendingHumanRequests } from "./useHumanRequests-ZiR0sSzS.js";
|
|
8
|
+
import { t as TechnicalDetails } from "./TechnicalDetails-D4xGy201.js";
|
|
9
|
+
import { t as FluidGridsDocsLink } from "./FluidGridsDocsLink-JxkBpOeb.js";
|
|
10
|
+
import { r as nodeDisplayTitle, t as buildNodeDisplayMap } from "./run-display-BycKYSU0.js";
|
|
11
|
+
import { i as SdkPermissionButton, s as useFluidGridsPermission, t as FLUIDGRIDS_PERMISSIONS } from "./constants-dcxg6zG6.js";
|
|
12
|
+
import { useCallback, useEffect, useState } from "react";
|
|
13
|
+
import { Link, useNavigate } from "react-router";
|
|
14
|
+
import { Icon } from "@iconify/react";
|
|
15
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
16
|
+
import { toast } from "sonner";
|
|
17
|
+
import { gql } from "@apollo/client";
|
|
18
|
+
import { Badge, Card, CardContent, CardHeader, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Textarea } from "@burdenoff/fe-libs/ui";
|
|
19
|
+
import { Button as Button$1 } from "@burdenoff/fe-libs/button";
|
|
20
|
+
|
|
21
|
+
// src/modules/human-response/utils/approval-labels.ts
|
|
22
|
+
/**
|
|
23
|
+
* The raw ids every Approvals card keeps one click away (Navigation v2,
|
|
24
|
+
* BOFF-11848): the workflow key, the run id and the step (node) id.
|
|
25
|
+
*/
|
|
26
|
+
function approvalTechnicalRows(ids, tr) {
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
label: tr("fluidgrids.approvals.technicalWorkflowKey", "Workflow key"),
|
|
30
|
+
value: ids.workflowKey
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: tr("fluidgrids.approvals.technicalRunId", "Run ID"),
|
|
34
|
+
value: ids.runId
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: tr("fluidgrids.approvals.technicalStepId", "Step ID"),
|
|
38
|
+
value: ids.nodeId
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Where an Approvals card comes from, in words (Navigation v2): "Step: Sort
|
|
44
|
+
* the ticket in Support desk", or "In Support desk" when the step's name is
|
|
45
|
+
* not known.
|
|
46
|
+
*/
|
|
47
|
+
function approvalSourceText(workflow, step, tr) {
|
|
48
|
+
return step ? tr("fluidgrids.approvals.stepInWorkflow", "Step: {{step}} in {{workflow}}", {
|
|
49
|
+
step,
|
|
50
|
+
workflow
|
|
51
|
+
}) : tr("fluidgrids.approvals.inWorkflow", "In {{workflow}}", { workflow });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/modules/human-response/components/ApprovalSource.tsx
|
|
55
|
+
/** "Open run": the run this card belongs to, on the Runs page. */
|
|
56
|
+
function OpenRunLink({ runId }) {
|
|
57
|
+
const tr = useTr();
|
|
58
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
59
|
+
to: `../runs/${encodeURIComponent(runId)}`,
|
|
60
|
+
"data-testid": "approval-open-run",
|
|
61
|
+
className: "font-medium text-text-link underline-offset-2 hover:text-text-linkHover hover:underline",
|
|
62
|
+
children: tr("fluidgrids.approvals.openRun", "Open run")
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** "Step: {step} in {workflow} · Open run" under an ask's title. */
|
|
66
|
+
function ApprovalSource({ workflow, step, runId }) {
|
|
67
|
+
const tr = useTr();
|
|
68
|
+
return /* @__PURE__ */ jsxs("p", {
|
|
69
|
+
className: "flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs text-text-secondary",
|
|
70
|
+
"data-testid": "approval-source",
|
|
71
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
72
|
+
className: "min-w-0 break-words",
|
|
73
|
+
children: approvalSourceText(workflow, step, tr)
|
|
74
|
+
}), /* @__PURE__ */ jsx(OpenRunLink, { runId })]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** The raw ids of a card, one click away under "Show technical details". */
|
|
78
|
+
function ApprovalTechnicalDetails({ rows, children }) {
|
|
79
|
+
return /* @__PURE__ */ jsxs(TechnicalDetails, {
|
|
80
|
+
"data-testid": "approval-technical-details",
|
|
81
|
+
children: [/* @__PURE__ */ jsx("dl", {
|
|
82
|
+
className: "space-y-1 text-xs",
|
|
83
|
+
children: rows.map((row) => /* @__PURE__ */ jsxs("div", {
|
|
84
|
+
className: "flex flex-wrap gap-x-2",
|
|
85
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
86
|
+
className: "font-medium text-text-secondary",
|
|
87
|
+
children: row.label
|
|
88
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
89
|
+
className: "break-all font-mono text-text-primary",
|
|
90
|
+
children: row.value
|
|
91
|
+
})]
|
|
92
|
+
}, row.label))
|
|
93
|
+
}), children]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/modules/human-response/components/HumanRequestCard.tsx
|
|
98
|
+
/**
|
|
99
|
+
* One "Send and Wait for Response" ask in the in-app inbox (BOFF-7651): what was asked, of whom,
|
|
100
|
+
* through which channel and until when — and, when the caller may answer it in-app, the controls
|
|
101
|
+
* to do so (Approve / Decline, a text box, or the custom form). An approval with several approvers
|
|
102
|
+
* (BOFF-9956) also shows where its quorum stands and takes an optional comment with the answer.
|
|
103
|
+
*
|
|
104
|
+
* Navigation v2 (BOFF-11848, FluidGrids only): the card names its workflow (and step) with an
|
|
105
|
+
* "Open run" link instead of the raw workflow-key badge, which moves under "Show technical
|
|
106
|
+
* details" with the run and step ids; a required field says "(required)" instead of " *".
|
|
107
|
+
*/
|
|
108
|
+
const CHANNEL_ICON = {
|
|
109
|
+
EMAIL: "mdi:email-outline",
|
|
110
|
+
SLACK: "mdi:slack",
|
|
111
|
+
IN_APP: "mdi:bell-outline"
|
|
112
|
+
};
|
|
113
|
+
function inputType(field) {
|
|
114
|
+
const type = (field.type || "text").toLowerCase();
|
|
115
|
+
return [
|
|
116
|
+
"email",
|
|
117
|
+
"number",
|
|
118
|
+
"date",
|
|
119
|
+
"tel",
|
|
120
|
+
"url"
|
|
121
|
+
].includes(type) ? type : "text";
|
|
122
|
+
}
|
|
123
|
+
function FormControls({ requestId, fields, values, onChange }) {
|
|
124
|
+
const tr = useTr();
|
|
125
|
+
const v2 = useNavigationV2();
|
|
126
|
+
const marker = v2 ? ` ${tr("fluidgrids.humanResponse.requiredMarker", "(required)")}` : " *";
|
|
127
|
+
return /* @__PURE__ */ jsx("div", {
|
|
128
|
+
className: "space-y-3",
|
|
129
|
+
children: fields.map((field) => {
|
|
130
|
+
const id = `human-field-${requestId}-${field.name}`;
|
|
131
|
+
const required = field.required || void 0;
|
|
132
|
+
const type = (field.type || "text").toLowerCase();
|
|
133
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
134
|
+
className: "space-y-1.5",
|
|
135
|
+
children: [/* @__PURE__ */ jsxs(Label, {
|
|
136
|
+
htmlFor: id,
|
|
137
|
+
children: [field.label || field.name, field.required && /* @__PURE__ */ jsx("span", {
|
|
138
|
+
"aria-hidden": "true",
|
|
139
|
+
className: v2 ? "text-text-secondary" : void 0,
|
|
140
|
+
children: marker
|
|
141
|
+
})]
|
|
142
|
+
}), type === "textarea" ? /* @__PURE__ */ jsx(Textarea, {
|
|
143
|
+
id,
|
|
144
|
+
"aria-required": required,
|
|
145
|
+
value: values[field.name] ?? "",
|
|
146
|
+
placeholder: field.placeholder ?? void 0,
|
|
147
|
+
onChange: (event) => onChange(field.name, event.target.value)
|
|
148
|
+
}) : type === "dropdown" || type === "select" ? /* @__PURE__ */ jsxs("select", {
|
|
149
|
+
id,
|
|
150
|
+
"aria-required": required,
|
|
151
|
+
className: "w-full rounded-md border border-border-secondary bg-background-primary px-3 py-2 text-sm text-text-primary",
|
|
152
|
+
value: values[field.name] ?? "",
|
|
153
|
+
onChange: (event) => onChange(field.name, event.target.value),
|
|
154
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
155
|
+
value: "",
|
|
156
|
+
children: "—"
|
|
157
|
+
}), (field.options ?? []).map((option) => /* @__PURE__ */ jsx("option", {
|
|
158
|
+
value: option,
|
|
159
|
+
children: option
|
|
160
|
+
}, option))]
|
|
161
|
+
}) : /* @__PURE__ */ jsx(Input, {
|
|
162
|
+
id,
|
|
163
|
+
"aria-required": required,
|
|
164
|
+
type: inputType(field),
|
|
165
|
+
value: values[field.name] ?? "",
|
|
166
|
+
placeholder: field.placeholder ?? void 0,
|
|
167
|
+
onChange: (event) => onChange(field.name, event.target.value)
|
|
168
|
+
})]
|
|
169
|
+
}, field.name);
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Approve / Decline, with an optional comment that is recorded with the answer (BOFF-9956). */
|
|
174
|
+
function ApprovalControls({ request, loading, onAnswer }) {
|
|
175
|
+
const tr = useTr();
|
|
176
|
+
const [comment, setComment] = useState("");
|
|
177
|
+
const note = comment.trim() ? { comment: comment.trim() } : {};
|
|
178
|
+
const commentId = `human-comment-${request.id}`;
|
|
179
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
180
|
+
className: "space-y-2 pt-1",
|
|
181
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
182
|
+
className: "space-y-1.5",
|
|
183
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
184
|
+
htmlFor: commentId,
|
|
185
|
+
children: tr("fluidgrids.humanResponse.commentLabel", "Comment (optional)")
|
|
186
|
+
}), /* @__PURE__ */ jsx(Textarea, {
|
|
187
|
+
id: commentId,
|
|
188
|
+
value: comment,
|
|
189
|
+
maxLength: 2e3,
|
|
190
|
+
placeholder: tr("fluidgrids.humanResponse.commentPlaceholder", "Why you approve or decline. It is saved with your answer on the run."),
|
|
191
|
+
onChange: (event) => setComment(event.target.value),
|
|
192
|
+
"data-testid": "human-request-comment"
|
|
193
|
+
})]
|
|
194
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
195
|
+
className: "flex flex-wrap items-center gap-2",
|
|
196
|
+
children: [/* @__PURE__ */ jsxs(Button$1, {
|
|
197
|
+
"data-agent": "confirm",
|
|
198
|
+
size: "sm",
|
|
199
|
+
disabled: loading,
|
|
200
|
+
onClick: () => onAnswer({
|
|
201
|
+
action: "approve",
|
|
202
|
+
...note
|
|
203
|
+
}),
|
|
204
|
+
"data-testid": "human-request-approve",
|
|
205
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
206
|
+
icon: "mdi:check",
|
|
207
|
+
className: "mr-1.5",
|
|
208
|
+
width: 16
|
|
209
|
+
}), request.approveLabel]
|
|
210
|
+
}), request.approvalType === "double" && /* @__PURE__ */ jsxs(Button$1, {
|
|
211
|
+
"data-agent": "confirm",
|
|
212
|
+
size: "sm",
|
|
213
|
+
variant: "destructive",
|
|
214
|
+
disabled: loading,
|
|
215
|
+
onClick: () => onAnswer({
|
|
216
|
+
action: "decline",
|
|
217
|
+
...note
|
|
218
|
+
}),
|
|
219
|
+
"data-testid": "human-request-decline",
|
|
220
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
221
|
+
icon: "mdi:close",
|
|
222
|
+
className: "mr-1.5",
|
|
223
|
+
width: 16
|
|
224
|
+
}), request.declineLabel]
|
|
225
|
+
})]
|
|
226
|
+
})]
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/** How the caller answers in-app: Approve/Decline, a text box, or the custom form. */
|
|
230
|
+
function AnswerArea({ request, loading, onAnswer }) {
|
|
231
|
+
const tr = useTr();
|
|
232
|
+
const [text, setText] = useState("");
|
|
233
|
+
const [values, setValues] = useState({});
|
|
234
|
+
if (request.responseType === "APPROVAL") return /* @__PURE__ */ jsx(ApprovalControls, {
|
|
235
|
+
request,
|
|
236
|
+
loading,
|
|
237
|
+
onAnswer
|
|
238
|
+
});
|
|
239
|
+
const missingRequired = request.responseType === "CUSTOM_FORM" && request.fields.some((field) => field.required && !(values[field.name] ?? "").trim());
|
|
240
|
+
const emptyText = request.responseType === "FREE_TEXT" && !text.trim();
|
|
241
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
242
|
+
request.responseType === "FREE_TEXT" && /* @__PURE__ */ jsxs("div", {
|
|
243
|
+
className: "space-y-1.5",
|
|
244
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
245
|
+
htmlFor: `human-text-${request.id}`,
|
|
246
|
+
children: tr("fluidgrids.humanResponse.yourResponse", "Your response")
|
|
247
|
+
}), /* @__PURE__ */ jsx(Textarea, {
|
|
248
|
+
id: `human-text-${request.id}`,
|
|
249
|
+
value: text,
|
|
250
|
+
onChange: (event) => setText(event.target.value)
|
|
251
|
+
})]
|
|
252
|
+
}),
|
|
253
|
+
request.responseType === "CUSTOM_FORM" && /* @__PURE__ */ jsx(FormControls, {
|
|
254
|
+
requestId: request.id,
|
|
255
|
+
fields: request.fields,
|
|
256
|
+
values,
|
|
257
|
+
onChange: (name, value) => setValues((current) => ({
|
|
258
|
+
...current,
|
|
259
|
+
[name]: value
|
|
260
|
+
}))
|
|
261
|
+
}),
|
|
262
|
+
/* @__PURE__ */ jsx("div", {
|
|
263
|
+
className: "flex flex-wrap items-center gap-2 pt-1",
|
|
264
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
265
|
+
"data-agent": "confirm",
|
|
266
|
+
size: "sm",
|
|
267
|
+
disabled: loading || missingRequired || emptyText,
|
|
268
|
+
onClick: () => onAnswer(request.responseType === "FREE_TEXT" ? { text } : { values }),
|
|
269
|
+
"data-testid": "human-request-submit",
|
|
270
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
271
|
+
icon: "mdi:send",
|
|
272
|
+
className: "mr-1.5",
|
|
273
|
+
width: 16
|
|
274
|
+
}), request.submitLabel]
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
] });
|
|
278
|
+
}
|
|
279
|
+
function HumanRequestCard({ request, onAnswered, workflowName, stepName }) {
|
|
280
|
+
const tr = useTr();
|
|
281
|
+
const v2 = useNavigationV2();
|
|
282
|
+
const { respond, loading } = useRespondToHumanRequest();
|
|
283
|
+
const submit = async (answer) => {
|
|
284
|
+
try {
|
|
285
|
+
const decision = await respond(request.id, answer);
|
|
286
|
+
toast.success(answeredToastText(decision, tr));
|
|
287
|
+
onAnswered();
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const failed = tr("fluidgrids.humanResponse.failed", "Your response could not be recorded");
|
|
290
|
+
toast.error(failed, { description: describeOperationError(err, failed) });
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const recipient = useRecipientLabel(request, request.channel === "IN_APP" ? tr("fluidgrids.humanResponse.anyMember", "any workspace member") : "");
|
|
294
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
295
|
+
className: "border border-border-secondary bg-background-secondary",
|
|
296
|
+
"data-testid": "human-request-card",
|
|
297
|
+
"data-request-id": request.id,
|
|
298
|
+
children: [/* @__PURE__ */ jsxs(CardHeader, {
|
|
299
|
+
className: "pb-3",
|
|
300
|
+
children: [
|
|
301
|
+
/* @__PURE__ */ jsxs("div", {
|
|
302
|
+
className: "flex flex-wrap items-center justify-between gap-2",
|
|
303
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
304
|
+
className: "flex min-w-0 items-center gap-2",
|
|
305
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
306
|
+
icon: CHANNEL_ICON[request.channel] ?? "mdi:account-question",
|
|
307
|
+
width: 18,
|
|
308
|
+
className: "shrink-0 text-text-secondary"
|
|
309
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
310
|
+
className: "truncate text-sm font-semibold text-text-primary",
|
|
311
|
+
children: request.title
|
|
312
|
+
})]
|
|
313
|
+
}), !v2 && /* @__PURE__ */ jsx(Badge, {
|
|
314
|
+
variant: "outline",
|
|
315
|
+
className: "font-mono text-[10px]",
|
|
316
|
+
children: request.workflowKey
|
|
317
|
+
})]
|
|
318
|
+
}),
|
|
319
|
+
v2 && /* @__PURE__ */ jsx(ApprovalSource, {
|
|
320
|
+
workflow: workflowName ?? request.workflowKey,
|
|
321
|
+
step: stepName,
|
|
322
|
+
runId: request.runId
|
|
323
|
+
}),
|
|
324
|
+
/* @__PURE__ */ jsxs("p", {
|
|
325
|
+
className: "text-xs text-text-secondary",
|
|
326
|
+
children: [tr("fluidgrids.humanResponse.askedVia", "Asked {{recipient}} via {{channel}}", {
|
|
327
|
+
recipient,
|
|
328
|
+
channel: channelLabel(request.channel, tr)
|
|
329
|
+
}), request.expiresAt && ` · ${tr("fluidgrids.humanResponse.until", "until {{time}}", { time: new Date(request.expiresAt).toLocaleString() })}`]
|
|
330
|
+
})
|
|
331
|
+
]
|
|
332
|
+
}), /* @__PURE__ */ jsxs(CardContent, {
|
|
333
|
+
className: "space-y-3",
|
|
334
|
+
children: [
|
|
335
|
+
request.message && /* @__PURE__ */ jsx("p", {
|
|
336
|
+
className: "whitespace-pre-wrap break-words text-sm text-text-primary",
|
|
337
|
+
children: request.message
|
|
338
|
+
}),
|
|
339
|
+
/* @__PURE__ */ jsx(ApprovalApprovers, { request }),
|
|
340
|
+
!request.canRespond && /* @__PURE__ */ jsx("p", {
|
|
341
|
+
className: "text-xs text-text-secondary",
|
|
342
|
+
children: notAnswerableText(request, channelLabel(request.channel, tr), tr)
|
|
343
|
+
}),
|
|
344
|
+
request.canRespond && /* @__PURE__ */ jsx(AnswerArea, {
|
|
345
|
+
request,
|
|
346
|
+
loading,
|
|
347
|
+
onAnswer: submit
|
|
348
|
+
}),
|
|
349
|
+
v2 && /* @__PURE__ */ jsx(ApprovalTechnicalDetails, { rows: approvalTechnicalRows(request, tr) })
|
|
350
|
+
]
|
|
351
|
+
})]
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/modules/tool-approval/hooks/useToolApprovals.ts
|
|
356
|
+
/**
|
|
357
|
+
* Custom hooks for the AI Agent tool-approval inbox (BOFF-7521).
|
|
358
|
+
* Queries/mutates wspace-workflow-svc via the federated GraphQL gateway —
|
|
359
|
+
* mirrors the variables module's hook pattern
|
|
360
|
+
* (`modules/variables/hooks/useVariables.ts`).
|
|
361
|
+
*/
|
|
362
|
+
const TOOL_APPROVAL_FIELDS = gql`
|
|
363
|
+
fragment ToolApprovalRequestFields on ToolApprovalRequest {
|
|
364
|
+
id
|
|
365
|
+
runId
|
|
366
|
+
nodeId
|
|
367
|
+
workflowId
|
|
368
|
+
workflowKey
|
|
369
|
+
status
|
|
370
|
+
toolCalls {
|
|
371
|
+
callId
|
|
372
|
+
name
|
|
373
|
+
argsJson
|
|
374
|
+
}
|
|
375
|
+
decisions {
|
|
376
|
+
callId
|
|
377
|
+
decision
|
|
378
|
+
reason
|
|
379
|
+
}
|
|
380
|
+
timeoutAt
|
|
381
|
+
timeoutAction
|
|
382
|
+
requestedAt
|
|
383
|
+
decidedAt
|
|
384
|
+
decidedBy
|
|
385
|
+
}
|
|
386
|
+
`;
|
|
387
|
+
const GET_PENDING_TOOL_APPROVALS = gql`
|
|
388
|
+
${TOOL_APPROVAL_FIELDS}
|
|
389
|
+
query GetPendingToolApprovals($limit: Int, $offset: Int) {
|
|
390
|
+
pendingToolApprovals(limit: $limit, offset: $offset) {
|
|
391
|
+
totalCount
|
|
392
|
+
items {
|
|
393
|
+
...ToolApprovalRequestFields
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
`;
|
|
398
|
+
const DECIDE_TOOL_APPROVAL = gql`
|
|
399
|
+
${TOOL_APPROVAL_FIELDS}
|
|
400
|
+
mutation DecideToolApproval(
|
|
401
|
+
$id: ID!
|
|
402
|
+
$decisions: [ToolApprovalDecisionInput!]!
|
|
403
|
+
) {
|
|
404
|
+
decideToolApproval(id: $id, decisions: $decisions) {
|
|
405
|
+
success
|
|
406
|
+
error
|
|
407
|
+
resumedFromNodeId
|
|
408
|
+
request {
|
|
409
|
+
...ToolApprovalRequestFields
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
`;
|
|
414
|
+
/**
|
|
415
|
+
* Polls the pending tool-approval inbox. Short polling interval (not a
|
|
416
|
+
* subscription) — approvals are rare/low-volume and the caller wants a
|
|
417
|
+
* near-real-time inbox without standing up a dedicated subscription channel
|
|
418
|
+
* for a v1 feature.
|
|
419
|
+
*
|
|
420
|
+
* Pass `enabled: false` when the caller lacks `tool-approval-request:read`:
|
|
421
|
+
* the query is then never sent, so the gateway's FORBIDDEN never reaches the
|
|
422
|
+
* shell's global error toast (BOFF-10104). If the gateway still denies the
|
|
423
|
+
* query, `forbidden` turns true and polling stops.
|
|
424
|
+
*/
|
|
425
|
+
function usePendingToolApprovals(options) {
|
|
426
|
+
const limit = options?.limit ?? 50;
|
|
427
|
+
const offset = options?.offset ?? 0;
|
|
428
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 15e3;
|
|
429
|
+
const enabled = options?.enabled ?? true;
|
|
430
|
+
const [items, setItems] = useState([]);
|
|
431
|
+
const [totalCount, setTotalCount] = useState(0);
|
|
432
|
+
const [loading, setLoading] = useState(enabled);
|
|
433
|
+
const [error, setError] = useState(null);
|
|
434
|
+
const [forbidden, setForbidden] = useState(false);
|
|
435
|
+
const fetchData = useCallback(async () => {
|
|
436
|
+
if (!enabled) return;
|
|
437
|
+
setError(null);
|
|
438
|
+
try {
|
|
439
|
+
const client = getFluidGridsClient();
|
|
440
|
+
const result = throwIfResultError(await client.query({
|
|
441
|
+
query: GET_PENDING_TOOL_APPROVALS,
|
|
442
|
+
variables: {
|
|
443
|
+
limit,
|
|
444
|
+
offset
|
|
445
|
+
},
|
|
446
|
+
fetchPolicy: "network-only"
|
|
447
|
+
}));
|
|
448
|
+
setItems(result.data?.pendingToolApprovals?.items ?? []);
|
|
449
|
+
setTotalCount(result.data?.pendingToolApprovals?.totalCount ?? 0);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (isForbiddenError(err)) {
|
|
452
|
+
setForbidden(true);
|
|
453
|
+
setItems([]);
|
|
454
|
+
setTotalCount(0);
|
|
455
|
+
} else setError(err);
|
|
456
|
+
} finally {
|
|
457
|
+
setLoading(false);
|
|
458
|
+
}
|
|
459
|
+
}, [
|
|
460
|
+
enabled,
|
|
461
|
+
limit,
|
|
462
|
+
offset
|
|
463
|
+
]);
|
|
464
|
+
useEffect(() => {
|
|
465
|
+
setForbidden(false);
|
|
466
|
+
if (!enabled) {
|
|
467
|
+
setItems([]);
|
|
468
|
+
setTotalCount(0);
|
|
469
|
+
setError(null);
|
|
470
|
+
}
|
|
471
|
+
}, [enabled]);
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
if (!enabled || forbidden) {
|
|
474
|
+
setLoading(false);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
setLoading(true);
|
|
478
|
+
fetchData();
|
|
479
|
+
if (pollIntervalMs <= 0) return;
|
|
480
|
+
const interval = setInterval(fetchData, pollIntervalMs);
|
|
481
|
+
return () => clearInterval(interval);
|
|
482
|
+
}, [
|
|
483
|
+
enabled,
|
|
484
|
+
fetchData,
|
|
485
|
+
forbidden,
|
|
486
|
+
pollIntervalMs
|
|
487
|
+
]);
|
|
488
|
+
return {
|
|
489
|
+
items,
|
|
490
|
+
totalCount,
|
|
491
|
+
loading,
|
|
492
|
+
error,
|
|
493
|
+
forbidden: forbidden || !enabled,
|
|
494
|
+
refetch: fetchData
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function useDecideToolApproval() {
|
|
498
|
+
const [loading, setLoading] = useState(false);
|
|
499
|
+
const [error, setError] = useState(null);
|
|
500
|
+
const decide = useCallback(async (id, decisions) => {
|
|
501
|
+
setLoading(true);
|
|
502
|
+
setError(null);
|
|
503
|
+
try {
|
|
504
|
+
const client = getFluidGridsClient();
|
|
505
|
+
const response = throwIfResultError(await client.mutate({
|
|
506
|
+
mutation: DECIDE_TOOL_APPROVAL,
|
|
507
|
+
variables: {
|
|
508
|
+
id,
|
|
509
|
+
decisions
|
|
510
|
+
}
|
|
511
|
+
})).data?.decideToolApproval;
|
|
512
|
+
if (!response?.success) throw new Error(response?.error ?? "Failed to record the decision");
|
|
513
|
+
return response;
|
|
514
|
+
} catch (err) {
|
|
515
|
+
setError(err);
|
|
516
|
+
throw err;
|
|
517
|
+
} finally {
|
|
518
|
+
setLoading(false);
|
|
519
|
+
}
|
|
520
|
+
}, []);
|
|
521
|
+
return {
|
|
522
|
+
decide,
|
|
523
|
+
decideAll: useCallback(async (request, decision, reason) => {
|
|
524
|
+
return decide(request.id, request.toolCalls.map((call) => ({
|
|
525
|
+
callId: call.callId,
|
|
526
|
+
decision,
|
|
527
|
+
reason
|
|
528
|
+
})));
|
|
529
|
+
}, [decide]),
|
|
530
|
+
loading,
|
|
531
|
+
error
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/modules/tool-approval/hooks/useApprovalWorkflows.ts
|
|
536
|
+
/**
|
|
537
|
+
* Names for the Approvals cards (Navigation v2, BOFF-11848): which workflow
|
|
538
|
+
* each ask or AI action belongs to, and the name of the step that asked.
|
|
539
|
+
*
|
|
540
|
+
* Neither `ToolApprovalRequest` nor `WorkflowHumanRequest` carries a name,
|
|
541
|
+
* only the workflow key (and, for AI actions, the definition id), so the page
|
|
542
|
+
* looks each workflow up once. A failed lookup (a deleted workflow, no access)
|
|
543
|
+
* leaves the card on the key; nothing is shown as an error.
|
|
544
|
+
*/
|
|
545
|
+
const GET_APPROVAL_WORKFLOW = gql`
|
|
546
|
+
query GetApprovalWorkflow($id: ID, $workflowKey: String) {
|
|
547
|
+
workflowDefinition(id: $id, workflowKey: $workflowKey) {
|
|
548
|
+
id
|
|
549
|
+
workflowKey
|
|
550
|
+
name
|
|
551
|
+
definitionJson
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
`;
|
|
555
|
+
/** At most this many workflows are looked up for one page of cards. */
|
|
556
|
+
const MAX_WORKFLOW_LOOKUPS = 20;
|
|
557
|
+
/** The lookup key of a ref: the definition id when known, else the key. */
|
|
558
|
+
function approvalWorkflowRefKey(ref) {
|
|
559
|
+
return ref.workflowId ? `id:${ref.workflowId}` : `key:${ref.workflowKey}`;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* The unique lookup keys of the cards, in first-seen order, capped at
|
|
563
|
+
* MAX_WORKFLOW_LOOKUPS, joined into one string: a poll that returns the same
|
|
564
|
+
* cards gives the same string, so nothing is looked up again.
|
|
565
|
+
*/
|
|
566
|
+
function workflowLookupSignature(refs) {
|
|
567
|
+
const keys = /* @__PURE__ */ new Set();
|
|
568
|
+
for (const ref of refs) {
|
|
569
|
+
keys.add(approvalWorkflowRefKey(ref));
|
|
570
|
+
if (keys.size >= 20) break;
|
|
571
|
+
}
|
|
572
|
+
return [...keys].join("\n");
|
|
573
|
+
}
|
|
574
|
+
/** The query variables for one lookup key ("id:…" or "key:…"). */
|
|
575
|
+
function lookupVariables(key) {
|
|
576
|
+
return key.startsWith("id:") ? { id: key.slice(3) } : { workflowKey: key.slice(4) };
|
|
577
|
+
}
|
|
578
|
+
async function lookUp(key) {
|
|
579
|
+
try {
|
|
580
|
+
const definition = throwIfResultError(await getFluidGridsClient().query({
|
|
581
|
+
query: GET_APPROVAL_WORKFLOW,
|
|
582
|
+
variables: lookupVariables(key),
|
|
583
|
+
fetchPolicy: "cache-first"
|
|
584
|
+
})).data?.workflowDefinition;
|
|
585
|
+
if (!definition?.name) return null;
|
|
586
|
+
return {
|
|
587
|
+
name: definition.name,
|
|
588
|
+
steps: buildNodeDisplayMap(definition.definitionJson)
|
|
589
|
+
};
|
|
590
|
+
} catch {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Looks up the workflows behind the cards while `enabled` (Navigation v2,
|
|
596
|
+
* and the caller may read workflows). Returns what is known so far, keyed by
|
|
597
|
+
* `approvalWorkflowRefKey`.
|
|
598
|
+
*/
|
|
599
|
+
function useApprovalWorkflows(refs, enabled) {
|
|
600
|
+
const signature = workflowLookupSignature(refs);
|
|
601
|
+
const [known, setKnown] = useState(() => /* @__PURE__ */ new Map());
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
if (!enabled || !signature) return;
|
|
604
|
+
let active = true;
|
|
605
|
+
Promise.all(signature.split("\n").map(async (key) => [key, await lookUp(key)])).then((results) => {
|
|
606
|
+
if (!active) return;
|
|
607
|
+
setKnown((current) => {
|
|
608
|
+
const next = new Map(current);
|
|
609
|
+
for (const [key, info] of results) if (info) next.set(key, info);
|
|
610
|
+
return next;
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
return () => {
|
|
614
|
+
active = false;
|
|
615
|
+
};
|
|
616
|
+
}, [enabled, signature]);
|
|
617
|
+
return known;
|
|
618
|
+
}
|
|
619
|
+
/** "Step: Sort the ticket in Support desk" parts for a card, or the key. */
|
|
620
|
+
function approvalLabels(info, workflowKey, nodeId) {
|
|
621
|
+
const display = info?.steps[nodeId];
|
|
622
|
+
return {
|
|
623
|
+
workflow: info?.name ?? workflowKey,
|
|
624
|
+
step: display ? nodeDisplayTitle(display, nodeId) : null
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/modules/tool-approval/utils/paging.ts
|
|
629
|
+
/**
|
|
630
|
+
* Paging for the Approvals inbox (BOFF-11848). Both lists (asks waiting for a
|
|
631
|
+
* person, and the AI assistant's actions waiting for an OK) load 50 at a time.
|
|
632
|
+
*/
|
|
633
|
+
/** Items per page in each Approvals list. */
|
|
634
|
+
const APPROVALS_PAGE_SIZE = 50;
|
|
635
|
+
/**
|
|
636
|
+
* The offset to show for a requested one: answering the last item on a later
|
|
637
|
+
* page empties it, so step back to the last page that still has items.
|
|
638
|
+
* Derived while rendering, so it never needs an effect to correct itself.
|
|
639
|
+
*/
|
|
640
|
+
function clampPageOffset(offset, total, pageSize = 50) {
|
|
641
|
+
if (offset <= 0 || offset < total) return Math.max(0, offset);
|
|
642
|
+
return Math.max(0, Math.ceil(total / pageSize) - 1) * pageSize;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/modules/tool-approval/components/InboxPager.tsx
|
|
646
|
+
/**
|
|
647
|
+
* "{from}–{to} of {total}" with Previous / Next, under each Approvals list
|
|
648
|
+
* (BOFF-7651 for asks; BOFF-11848 adds it to the AI assistant's actions, which
|
|
649
|
+
* had no pager, so anything past the first 50 could not be reached).
|
|
650
|
+
* Renders nothing while everything fits on one page.
|
|
651
|
+
*/
|
|
652
|
+
function InboxPager({ offset, shown, total, onOffsetChange, "data-testid": testId }) {
|
|
653
|
+
const tr = useTr();
|
|
654
|
+
if (total <= 50) return null;
|
|
655
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
656
|
+
className: "mt-4 flex items-center gap-3",
|
|
657
|
+
"data-testid": testId,
|
|
658
|
+
children: [
|
|
659
|
+
/* @__PURE__ */ jsx("span", {
|
|
660
|
+
className: "text-xs text-text-secondary",
|
|
661
|
+
children: tr("fluidgrids.humanResponse.pageRange", "{{from}}–{{to}} of {{total}}", {
|
|
662
|
+
from: offset + 1,
|
|
663
|
+
to: offset + shown,
|
|
664
|
+
total
|
|
665
|
+
})
|
|
666
|
+
}),
|
|
667
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
668
|
+
variant: "outline",
|
|
669
|
+
size: "sm",
|
|
670
|
+
disabled: offset === 0,
|
|
671
|
+
onClick: () => onOffsetChange(Math.max(0, offset - 50)),
|
|
672
|
+
children: tr("fluidgrids.humanResponse.previousPage", "Previous")
|
|
673
|
+
}),
|
|
674
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
675
|
+
variant: "outline",
|
|
676
|
+
size: "sm",
|
|
677
|
+
disabled: offset + 50 >= total,
|
|
678
|
+
onClick: () => onOffsetChange(offset + 50),
|
|
679
|
+
children: tr("fluidgrids.humanResponse.nextPage", "Next")
|
|
680
|
+
})
|
|
681
|
+
]
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/modules/tool-approval/utils/timeout.ts
|
|
686
|
+
/**
|
|
687
|
+
* Classifies timeout metadata without changing server-owned request status.
|
|
688
|
+
* The timeout sweep remains authoritative for auto-resolving WAITING runs.
|
|
689
|
+
*/
|
|
690
|
+
function getToolApprovalTimeoutState(timeoutAt, nowMs = Date.now()) {
|
|
691
|
+
if (!timeoutAt) return "NONE";
|
|
692
|
+
const timeoutMs = Date.parse(timeoutAt);
|
|
693
|
+
if (!Number.isFinite(timeoutMs)) return "NONE";
|
|
694
|
+
return timeoutMs <= nowMs ? "DUE" : "ACTIVE";
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/modules/tool-approval/utils/tool-summary.ts
|
|
698
|
+
/**
|
|
699
|
+
* Plain-language view of an AI assistant's proposed action on the Approvals
|
|
700
|
+
* inbox (Navigation v2, BOFF-11848): "The AI assistant wants to: Send email"
|
|
701
|
+
* with a short key/value summary of its arguments. The raw tool name and JSON
|
|
702
|
+
* stay available under "Show technical details".
|
|
703
|
+
*/
|
|
704
|
+
/** How many arguments the summary lists before "and N more". */
|
|
705
|
+
const SUMMARY_MAX_ENTRIES = 6;
|
|
706
|
+
/** Longest value the summary shows before cutting it with "…". */
|
|
707
|
+
const SUMMARY_MAX_VALUE_LENGTH = 120;
|
|
708
|
+
/**
|
|
709
|
+
* Split an identifier into lower-case words: snake_case, kebab-case,
|
|
710
|
+
* dotted/namespaced (`github.search_issues`, `slack__post`) and camelCase
|
|
711
|
+
* (`searchIssues`, `HTTPRequest`).
|
|
712
|
+
*/
|
|
713
|
+
function words(identifier) {
|
|
714
|
+
return identifier.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(/[\s_\-.:/]+/).map((word) => word.trim().toLowerCase()).filter(Boolean);
|
|
715
|
+
}
|
|
716
|
+
function sentenceCase(parts) {
|
|
717
|
+
const text = parts.join(" ");
|
|
718
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* "send_email" → "Send email", "github.searchIssues" → "Github search issues".
|
|
722
|
+
* An empty or symbol-only name is returned as it is.
|
|
723
|
+
*/
|
|
724
|
+
function friendlyToolName(name) {
|
|
725
|
+
const parts = words(name);
|
|
726
|
+
return parts.length > 0 ? sentenceCase(parts) : name;
|
|
727
|
+
}
|
|
728
|
+
/** "recipient_email" → "Recipient email". */
|
|
729
|
+
function friendlyArgumentName(key) {
|
|
730
|
+
return friendlyToolName(key);
|
|
731
|
+
}
|
|
732
|
+
function shorten(text) {
|
|
733
|
+
return text.length > 120 ? `${text.slice(0, 119)}…` : text;
|
|
734
|
+
}
|
|
735
|
+
function valueText(value) {
|
|
736
|
+
if (value === null || value === void 0) return "—";
|
|
737
|
+
if (typeof value === "string") return shorten(value);
|
|
738
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
739
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) return shorten(value.join(", "));
|
|
740
|
+
return shorten(JSON.stringify(value));
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* The top-level arguments of a tool call as label/value rows. Arguments that
|
|
744
|
+
* are not a JSON object (a list, a bare value, malformed JSON) give no rows:
|
|
745
|
+
* the raw JSON under the technical details says it all.
|
|
746
|
+
*/
|
|
747
|
+
function summarizeToolArguments(argsJson) {
|
|
748
|
+
let parsed;
|
|
749
|
+
try {
|
|
750
|
+
parsed = JSON.parse(argsJson);
|
|
751
|
+
} catch {
|
|
752
|
+
return {
|
|
753
|
+
rows: [],
|
|
754
|
+
hidden: 0
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {
|
|
758
|
+
rows: [],
|
|
759
|
+
hidden: 0
|
|
760
|
+
};
|
|
761
|
+
const entries = Object.entries(parsed);
|
|
762
|
+
return {
|
|
763
|
+
rows: entries.slice(0, 6).map(([key, value]) => ({
|
|
764
|
+
key,
|
|
765
|
+
label: friendlyArgumentName(key),
|
|
766
|
+
value: valueText(value)
|
|
767
|
+
})),
|
|
768
|
+
hidden: Math.max(0, entries.length - 6)
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
/** Pretty-printed raw arguments for the technical details. */
|
|
772
|
+
function formatToolArguments(argsJson) {
|
|
773
|
+
try {
|
|
774
|
+
return JSON.stringify(JSON.parse(argsJson), null, 2);
|
|
775
|
+
} catch {
|
|
776
|
+
return argsJson;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/modules/tool-approval/components/ToolApprovalCard.tsx
|
|
781
|
+
/**
|
|
782
|
+
* One AI assistant action waiting for a decision on the Approvals inbox
|
|
783
|
+
* (BOFF-7521), with the Reject dialog.
|
|
784
|
+
*
|
|
785
|
+
* Navigation v2 (BOFF-11848, FluidGrids only): the card reads "Step: {step}
|
|
786
|
+
* in {workflow}" with an "Open run" link, each proposed call reads "The AI
|
|
787
|
+
* assistant wants to: {action}" with a short summary of its arguments, a
|
|
788
|
+
* time limit says whether it will be approved or rejected automatically, and
|
|
789
|
+
* a request with several calls says the decision covers all of them. The
|
|
790
|
+
* node id, workflow key, run id and raw JSON move under "Show technical
|
|
791
|
+
* details". With the switch off the card is exactly as before.
|
|
792
|
+
*/
|
|
793
|
+
function timeAgo(iso, tr) {
|
|
794
|
+
const then = new Date(iso).getTime();
|
|
795
|
+
const diffMs = Date.now() - then;
|
|
796
|
+
const minutes = Math.floor(diffMs / 6e4);
|
|
797
|
+
if (minutes < 1) return tr("fluidgrids.toolApproval.justNow", "just now");
|
|
798
|
+
if (minutes < 60) return tr("fluidgrids.toolApproval.minutesAgo", "{{minutes}}m ago", { minutes });
|
|
799
|
+
const hours = Math.floor(minutes / 60);
|
|
800
|
+
if (hours < 24) return tr("fluidgrids.toolApproval.hoursAgo", "{{hours}}h ago", { hours });
|
|
801
|
+
return tr("fluidgrids.toolApproval.daysAgo", "{{days}}d ago", { days: Math.floor(hours / 24) });
|
|
802
|
+
}
|
|
803
|
+
/** What the time limit does: today's words, or (v2) approve vs reject. */
|
|
804
|
+
function timeoutText(request, tr, plain) {
|
|
805
|
+
const time = new Date(request.timeoutAt ?? "").toLocaleString();
|
|
806
|
+
if (!plain) return tr("fluidgrids.toolApproval.autoResolvesAt", "auto-resolves at {{time}}", { time });
|
|
807
|
+
return request.timeoutAction === "AUTO_APPROVE" ? tr("fluidgrids.toolApproval.autoApprovesAt", "If nobody decides by {{time}}, it will be approved automatically", { time }) : tr("fluidgrids.toolApproval.autoRejectsAt", "If nobody decides by {{time}}, it will be rejected automatically", { time });
|
|
808
|
+
}
|
|
809
|
+
function RejectDialog({ open, onOpenChange, onConfirm, submitting }) {
|
|
810
|
+
const tr = useTr();
|
|
811
|
+
const v2 = useNavigationV2();
|
|
812
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
813
|
+
open,
|
|
814
|
+
onOpenChange,
|
|
815
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: tr("fluidgrids.toolApproval.rejectDialogTitle", v2 ? "Don't allow this action" : "Reject tool call") }), v2 && /* @__PURE__ */ jsx(DialogDescription, { children: tr("fluidgrids.toolApproval.rejectDialogDescription", "The AI assistant won't take this action, and your workflow will continue.") })] }), /* @__PURE__ */ jsx(RejectReasonForm, {
|
|
816
|
+
onCancel: () => onOpenChange(false),
|
|
817
|
+
onConfirm,
|
|
818
|
+
submitting
|
|
819
|
+
})] })
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
function RejectReasonForm({ onCancel, onConfirm, submitting }) {
|
|
823
|
+
const tr = useTr();
|
|
824
|
+
const v2 = useNavigationV2();
|
|
825
|
+
const [reason, setReason] = useState("");
|
|
826
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
|
|
827
|
+
className: "space-y-1.5",
|
|
828
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
829
|
+
htmlFor: "reject-reason",
|
|
830
|
+
children: v2 ? tr("fluidgrids.toolApproval.rejectReasonLabelPlain", "Tell the AI assistant why (optional)") : tr("fluidgrids.toolApproval.rejectReasonLabel", "Reason (optional)")
|
|
831
|
+
}), /* @__PURE__ */ jsx(Textarea, {
|
|
832
|
+
id: "reject-reason",
|
|
833
|
+
"data-testid": "tool-approval-reject-reason",
|
|
834
|
+
value: reason,
|
|
835
|
+
onChange: (e) => setReason(e.target.value),
|
|
836
|
+
placeholder: v2 ? void 0 : tr("fluidgrids.toolApproval.rejectReasonPlaceholder", "Why is this call being rejected? The agent sees this.")
|
|
837
|
+
})]
|
|
838
|
+
}), /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
|
|
839
|
+
variant: "outline",
|
|
840
|
+
onClick: onCancel,
|
|
841
|
+
disabled: submitting,
|
|
842
|
+
children: tr("fluidgrids.toolApproval.cancel", "Cancel")
|
|
843
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
844
|
+
variant: "destructive",
|
|
845
|
+
onClick: () => onConfirm(reason),
|
|
846
|
+
disabled: submitting,
|
|
847
|
+
children: submitting ? tr("fluidgrids.toolApproval.rejecting", "Rejecting…") : tr("fluidgrids.toolApproval.rejectConfirm", v2 ? "Don't allow" : "Reject")
|
|
848
|
+
})] })] });
|
|
849
|
+
}
|
|
850
|
+
/** Navigation v2: "The AI assistant wants to: Send email" + its arguments. */
|
|
851
|
+
function PlainToolCall({ call }) {
|
|
852
|
+
const tr = useTr();
|
|
853
|
+
const summary = summarizeToolArguments(call.argsJson);
|
|
854
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
855
|
+
className: "rounded-md border border-border-secondary bg-background-primary p-3",
|
|
856
|
+
"data-testid": "tool-approval-call",
|
|
857
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
858
|
+
className: "text-sm font-medium text-text-primary",
|
|
859
|
+
children: tr("fluidgrids.toolApproval.wantsTo", "The AI assistant wants to: {{action}}", { action: friendlyToolName(call.name) })
|
|
860
|
+
}), summary.rows.length > 0 && /* @__PURE__ */ jsxs("dl", {
|
|
861
|
+
className: "mt-2 space-y-1 text-xs",
|
|
862
|
+
children: [summary.rows.map((row) => /* @__PURE__ */ jsxs("div", {
|
|
863
|
+
className: "flex flex-wrap gap-x-2",
|
|
864
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
865
|
+
className: "font-medium text-text-secondary",
|
|
866
|
+
children: row.label
|
|
867
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
868
|
+
className: "min-w-0 whitespace-pre-wrap break-words text-text-primary",
|
|
869
|
+
children: row.value
|
|
870
|
+
})]
|
|
871
|
+
}, row.key)), summary.hidden > 0 && /* @__PURE__ */ jsx("p", {
|
|
872
|
+
className: "text-text-secondary",
|
|
873
|
+
children: tr("fluidgrids.toolApproval.moreArguments", "and {{count}} more (see technical details)", { count: summary.hidden })
|
|
874
|
+
})]
|
|
875
|
+
})]
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
/** Today's call block: the raw tool name and its JSON arguments. */
|
|
879
|
+
function RawToolCall({ call }) {
|
|
880
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
881
|
+
className: "rounded-md border border-border-secondary bg-background-primary p-3",
|
|
882
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
883
|
+
className: "mb-1 flex items-center gap-2",
|
|
884
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
885
|
+
icon: "mdi:function-variant",
|
|
886
|
+
width: 14,
|
|
887
|
+
className: "text-text-secondary"
|
|
888
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
889
|
+
className: "text-sm font-medium text-text-primary",
|
|
890
|
+
children: call.name
|
|
891
|
+
})]
|
|
892
|
+
}), /* @__PURE__ */ jsx("pre", {
|
|
893
|
+
className: "overflow-x-auto whitespace-pre-wrap break-all text-xs text-text-secondary",
|
|
894
|
+
children: formatToolArguments(call.argsJson)
|
|
895
|
+
})]
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
function ToolApprovalCard({ request, onDecided, workflowName, stepName }) {
|
|
899
|
+
const tr = useTr();
|
|
900
|
+
const v2 = useNavigationV2();
|
|
901
|
+
const { decideAll, loading } = useDecideToolApproval();
|
|
902
|
+
const [showReject, setShowReject] = useState(false);
|
|
903
|
+
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
904
|
+
const timeoutState = getToolApprovalTimeoutState(request.timeoutAt, nowMs);
|
|
905
|
+
const timeoutDue = timeoutState === "DUE";
|
|
906
|
+
useEffect(() => {
|
|
907
|
+
if (timeoutState !== "ACTIVE") return;
|
|
908
|
+
const interval = setInterval(() => setNowMs(Date.now()), 1e3);
|
|
909
|
+
return () => clearInterval(interval);
|
|
910
|
+
}, [timeoutState]);
|
|
911
|
+
const handleDecide = async (decision, reason) => {
|
|
912
|
+
try {
|
|
913
|
+
await decideAll(request, decision, reason);
|
|
914
|
+
const approved = decision === "APPROVED";
|
|
915
|
+
toast.success(v2 ? approved ? tr("fluidgrids.toolApproval.allowedToast", "Allowed — your workflow is continuing") : tr("fluidgrids.toolApproval.notAllowedToast", "Not allowed — your workflow is continuing") : approved ? tr("fluidgrids.toolApproval.approvedToast", "Tool call approved — run resumed") : tr("fluidgrids.toolApproval.rejectedToast", "Tool call rejected — run resumed"));
|
|
916
|
+
setShowReject(false);
|
|
917
|
+
onDecided();
|
|
918
|
+
} catch (err) {
|
|
919
|
+
const failed = tr("fluidgrids.toolApproval.decisionFailed", "Failed to record the decision");
|
|
920
|
+
toast.error(failed, { description: describeOperationError(err, failed) });
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
const deniedTooltip = tr("fluidgrids.toolApproval.decideDeniedTooltip", "You don't have permission to decide tool approvals");
|
|
924
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
925
|
+
className: "border border-border-secondary bg-background-secondary",
|
|
926
|
+
"data-testid": "tool-approval-card",
|
|
927
|
+
"data-request-id": request.id,
|
|
928
|
+
children: [
|
|
929
|
+
/* @__PURE__ */ jsxs(CardHeader, {
|
|
930
|
+
className: "pb-3",
|
|
931
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
932
|
+
className: "flex flex-wrap items-center justify-between gap-2",
|
|
933
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
934
|
+
className: "flex items-center gap-2",
|
|
935
|
+
children: [
|
|
936
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
937
|
+
icon: "mdi:robot-outline",
|
|
938
|
+
width: 18,
|
|
939
|
+
className: "text-text-secondary"
|
|
940
|
+
}),
|
|
941
|
+
!v2 && /* @__PURE__ */ jsxs("span", {
|
|
942
|
+
className: "text-sm font-semibold text-text-primary",
|
|
943
|
+
children: [
|
|
944
|
+
tr("fluidgrids.toolApproval.nodeLabel", "Node"),
|
|
945
|
+
":",
|
|
946
|
+
" ",
|
|
947
|
+
request.nodeId
|
|
948
|
+
]
|
|
949
|
+
}),
|
|
950
|
+
v2 && /* @__PURE__ */ jsx("span", {
|
|
951
|
+
className: "text-sm font-semibold text-text-primary",
|
|
952
|
+
"data-testid": "approval-source",
|
|
953
|
+
children: approvalSourceText(workflowName ?? request.workflowKey, stepName, tr)
|
|
954
|
+
})
|
|
955
|
+
]
|
|
956
|
+
}), !v2 && /* @__PURE__ */ jsx(Badge, {
|
|
957
|
+
variant: "outline",
|
|
958
|
+
className: "font-mono text-[10px]",
|
|
959
|
+
children: request.workflowKey
|
|
960
|
+
})]
|
|
961
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
962
|
+
className: "text-xs text-text-secondary",
|
|
963
|
+
children: [
|
|
964
|
+
tr("fluidgrids.toolApproval.requestedAt", "Requested {{when}}", { when: timeAgo(request.requestedAt, tr) }),
|
|
965
|
+
request.timeoutAt && timeoutDue ? ` · ${tr("fluidgrids.toolApproval.timeoutDue", "timeout reached; automatic decision pending")}` : request.timeoutAt && ` · ${timeoutText(request, tr, v2)}`,
|
|
966
|
+
v2 && /* @__PURE__ */ jsxs(Fragment$1, { children: [" · ", /* @__PURE__ */ jsx(OpenRunLink, { runId: request.runId })] })
|
|
967
|
+
]
|
|
968
|
+
})]
|
|
969
|
+
}),
|
|
970
|
+
/* @__PURE__ */ jsxs(CardContent, {
|
|
971
|
+
className: "space-y-3",
|
|
972
|
+
children: [
|
|
973
|
+
request.toolCalls.map((call) => v2 ? /* @__PURE__ */ jsx(PlainToolCall, { call }, call.callId) : /* @__PURE__ */ jsx(RawToolCall, { call }, call.callId)),
|
|
974
|
+
v2 && request.toolCalls.length > 1 && /* @__PURE__ */ jsx("p", {
|
|
975
|
+
className: "text-xs text-text-secondary",
|
|
976
|
+
"data-testid": "tool-approval-all-or-nothing",
|
|
977
|
+
children: tr("fluidgrids.toolApproval.allOrNothing", "Your decision applies to all {{count}} actions in this request.", { count: request.toolCalls.length })
|
|
978
|
+
}),
|
|
979
|
+
/* @__PURE__ */ jsxs("div", {
|
|
980
|
+
className: "flex items-center gap-2 pt-1",
|
|
981
|
+
children: [/* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
982
|
+
permission: FLUIDGRIDS_PERMISSIONS.TOOL_APPROVAL_REQUEST_EXECUTE,
|
|
983
|
+
deniedTooltip,
|
|
984
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
985
|
+
"data-agent": "confirm",
|
|
986
|
+
size: "sm",
|
|
987
|
+
disabled: loading || timeoutDue,
|
|
988
|
+
onClick: () => handleDecide("APPROVED"),
|
|
989
|
+
"data-testid": "tool-approval-approve",
|
|
990
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
991
|
+
icon: "mdi:check",
|
|
992
|
+
className: "mr-1.5",
|
|
993
|
+
width: 16
|
|
994
|
+
}), tr("fluidgrids.toolApproval.approve", "Approve")]
|
|
995
|
+
})
|
|
996
|
+
}), /* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
997
|
+
permission: FLUIDGRIDS_PERMISSIONS.TOOL_APPROVAL_REQUEST_EXECUTE,
|
|
998
|
+
deniedTooltip,
|
|
999
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
1000
|
+
size: "sm",
|
|
1001
|
+
variant: "destructive",
|
|
1002
|
+
disabled: loading || timeoutDue,
|
|
1003
|
+
onClick: () => setShowReject(true),
|
|
1004
|
+
"data-testid": "tool-approval-reject",
|
|
1005
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
1006
|
+
icon: "mdi:close",
|
|
1007
|
+
className: "mr-1.5",
|
|
1008
|
+
width: 16
|
|
1009
|
+
}), tr("fluidgrids.toolApproval.reject", v2 ? "Don't allow" : "Reject")]
|
|
1010
|
+
})
|
|
1011
|
+
})]
|
|
1012
|
+
}),
|
|
1013
|
+
v2 && /* @__PURE__ */ jsx(ApprovalTechnicalDetails, {
|
|
1014
|
+
rows: approvalTechnicalRows(request, tr),
|
|
1015
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1016
|
+
className: "mt-2 space-y-2",
|
|
1017
|
+
children: request.toolCalls.map((call) => /* @__PURE__ */ jsx(RawToolCall, { call }, call.callId))
|
|
1018
|
+
})
|
|
1019
|
+
})
|
|
1020
|
+
]
|
|
1021
|
+
}),
|
|
1022
|
+
/* @__PURE__ */ jsx(RejectDialog, {
|
|
1023
|
+
open: showReject,
|
|
1024
|
+
onOpenChange: setShowReject,
|
|
1025
|
+
submitting: loading,
|
|
1026
|
+
onConfirm: (reason) => handleDecide("REJECTED", reason || void 0)
|
|
1027
|
+
})
|
|
1028
|
+
]
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/modules/tool-approval/pages/ApprovalInboxPage.tsx
|
|
1033
|
+
/**
|
|
1034
|
+
* Approvals inbox (BOFF-7521, BOFF-7651): asks from workflows waiting for a
|
|
1035
|
+
* person, and AI Agent tool calls waiting for a decision (see
|
|
1036
|
+
* `components/ToolApprovalCard.tsx`). A request with several calls is
|
|
1037
|
+
* all-or-nothing: the one decision applies to every call in it (there is no
|
|
1038
|
+
* per-call control). Approving/rejecting resumes the underlying run via
|
|
1039
|
+
* `decideToolApproval`, which reuses the exact same WAITING/
|
|
1040
|
+
* `resumeWorkflowRun` mechanism the Wait node uses.
|
|
1041
|
+
*
|
|
1042
|
+
* Navigation v2 (BOFF-11848, FluidGrids only): both kinds of card name their
|
|
1043
|
+
* workflow and step and link the run, the counter says how many wait in all,
|
|
1044
|
+
* the empty state covers both kinds of ask, and a load error is plain words
|
|
1045
|
+
* with a Retry and the server's message under "Show technical details".
|
|
1046
|
+
*/
|
|
1047
|
+
var ApprovalInboxPage_exports = /* @__PURE__ */ __exportAll({ ApprovalInboxPage: () => ApprovalInboxPage });
|
|
1048
|
+
/** Navigation v2: a list that failed to load, with Retry. */
|
|
1049
|
+
function InboxLoadError({ message, detail, onRetry, className }) {
|
|
1050
|
+
const tr = useTr();
|
|
1051
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1052
|
+
className,
|
|
1053
|
+
role: "alert",
|
|
1054
|
+
"data-testid": "approvals-load-error",
|
|
1055
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1056
|
+
className: "flex flex-wrap items-center gap-3",
|
|
1057
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1058
|
+
className: "text-sm text-destructive",
|
|
1059
|
+
children: message
|
|
1060
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
1061
|
+
variant: "outline",
|
|
1062
|
+
size: "sm",
|
|
1063
|
+
onClick: onRetry,
|
|
1064
|
+
children: tr("fluidgrids.humanResponse.retry", "Retry")
|
|
1065
|
+
})]
|
|
1066
|
+
}), /* @__PURE__ */ jsx(TechnicalDetails, {
|
|
1067
|
+
className: "mt-1",
|
|
1068
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1069
|
+
className: "break-words font-mono text-xs text-text-secondary",
|
|
1070
|
+
children: detail
|
|
1071
|
+
})
|
|
1072
|
+
})]
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
/** A list that failed to load: today's line, or (v2) plain words with Retry. */
|
|
1076
|
+
function InboxError({ error, plain, oldText, plainText, className, onRetry }) {
|
|
1077
|
+
if (!error) return null;
|
|
1078
|
+
if (!plain) return /* @__PURE__ */ jsx("p", {
|
|
1079
|
+
className,
|
|
1080
|
+
role: "alert",
|
|
1081
|
+
children: oldText(error.message)
|
|
1082
|
+
});
|
|
1083
|
+
return /* @__PURE__ */ jsx(InboxLoadError, {
|
|
1084
|
+
className: "mb-4",
|
|
1085
|
+
message: plainText,
|
|
1086
|
+
detail: error.message,
|
|
1087
|
+
onRetry
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
/** "Waiting for a response": the asks for a person, with their pager. */
|
|
1091
|
+
function HumanRequestsSection({ items, totalCount, offset, onOffsetChange, onAnswered, labelsFor }) {
|
|
1092
|
+
const tr = useTr();
|
|
1093
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
1094
|
+
className: "mb-8",
|
|
1095
|
+
"data-testid": "human-requests-section",
|
|
1096
|
+
"data-tour": "fluidgrids-approvals-human",
|
|
1097
|
+
children: [
|
|
1098
|
+
/* @__PURE__ */ jsx("h2", {
|
|
1099
|
+
className: "mb-1 text-lg font-semibold text-text-primary",
|
|
1100
|
+
children: tr("fluidgrids.humanResponse.inboxHeading", "Waiting for a response")
|
|
1101
|
+
}),
|
|
1102
|
+
/* @__PURE__ */ jsx("p", {
|
|
1103
|
+
className: "mb-3 text-xs text-text-secondary",
|
|
1104
|
+
children: tr("fluidgrids.humanResponse.inboxSubtitle", "Workflows paused on a person — approve, decline or answer to let them continue.")
|
|
1105
|
+
}),
|
|
1106
|
+
/* @__PURE__ */ jsx("div", {
|
|
1107
|
+
className: "grid grid-cols-1 gap-4 lg:grid-cols-2",
|
|
1108
|
+
children: items.map((request) => /* @__PURE__ */ jsx(HumanRequestCard, {
|
|
1109
|
+
request,
|
|
1110
|
+
onAnswered,
|
|
1111
|
+
...labelsFor?.(request, request.nodeId)
|
|
1112
|
+
}, request.id))
|
|
1113
|
+
}),
|
|
1114
|
+
/* @__PURE__ */ jsx(InboxPager, {
|
|
1115
|
+
offset,
|
|
1116
|
+
shown: items.length,
|
|
1117
|
+
total: totalCount,
|
|
1118
|
+
onOffsetChange
|
|
1119
|
+
})
|
|
1120
|
+
]
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
/** A role that may not read tool approvals (BOFF-10104). */
|
|
1124
|
+
function ToolApprovalsNoAccess() {
|
|
1125
|
+
const tr = useTr();
|
|
1126
|
+
return /* @__PURE__ */ jsx(Card, {
|
|
1127
|
+
className: "mb-6 border border-border-secondary bg-background-secondary",
|
|
1128
|
+
"data-testid": "tool-approval-no-access",
|
|
1129
|
+
children: /* @__PURE__ */ jsxs(CardContent, {
|
|
1130
|
+
className: "flex items-start gap-3 py-4",
|
|
1131
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
1132
|
+
icon: "mdi:lock-outline",
|
|
1133
|
+
width: 20,
|
|
1134
|
+
className: "mt-0.5 shrink-0 text-text-secondary"
|
|
1135
|
+
}), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
1136
|
+
className: "text-sm font-medium text-text-primary",
|
|
1137
|
+
children: tr("fluidgrids.toolApproval.noAccessTitle", "You can't review AI Agent tool calls")
|
|
1138
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1139
|
+
className: "mt-1 text-xs text-text-secondary",
|
|
1140
|
+
children: tr("fluidgrids.toolApproval.noAccessSubtitle", "Your role doesn't include permission to read tool approval requests. Ask a workspace admin if you need access.")
|
|
1141
|
+
})] })]
|
|
1142
|
+
})
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
/** Nothing waiting: today's words, or (v2) words for both kinds of ask. */
|
|
1146
|
+
function ApprovalsEmptyState({ plain }) {
|
|
1147
|
+
const tr = useTr();
|
|
1148
|
+
return /* @__PURE__ */ jsx(Card, {
|
|
1149
|
+
className: "border border-border-secondary bg-background-secondary",
|
|
1150
|
+
"data-tour": "fluidgrids-approvals-empty",
|
|
1151
|
+
children: /* @__PURE__ */ jsxs(CardContent, {
|
|
1152
|
+
className: "flex flex-col items-center gap-2 py-10 text-center",
|
|
1153
|
+
children: [
|
|
1154
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
1155
|
+
icon: "mdi:shield-check-outline",
|
|
1156
|
+
width: 32,
|
|
1157
|
+
className: "text-text-secondary"
|
|
1158
|
+
}),
|
|
1159
|
+
/* @__PURE__ */ jsx("p", {
|
|
1160
|
+
className: "text-sm font-medium text-text-primary",
|
|
1161
|
+
children: tr("fluidgrids.toolApproval.emptyTitle", "Nothing waiting for approval")
|
|
1162
|
+
}),
|
|
1163
|
+
/* @__PURE__ */ jsx("p", {
|
|
1164
|
+
className: "text-xs text-text-secondary",
|
|
1165
|
+
children: plain ? tr("fluidgrids.approvals.emptyHint", "When a workflow needs your OK or an answer from you, it shows up here.") : tr("fluidgrids.toolApproval.emptySubtitle", "Runs will appear here when an AI Agent with tool approval enabled proposes a tool call.")
|
|
1166
|
+
})
|
|
1167
|
+
]
|
|
1168
|
+
})
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
/** The AI assistant's actions: the counter, the cards and their pager. */
|
|
1172
|
+
function ToolApprovalsList({ items, totalCount, offset, onOffsetChange, onDecided, labelsFor }) {
|
|
1173
|
+
const tr = useTr();
|
|
1174
|
+
const plain = labelsFor !== void 0;
|
|
1175
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1176
|
+
items.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1177
|
+
className: "mb-3 text-xs text-text-secondary",
|
|
1178
|
+
"data-testid": "tool-approval-count",
|
|
1179
|
+
children: plain ? tr("fluidgrids.toolApproval.count", "{{count}} waiting for you", {
|
|
1180
|
+
count: totalCount,
|
|
1181
|
+
total: totalCount
|
|
1182
|
+
}) : tr("fluidgrids.toolApproval.count", "{{count}} pending ({{total}} total)", {
|
|
1183
|
+
count: items.length,
|
|
1184
|
+
total: totalCount
|
|
1185
|
+
})
|
|
1186
|
+
}),
|
|
1187
|
+
/* @__PURE__ */ jsx("div", {
|
|
1188
|
+
className: "grid grid-cols-1 gap-4 lg:grid-cols-2",
|
|
1189
|
+
"data-tour": "fluidgrids-approvals-tools",
|
|
1190
|
+
children: items.map((request) => /* @__PURE__ */ jsx(ToolApprovalCard, {
|
|
1191
|
+
request,
|
|
1192
|
+
onDecided,
|
|
1193
|
+
...labelsFor?.(request, request.nodeId)
|
|
1194
|
+
}, request.id))
|
|
1195
|
+
}),
|
|
1196
|
+
/* @__PURE__ */ jsx(InboxPager, {
|
|
1197
|
+
offset,
|
|
1198
|
+
shown: items.length,
|
|
1199
|
+
total: totalCount,
|
|
1200
|
+
onOffsetChange,
|
|
1201
|
+
"data-testid": "tool-approval-pager"
|
|
1202
|
+
})
|
|
1203
|
+
] });
|
|
1204
|
+
}
|
|
1205
|
+
const HUMAN_PAGE_SIZE = 50;
|
|
1206
|
+
function ApprovalInboxPage() {
|
|
1207
|
+
const tr = useTr();
|
|
1208
|
+
const v2 = useNavigationV2();
|
|
1209
|
+
const navigate = useNavigate();
|
|
1210
|
+
const { allowed: canReadToolApprovals, isLoading: permissionsLoading } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.TOOL_APPROVAL_REQUEST_READ);
|
|
1211
|
+
const [toolOffset, setToolOffset] = useState(0);
|
|
1212
|
+
const { items, totalCount, loading, error, forbidden, refetch } = usePendingToolApprovals({
|
|
1213
|
+
enabled: canReadToolApprovals,
|
|
1214
|
+
limit: 50,
|
|
1215
|
+
offset: toolOffset
|
|
1216
|
+
});
|
|
1217
|
+
const toolPageOffset = clampPageOffset(toolOffset, totalCount);
|
|
1218
|
+
if (toolPageOffset !== toolOffset && !loading) setToolOffset(toolPageOffset);
|
|
1219
|
+
const toolsForbidden = forbidden && !permissionsLoading;
|
|
1220
|
+
const [humanOffset, setHumanOffset] = useState(0);
|
|
1221
|
+
const human = usePendingHumanRequests({
|
|
1222
|
+
limit: HUMAN_PAGE_SIZE,
|
|
1223
|
+
offset: humanOffset
|
|
1224
|
+
});
|
|
1225
|
+
const humanTotal = human.totalCount;
|
|
1226
|
+
useEffect(() => {
|
|
1227
|
+
if (humanOffset > 0 && humanOffset >= humanTotal) setHumanOffset(Math.max(0, Math.ceil(humanTotal / HUMAN_PAGE_SIZE) - 1) * HUMAN_PAGE_SIZE);
|
|
1228
|
+
}, [humanOffset, humanTotal]);
|
|
1229
|
+
const { allowed: canReadWorkflows } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_READ);
|
|
1230
|
+
const workflowRefs = [...human.items.map((request) => ({ workflowKey: request.workflowKey })), ...(toolsForbidden ? [] : items).map((request) => ({
|
|
1231
|
+
workflowKey: request.workflowKey,
|
|
1232
|
+
workflowId: request.workflowId
|
|
1233
|
+
}))];
|
|
1234
|
+
const workflows = useApprovalWorkflows(workflowRefs, v2 && canReadWorkflows);
|
|
1235
|
+
const labelsFor = (ref, nodeId) => {
|
|
1236
|
+
const labels = approvalLabels(workflows.get(approvalWorkflowRefKey(ref)), ref.workflowKey, nodeId);
|
|
1237
|
+
return {
|
|
1238
|
+
workflowName: labels.workflow,
|
|
1239
|
+
stepName: labels.step
|
|
1240
|
+
};
|
|
1241
|
+
};
|
|
1242
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1243
|
+
className: "p-4 sm:p-6 bg-background-primary min-h-screen",
|
|
1244
|
+
children: [
|
|
1245
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1246
|
+
className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6",
|
|
1247
|
+
"data-tour": "fluidgrids-approvals-header",
|
|
1248
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [
|
|
1249
|
+
/* @__PURE__ */ jsx("h1", {
|
|
1250
|
+
className: "text-2xl sm:text-3xl font-bold text-text-primary",
|
|
1251
|
+
children: tr("fluidgrids.approvals.pageTitle", "Approvals")
|
|
1252
|
+
}),
|
|
1253
|
+
/* @__PURE__ */ jsx("p", {
|
|
1254
|
+
className: "text-text-secondary mt-1",
|
|
1255
|
+
children: tr("fluidgrids.approvals.pageSubtitle", v2 ? "Requests from your workflows waiting for a person, and actions your AI assistant wants to take" : "Requests from your workflows waiting for a person, and AI Agent tool calls waiting for a decision")
|
|
1256
|
+
}),
|
|
1257
|
+
/* @__PURE__ */ jsx(FluidGridsDocsLink, {
|
|
1258
|
+
topic: "approvals",
|
|
1259
|
+
className: "mt-1"
|
|
1260
|
+
})
|
|
1261
|
+
] }), /* @__PURE__ */ jsxs(Button$1, {
|
|
1262
|
+
variant: "outline",
|
|
1263
|
+
onClick: () => navigate("../runs"),
|
|
1264
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
1265
|
+
icon: "mdi:arrow-left",
|
|
1266
|
+
className: "mr-2",
|
|
1267
|
+
width: 16
|
|
1268
|
+
}), tr("fluidgrids.toolApproval.backToRuns", "Back to Runs")]
|
|
1269
|
+
})]
|
|
1270
|
+
}),
|
|
1271
|
+
(loading || permissionsLoading) && items.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
1272
|
+
className: "text-text-secondary text-sm",
|
|
1273
|
+
children: tr("fluidgrids.toolApproval.loading", "Loading…")
|
|
1274
|
+
}),
|
|
1275
|
+
/* @__PURE__ */ jsx(InboxError, {
|
|
1276
|
+
error,
|
|
1277
|
+
plain: v2,
|
|
1278
|
+
className: "text-sm text-destructive",
|
|
1279
|
+
oldText: (message) => tr("fluidgrids.toolApproval.loadFailed", "Failed to load pending approvals: {{message}}", { message }),
|
|
1280
|
+
plainText: tr("fluidgrids.toolApproval.loadFailedPlain", "We couldn't load the actions waiting for your OK. Try again in a moment."),
|
|
1281
|
+
onRetry: () => void refetch()
|
|
1282
|
+
}),
|
|
1283
|
+
human.items.length > 0 && /* @__PURE__ */ jsx(HumanRequestsSection, {
|
|
1284
|
+
items: human.items,
|
|
1285
|
+
totalCount: human.totalCount,
|
|
1286
|
+
offset: humanOffset,
|
|
1287
|
+
onOffsetChange: setHumanOffset,
|
|
1288
|
+
onAnswered: human.refetch,
|
|
1289
|
+
labelsFor: v2 ? labelsFor : void 0
|
|
1290
|
+
}),
|
|
1291
|
+
/* @__PURE__ */ jsx(InboxError, {
|
|
1292
|
+
error: human.error,
|
|
1293
|
+
plain: v2,
|
|
1294
|
+
className: "mb-4 text-sm text-destructive",
|
|
1295
|
+
oldText: (message) => tr("fluidgrids.humanResponse.loadFailed", "Failed to load requests waiting for a response: {{message}}", { message }),
|
|
1296
|
+
plainText: tr("fluidgrids.humanResponse.loadFailedPlain", "We couldn't load the requests waiting for your answer. Try again in a moment."),
|
|
1297
|
+
onRetry: () => void human.refetch()
|
|
1298
|
+
}),
|
|
1299
|
+
toolsForbidden && /* @__PURE__ */ jsx(ToolApprovalsNoAccess, {}),
|
|
1300
|
+
!loading && !permissionsLoading && !toolsForbidden && !error && !human.loading && !human.error && items.length === 0 && human.items.length === 0 && /* @__PURE__ */ jsx(ApprovalsEmptyState, { plain: v2 }),
|
|
1301
|
+
!toolsForbidden && /* @__PURE__ */ jsx(ToolApprovalsList, {
|
|
1302
|
+
items,
|
|
1303
|
+
totalCount,
|
|
1304
|
+
offset: toolOffset,
|
|
1305
|
+
onOffsetChange: setToolOffset,
|
|
1306
|
+
onDecided: refetch,
|
|
1307
|
+
labelsFor: v2 ? labelsFor : void 0
|
|
1308
|
+
}),
|
|
1309
|
+
toolsForbidden && /* @__PURE__ */ jsx("div", {
|
|
1310
|
+
className: "grid grid-cols-1 gap-4 lg:grid-cols-2",
|
|
1311
|
+
"data-tour": "fluidgrids-approvals-tools"
|
|
1312
|
+
})
|
|
1313
|
+
]
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
export { HumanRequestCard as a, usePendingToolApprovals as i, ApprovalInboxPage_exports as n, useDecideToolApproval as r, ApprovalInboxPage as t };
|