@burdenoff/fluidgrids-fe-sdk 2026.1021.8 → 2026.1021.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.
@@ -1,4 +1,4 @@
1
- export { ExecutionDetailPage } from './chunk-WUA2WMJH.js';
1
+ export { ExecutionDetailPage } from './chunk-XOUL22OI.js';
2
2
  import './chunk-3SCUZ75C.js';
3
3
  import './chunk-QBFAJNUC.js';
4
4
  import './chunk-ONVGFVNH.js';
@@ -1,6 +1,6 @@
1
- export { WorkflowBuilder } from './chunk-66FIPZKK.js';
1
+ export { WorkflowBuilder } from './chunk-RFG3PJX3.js';
2
2
  import './chunk-6N27YGMB.js';
3
- import './chunk-FZONRYII.js';
3
+ import './chunk-2CDLVDSU.js';
4
4
  import './chunk-GEMNVFPK.js';
5
5
  import './chunk-IEVSFCFS.js';
6
6
  import './chunk-7LSGGJ7D.js';
@@ -3587,7 +3587,11 @@ function isDynamicSpec(spec) {
3587
3587
  var DEFAULT_HANDLES = [{ id: "source", label: "" }];
3588
3588
  function resolveOutputHandles(data) {
3589
3589
  const nodeData = data?.nodeData;
3590
- const spec = nodeData?.outputHandles ?? nodeData?.operations?.outputHandles;
3590
+ const operationsBlob = Array.isArray(nodeData?.operations) ? void 0 : nodeData?.operations;
3591
+ const operationList = Array.isArray(nodeData?.operations) ? nodeData.operations : operationsBlob?.operations ?? [];
3592
+ const operationName = typeof data?.operation_name === "string" ? data.operation_name : void 0;
3593
+ const operationSpec = operationName ? operationList.find((op) => op?.name === operationName)?.outputHandles : void 0;
3594
+ const spec = operationSpec ?? nodeData?.outputHandles ?? operationsBlob?.outputHandles;
3591
3595
  if (!spec) return DEFAULT_HANDLES;
3592
3596
  if (isDynamicSpec(spec)) {
3593
3597
  const args = data?.args;
@@ -1,4 +1,4 @@
1
- import { useImportWorkflowTemplate, FlowBuilderPage, useWorkflowTemplates, TemplateCard } from './chunk-FZONRYII.js';
1
+ import { useImportWorkflowTemplate, FlowBuilderPage, useWorkflowTemplates, TemplateCard } from './chunk-2CDLVDSU.js';
2
2
  import { QuotaExhaustedDialog } from './chunk-SQFFJUAT.js';
3
3
  import { useWorkflowDefinitions, useDuplicateWorkflow } from './chunk-O6E247J6.js';
4
4
  import { templatesControls } from './chunk-D3UWK7GY.js';
@@ -1,5 +1,5 @@
1
1
  import { isCreateIntent, decideMissingWorkflow } from './chunk-6N27YGMB.js';
2
- import { useInstalledNodes, buildNodeCheck, refetchStoreNodes, FlowBuilderPage, buildRequiredNodeTypes, mergeStickyNotes } from './chunk-FZONRYII.js';
2
+ import { useInstalledNodes, buildNodeCheck, refetchStoreNodes, FlowBuilderPage, buildRequiredNodeTypes, mergeStickyNotes } from './chunk-2CDLVDSU.js';
3
3
  import { useLazyWorkflowDefinition, useCreateWorkflow } from './chunk-O6E247J6.js';
4
4
  import { useFlowStore } from './chunk-EGYJKHAL.js';
5
5
  import { useTr } from './chunk-UULVIT25.js';
@@ -1,4 +1,4 @@
1
- import { FluidGridsProvider, Card, FluidGridsMark } from './chunk-FZONRYII.js';
1
+ import { FluidGridsProvider, Card, FluidGridsMark } from './chunk-2CDLVDSU.js';
2
2
  import { Input } from './chunk-IEVSFCFS.js';
3
3
  import { Button } from './chunk-GDAFKRQO.js';
4
4
  import { cn } from './chunk-JL7CASN5.js';
@@ -4,15 +4,16 @@ import { getFluidGridsConfig } from './chunk-UZG3ZIQM.js';
4
4
  import { RunLogsDocument, RunLogAppendedDocument } from './chunk-L2UWPFJM.js';
5
5
  import { throwIfResultError } from './chunk-CFCUM4CN.js';
6
6
  import { useTr } from './chunk-UULVIT25.js';
7
- import { getFluidGridsClientSafe } from './chunk-NTWG3CJX.js';
8
- import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
7
+ import { getFluidGridsClient, getFluidGridsClientSafe } from './chunk-NTWG3CJX.js';
8
+ import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
9
9
  import { useParams, useNavigate, Link } from 'react-router';
10
10
  import { Icon } from '@iconify/react';
11
11
  import { toast } from 'sonner';
12
12
  import { Button } from '@burdenoff/fe-libs/button';
13
- import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from '@burdenoff/fe-libs/ui';
13
+ import { Card, CardHeader, Badge, CardContent, Label, Textarea, AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, Input } from '@burdenoff/fe-libs/ui';
14
14
  import { useActorProfiles, buildActorFallback } from '@burdenoff/fe-libs/shared';
15
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
+ import { gql } from '@apollo/client';
16
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
16
17
 
17
18
  var POLL_INTERVAL_MS = 2e3;
18
19
  var PAGE_SIZE = 500;
@@ -164,6 +165,524 @@ function useRunLogs(runId, options = {}) {
164
165
  refetch
165
166
  };
166
167
  }
168
+ var HUMAN_REQUEST_FIELDS = gql`
169
+ fragment WorkflowHumanRequestFields on WorkflowHumanRequest {
170
+ id
171
+ runId
172
+ nodeId
173
+ workflowKey
174
+ channel
175
+ recipient
176
+ responseType
177
+ title
178
+ message
179
+ approvalType
180
+ approveLabel
181
+ declineLabel
182
+ submitLabel
183
+ fields {
184
+ name
185
+ label
186
+ type
187
+ required
188
+ placeholder
189
+ options
190
+ }
191
+ status
192
+ decision
193
+ response
194
+ respondedBy
195
+ respondedVia
196
+ expiresAt
197
+ respondedAt
198
+ createdAt
199
+ canRespond
200
+ }
201
+ `;
202
+ var GET_PENDING_HUMAN_REQUESTS = gql`
203
+ ${HUMAN_REQUEST_FIELDS}
204
+ query GetPendingWorkflowHumanRequests($limit: Int, $offset: Int) {
205
+ pendingWorkflowHumanRequests(limit: $limit, offset: $offset) {
206
+ totalCount
207
+ items {
208
+ ...WorkflowHumanRequestFields
209
+ }
210
+ }
211
+ }
212
+ `;
213
+ var GET_RUN_HUMAN_REQUESTS = gql`
214
+ ${HUMAN_REQUEST_FIELDS}
215
+ query GetWorkflowHumanRequests($runId: ID!) {
216
+ workflowHumanRequests(runId: $runId) {
217
+ ...WorkflowHumanRequestFields
218
+ }
219
+ }
220
+ `;
221
+ var RESPOND_TO_HUMAN_REQUEST = gql`
222
+ ${HUMAN_REQUEST_FIELDS}
223
+ mutation RespondToWorkflowHumanRequest(
224
+ $input: RespondToWorkflowHumanRequestInput!
225
+ ) {
226
+ respondToWorkflowHumanRequest(input: $input) {
227
+ success
228
+ decision
229
+ error
230
+ request {
231
+ ...WorkflowHumanRequestFields
232
+ }
233
+ }
234
+ }
235
+ `;
236
+ function usePendingHumanRequests(options) {
237
+ const limit = options?.limit ?? 50;
238
+ const offset = options?.offset ?? 0;
239
+ const pollIntervalMs = options?.pollIntervalMs ?? 15e3;
240
+ const [items, setItems] = useState([]);
241
+ const [totalCount, setTotalCount] = useState(0);
242
+ const [loading, setLoading] = useState(true);
243
+ const [error, setError] = useState(null);
244
+ const generation = useRef(0);
245
+ const fetchData = useCallback(async () => {
246
+ const ticket = ++generation.current;
247
+ setError(null);
248
+ try {
249
+ const result = throwIfResultError(
250
+ await getFluidGridsClient().query({
251
+ query: GET_PENDING_HUMAN_REQUESTS,
252
+ variables: { limit, offset },
253
+ fetchPolicy: "network-only"
254
+ })
255
+ );
256
+ if (ticket !== generation.current) return;
257
+ setItems(result.data?.pendingWorkflowHumanRequests?.items ?? []);
258
+ setTotalCount(result.data?.pendingWorkflowHumanRequests?.totalCount ?? 0);
259
+ } catch (err) {
260
+ if (ticket !== generation.current) return;
261
+ setError(err);
262
+ } finally {
263
+ if (ticket === generation.current) setLoading(false);
264
+ }
265
+ }, [limit, offset]);
266
+ useEffect(() => {
267
+ fetchData();
268
+ if (pollIntervalMs <= 0) return;
269
+ const interval = setInterval(fetchData, pollIntervalMs);
270
+ return () => clearInterval(interval);
271
+ }, [fetchData, pollIntervalMs]);
272
+ return { items, totalCount, loading, error, refetch: fetchData };
273
+ }
274
+ function useRunHumanRequests(runId, options) {
275
+ const pollIntervalMs = options?.pollIntervalMs ?? 0;
276
+ const [items, setItems] = useState([]);
277
+ const [error, setError] = useState(null);
278
+ const [loaded, setLoaded] = useState(false);
279
+ const [loadedAt, setLoadedAt] = useState(null);
280
+ const currentRunId = useRef(runId);
281
+ useEffect(() => {
282
+ currentRunId.current = runId;
283
+ setItems([]);
284
+ setError(null);
285
+ setLoaded(false);
286
+ setLoadedAt(null);
287
+ }, [runId]);
288
+ const generation = useRef(0);
289
+ const fetchData = useCallback(async () => {
290
+ if (!runId) return;
291
+ const ticket = ++generation.current;
292
+ const startedAt = Date.now();
293
+ setError(null);
294
+ try {
295
+ const result = throwIfResultError(
296
+ await getFluidGridsClient().query({
297
+ query: GET_RUN_HUMAN_REQUESTS,
298
+ variables: { runId },
299
+ fetchPolicy: "network-only",
300
+ // A fresh request every time: joining a read already in flight would hand back a list
301
+ // fetched before the caller's state change, stamped as if it were newer.
302
+ context: { queryDeduplication: false }
303
+ })
304
+ );
305
+ if (ticket !== generation.current || currentRunId.current !== runId) {
306
+ return;
307
+ }
308
+ setItems(result.data?.workflowHumanRequests ?? []);
309
+ setLoaded(true);
310
+ setLoadedAt(startedAt);
311
+ } catch (err) {
312
+ if (ticket !== generation.current || currentRunId.current !== runId) {
313
+ return;
314
+ }
315
+ setError(err);
316
+ }
317
+ }, [runId]);
318
+ useEffect(() => {
319
+ fetchData();
320
+ if (pollIntervalMs <= 0) return;
321
+ const interval = setInterval(fetchData, pollIntervalMs);
322
+ return () => clearInterval(interval);
323
+ }, [fetchData, pollIntervalMs]);
324
+ return { items, error, loaded, loadedAt, refetch: fetchData };
325
+ }
326
+ function useRespondToHumanRequest() {
327
+ const [loading, setLoading] = useState(false);
328
+ const respond = useCallback(
329
+ async (id, answer) => {
330
+ setLoading(true);
331
+ try {
332
+ const result = throwIfResultError(
333
+ await getFluidGridsClient().mutate({
334
+ mutation: RESPOND_TO_HUMAN_REQUEST,
335
+ variables: { input: { id, ...answer } }
336
+ })
337
+ );
338
+ const payload = result.data?.respondToWorkflowHumanRequest;
339
+ if (!payload?.success) {
340
+ throw new Error(payload?.error || "The response was not recorded");
341
+ }
342
+ return payload.decision;
343
+ } finally {
344
+ setLoading(false);
345
+ }
346
+ },
347
+ []
348
+ );
349
+ return { respond, loading };
350
+ }
351
+ var CHANNEL_ICON = {
352
+ EMAIL: "mdi:email-outline",
353
+ SLACK: "mdi:slack",
354
+ IN_APP: "mdi:bell-outline"
355
+ };
356
+ function channelLabel(channel, tr) {
357
+ switch (channel) {
358
+ case "EMAIL":
359
+ return tr("fluidgrids.humanResponse.channelEmail", "email");
360
+ case "SLACK":
361
+ return tr("fluidgrids.humanResponse.channelSlack", "Slack");
362
+ default:
363
+ return tr("fluidgrids.humanResponse.channelInApp", "in-app");
364
+ }
365
+ }
366
+ function inputType(field) {
367
+ const type = (field.type || "text").toLowerCase();
368
+ return ["email", "number", "date", "tel", "url"].includes(type) ? type : "text";
369
+ }
370
+ function FormControls({
371
+ requestId,
372
+ fields,
373
+ values,
374
+ onChange
375
+ }) {
376
+ return /* @__PURE__ */ jsx("div", { className: "space-y-3", children: fields.map((field) => {
377
+ const id = `human-field-${requestId}-${field.name}`;
378
+ const label = `${field.label || field.name}${field.required ? " *" : ""}`;
379
+ const type = (field.type || "text").toLowerCase();
380
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
381
+ /* @__PURE__ */ jsx(Label, { htmlFor: id, children: label }),
382
+ type === "textarea" ? /* @__PURE__ */ jsx(
383
+ Textarea,
384
+ {
385
+ id,
386
+ value: values[field.name] ?? "",
387
+ placeholder: field.placeholder ?? void 0,
388
+ onChange: (event) => onChange(field.name, event.target.value)
389
+ }
390
+ ) : type === "dropdown" || type === "select" ? /* @__PURE__ */ jsxs(
391
+ "select",
392
+ {
393
+ id,
394
+ className: "w-full rounded-md border border-border-secondary bg-background-primary px-3 py-2 text-sm text-text-primary",
395
+ value: values[field.name] ?? "",
396
+ onChange: (event) => onChange(field.name, event.target.value),
397
+ children: [
398
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
399
+ (field.options ?? []).map((option) => /* @__PURE__ */ jsx("option", { value: option, children: option }, option))
400
+ ]
401
+ }
402
+ ) : /* @__PURE__ */ jsx(
403
+ Input,
404
+ {
405
+ id,
406
+ type: inputType(field),
407
+ value: values[field.name] ?? "",
408
+ placeholder: field.placeholder ?? void 0,
409
+ onChange: (event) => onChange(field.name, event.target.value)
410
+ }
411
+ )
412
+ ] }, field.name);
413
+ }) });
414
+ }
415
+ function HumanRequestCard({
416
+ request,
417
+ onAnswered
418
+ }) {
419
+ const tr = useTr();
420
+ const { respond, loading } = useRespondToHumanRequest();
421
+ const [text, setText] = useState("");
422
+ const [values, setValues] = useState({});
423
+ const submit = async (answer) => {
424
+ try {
425
+ const decision = await respond(request.id, answer);
426
+ toast.success(
427
+ decision === "declined" ? tr(
428
+ "fluidgrids.humanResponse.declinedToast",
429
+ "Declined \u2014 the workflow is continuing"
430
+ ) : tr(
431
+ "fluidgrids.humanResponse.answeredToast",
432
+ "Response sent \u2014 the workflow is continuing"
433
+ )
434
+ );
435
+ onAnswered();
436
+ } catch (err) {
437
+ toast.error(
438
+ err instanceof Error ? err.message : tr(
439
+ "fluidgrids.humanResponse.failed",
440
+ "Your response could not be recorded"
441
+ )
442
+ );
443
+ }
444
+ };
445
+ const missingRequired = request.responseType === "CUSTOM_FORM" && request.fields.some(
446
+ (field) => field.required && !(values[field.name] ?? "").trim()
447
+ );
448
+ const emptyText = request.responseType === "FREE_TEXT" && !text.trim();
449
+ const recipient = request.channel === "IN_APP" && !request.recipient ? tr("fluidgrids.humanResponse.anyMember", "any workspace member") : request.recipient;
450
+ return /* @__PURE__ */ jsxs(
451
+ Card,
452
+ {
453
+ className: "border border-border-secondary bg-background-secondary",
454
+ "data-testid": "human-request-card",
455
+ "data-request-id": request.id,
456
+ children: [
457
+ /* @__PURE__ */ jsxs(CardHeader, { className: "pb-3", children: [
458
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
459
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
460
+ /* @__PURE__ */ jsx(
461
+ Icon,
462
+ {
463
+ icon: CHANNEL_ICON[request.channel] ?? "mdi:account-question",
464
+ width: 18,
465
+ className: "shrink-0 text-text-secondary"
466
+ }
467
+ ),
468
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-text-primary", children: request.title })
469
+ ] }),
470
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "font-mono text-[10px]", children: request.workflowKey })
471
+ ] }),
472
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-text-secondary", children: [
473
+ tr(
474
+ "fluidgrids.humanResponse.askedVia",
475
+ "Asked {{recipient}} via {{channel}}",
476
+ {
477
+ recipient: recipient ?? "",
478
+ channel: channelLabel(request.channel, tr)
479
+ }
480
+ ),
481
+ request.expiresAt && ` \xB7 ${tr("fluidgrids.humanResponse.until", "until {{time}}", {
482
+ time: new Date(request.expiresAt).toLocaleString()
483
+ })}`
484
+ ] })
485
+ ] }),
486
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-3", children: [
487
+ request.message && /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words text-sm text-text-primary", children: request.message }),
488
+ !request.canRespond && /* @__PURE__ */ jsx("p", { className: "text-xs text-text-secondary", children: request.channel === "IN_APP" ? tr(
489
+ "fluidgrids.humanResponse.notForYou",
490
+ "This request was sent to someone else."
491
+ ) : tr(
492
+ "fluidgrids.humanResponse.answerByLink",
493
+ "Answered through the link in the {{channel}} message.",
494
+ { channel: channelLabel(request.channel, tr) }
495
+ ) }),
496
+ request.canRespond && request.responseType === "FREE_TEXT" && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
497
+ /* @__PURE__ */ jsx(Label, { htmlFor: `human-text-${request.id}`, children: tr("fluidgrids.humanResponse.yourResponse", "Your response") }),
498
+ /* @__PURE__ */ jsx(
499
+ Textarea,
500
+ {
501
+ id: `human-text-${request.id}`,
502
+ value: text,
503
+ onChange: (event) => setText(event.target.value)
504
+ }
505
+ )
506
+ ] }),
507
+ request.canRespond && request.responseType === "CUSTOM_FORM" && /* @__PURE__ */ jsx(
508
+ FormControls,
509
+ {
510
+ requestId: request.id,
511
+ fields: request.fields,
512
+ values,
513
+ onChange: (name, value) => setValues((current) => ({ ...current, [name]: value }))
514
+ }
515
+ ),
516
+ request.canRespond && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 pt-1", children: request.responseType === "APPROVAL" ? /* @__PURE__ */ jsxs(Fragment, { children: [
517
+ /* @__PURE__ */ jsxs(
518
+ Button,
519
+ {
520
+ size: "sm",
521
+ disabled: loading,
522
+ onClick: () => submit({ action: "approve" }),
523
+ "data-testid": "human-request-approve",
524
+ children: [
525
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:check", className: "mr-1.5", width: 16 }),
526
+ request.approveLabel
527
+ ]
528
+ }
529
+ ),
530
+ request.approvalType === "double" && /* @__PURE__ */ jsxs(
531
+ Button,
532
+ {
533
+ size: "sm",
534
+ variant: "destructive",
535
+ disabled: loading,
536
+ onClick: () => submit({ action: "decline" }),
537
+ "data-testid": "human-request-decline",
538
+ children: [
539
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:close", className: "mr-1.5", width: 16 }),
540
+ request.declineLabel
541
+ ]
542
+ }
543
+ )
544
+ ] }) : /* @__PURE__ */ jsxs(
545
+ Button,
546
+ {
547
+ size: "sm",
548
+ disabled: loading || missingRequired || emptyText,
549
+ onClick: () => submit(
550
+ request.responseType === "FREE_TEXT" ? { text } : { values }
551
+ ),
552
+ "data-testid": "human-request-submit",
553
+ children: [
554
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:send", className: "mr-1.5", width: 16 }),
555
+ request.submitLabel
556
+ ]
557
+ }
558
+ ) })
559
+ ] })
560
+ ]
561
+ }
562
+ );
563
+ }
564
+
565
+ // src/modules/human-response/utils/answer-rows.ts
566
+ function answerRows(request, tr) {
567
+ if (request.status !== "RESPONDED" || request.responseType === "APPROVAL") {
568
+ return [];
569
+ }
570
+ const data = request.response?.data;
571
+ if (!data || typeof data !== "object" || Array.isArray(data)) return [];
572
+ const values = data;
573
+ const show = (value) => {
574
+ if (Array.isArray(value)) return value.map(String).join(", ");
575
+ if (value === null || value === void 0) return "";
576
+ return String(value);
577
+ };
578
+ if (request.responseType === "FREE_TEXT") {
579
+ return [
580
+ {
581
+ key: "text",
582
+ label: tr("fluidgrids.humanResponse.answerLabel", "Response"),
583
+ value: show(values.text)
584
+ }
585
+ ];
586
+ }
587
+ return request.fields.filter((field) => values[field.name] !== void 0).map((field) => ({
588
+ key: field.name,
589
+ label: field.label || field.name,
590
+ value: show(values[field.name])
591
+ }));
592
+ }
593
+ function outcome(request, tr) {
594
+ const when = request.respondedAt ? new Date(request.respondedAt).toLocaleString() : "";
595
+ switch (request.status) {
596
+ case "RESPONDED":
597
+ return request.decision === "declined" ? tr("fluidgrids.humanResponse.outcomeDeclined", "Declined {{when}}", {
598
+ when
599
+ }) : request.decision === "approved" ? tr(
600
+ "fluidgrids.humanResponse.outcomeApproved",
601
+ "Approved {{when}}",
602
+ {
603
+ when
604
+ }
605
+ ) : tr(
606
+ "fluidgrids.humanResponse.outcomeResponded",
607
+ "Answered {{when}}",
608
+ { when }
609
+ );
610
+ case "TIMED_OUT":
611
+ return tr(
612
+ "fluidgrids.humanResponse.outcomeTimedOut",
613
+ "Nobody answered in time \u2014 continued down Timed out"
614
+ );
615
+ case "CANCELLED":
616
+ return tr(
617
+ "fluidgrids.humanResponse.outcomeCancelled",
618
+ "Withdrawn (the node was retried or the run stopped)"
619
+ );
620
+ default:
621
+ return request.expiresAt ? tr(
622
+ "fluidgrids.humanResponse.waitingUntil",
623
+ "Waiting until {{time}}",
624
+ {
625
+ time: new Date(request.expiresAt).toLocaleString()
626
+ }
627
+ ) : tr("fluidgrids.humanResponse.waiting", "Waiting for a response");
628
+ }
629
+ }
630
+ function RunHumanRequests({ requests }) {
631
+ const tr = useTr();
632
+ if (requests.length === 0) return null;
633
+ return /* @__PURE__ */ jsxs(
634
+ "div",
635
+ {
636
+ className: "mb-6 space-y-2 rounded-lg border border-border-secondary bg-background-secondary p-4",
637
+ "data-testid": "run-human-requests",
638
+ children: [
639
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-text-primary", children: [
640
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:account-clock-outline", width: 18 }),
641
+ tr("fluidgrids.humanResponse.runHeading", "Waiting on people")
642
+ ] }),
643
+ requests.map((request) => {
644
+ const answer = answerRows(request, tr);
645
+ const recipient = request.recipient || tr("fluidgrids.humanResponse.anyMember", "any workspace member");
646
+ return /* @__PURE__ */ jsxs(
647
+ "div",
648
+ {
649
+ className: "flex flex-col gap-0.5 text-sm sm:flex-row sm:flex-wrap sm:items-baseline sm:gap-x-2",
650
+ "data-testid": "run-human-request",
651
+ "data-status": request.status,
652
+ children: [
653
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-text-primary", children: request.title }),
654
+ /* @__PURE__ */ jsxs("span", { className: "text-text-secondary", children: [
655
+ tr(
656
+ "fluidgrids.humanResponse.askedVia",
657
+ "Asked {{recipient}} via {{channel}}",
658
+ { recipient, channel: channelLabel(request.channel, tr) }
659
+ ),
660
+ " \xB7 ",
661
+ outcome(request, tr)
662
+ ] }),
663
+ answer.length > 0 && /* @__PURE__ */ jsx(
664
+ "dl",
665
+ {
666
+ className: "mt-1 w-full space-y-0.5 text-xs sm:mt-0",
667
+ "data-testid": "run-human-answer",
668
+ children: answer.map((row) => /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
669
+ /* @__PURE__ */ jsxs("dt", { className: "shrink-0 font-medium text-text-secondary", children: [
670
+ row.label,
671
+ ":"
672
+ ] }),
673
+ /* @__PURE__ */ jsx("dd", { className: "whitespace-pre-wrap break-words text-text-primary", children: row.value })
674
+ ] }, row.key))
675
+ }
676
+ )
677
+ ]
678
+ },
679
+ request.id
680
+ );
681
+ })
682
+ ]
683
+ }
684
+ );
685
+ }
167
686
  function ExecutionDetailPage() {
168
687
  const tr = useTr();
169
688
  const { runId } = useParams();
@@ -176,6 +695,29 @@ function ExecutionDetailPage() {
176
695
  );
177
696
  const [minLogLevel, setMinLogLevel] = useState(null);
178
697
  const { execution, loading, error, refetch } = useExecution(runId);
698
+ const runCanChange = execution?.status === "RUNNING" || execution?.status === "PENDING" || execution?.status === "WAITING";
699
+ const {
700
+ items: humanRequests,
701
+ loadedAt: humanRequestsLoadedAt,
702
+ error: humanRequestsError,
703
+ refetch: refetchHumanRequests
704
+ } = useRunHumanRequests(runId, {
705
+ pollIntervalMs: runCanChange ? 1e4 : 0
706
+ });
707
+ const awaitingPerson = humanRequests.some(
708
+ (request) => request.status === "PENDING"
709
+ );
710
+ const [waitingSeenAt, setWaitingSeenAt] = useState(null);
711
+ const isWaiting = execution?.status === "WAITING";
712
+ useEffect(() => {
713
+ if (!isWaiting) {
714
+ setWaitingSeenAt(null);
715
+ return;
716
+ }
717
+ setWaitingSeenAt(Date.now());
718
+ void refetchHumanRequests();
719
+ }, [isWaiting, refetchHumanRequests]);
720
+ const humanRequestsCurrent = waitingSeenAt !== null && humanRequestsLoadedAt !== null && humanRequestsLoadedAt >= waitingSeenAt;
179
721
  const nodeDisplay = useMemo(
180
722
  () => buildNodeDisplayMap(execution?.workflow?.definitionJson),
181
723
  [execution?.workflow?.definitionJson]
@@ -203,7 +745,7 @@ function ExecutionDetailPage() {
203
745
  terminateExecution,
204
746
  loading: actionLoading
205
747
  } = useExecutionActions();
206
- const isActive = execution?.status === "RUNNING" || execution?.status === "PENDING" || execution?.status === "WAITING";
748
+ const isActive = runCanChange;
207
749
  const isHistoricalTestRun = execution?.mode === "TEST" && (execution.status === "COMPLETED" || execution.status === "FAILED" || execution.status === "CANCELLED" || execution.status === "TERMINATED");
208
750
  useAutoRefresh(
209
751
  useCallback(() => {
@@ -534,7 +1076,7 @@ function ExecutionDetailPage() {
534
1076
  )
535
1077
  ] }),
536
1078
  canControlRuns && execution.status === "WAITING" && /* @__PURE__ */ jsxs(Fragment, { children: [
537
- /* @__PURE__ */ jsxs(
1079
+ humanRequestsCurrent && !awaitingPerson && /* @__PURE__ */ jsxs(
538
1080
  Button,
539
1081
  {
540
1082
  variant: "outline",
@@ -550,6 +1092,31 @@ function ExecutionDetailPage() {
550
1092
  ]
551
1093
  }
552
1094
  ),
1095
+ !humanRequestsCurrent && humanRequestsError && /* @__PURE__ */ jsxs(
1096
+ "span",
1097
+ {
1098
+ className: "flex items-center gap-2 text-sm text-[var(--color-status-error-text)]",
1099
+ "data-testid": "human-requests-error",
1100
+ children: [
1101
+ tr(
1102
+ "fluidgrids.humanResponse.checkFailed",
1103
+ "Could not check whether this run is waiting on a person."
1104
+ ),
1105
+ /* @__PURE__ */ jsxs(
1106
+ Button,
1107
+ {
1108
+ variant: "outline",
1109
+ size: "sm",
1110
+ onClick: () => void refetchHumanRequests(),
1111
+ children: [
1112
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:refresh", className: "mr-1", width: 16 }),
1113
+ tr("fluidgrids.humanResponse.retry", "Retry")
1114
+ ]
1115
+ }
1116
+ )
1117
+ ]
1118
+ }
1119
+ ),
553
1120
  /* @__PURE__ */ jsxs(
554
1121
  Button,
555
1122
  {
@@ -693,6 +1260,7 @@ function ExecutionDetailPage() {
693
1260
  ] }),
694
1261
  /* @__PURE__ */ jsx("p", { className: "text-[var(--color-status-error-text)]", children: execution.errorMessage })
695
1262
  ] }),
1263
+ /* @__PURE__ */ jsx(RunHumanRequests, { requests: humanRequests }),
696
1264
  /* @__PURE__ */ jsx("div", { className: "border-b border-border-secondary mb-6", children: /* @__PURE__ */ jsx("nav", { className: "flex gap-4 -mb-px", children: tabs.map((tab) => /* @__PURE__ */ jsxs(
697
1265
  "button",
698
1266
  {
@@ -885,4 +1453,4 @@ function ExecutionDetailPage() {
885
1453
  ] });
886
1454
  }
887
1455
 
888
- export { ExecutionDetailPage };
1456
+ export { ExecutionDetailPage, HumanRequestCard, RunHumanRequests, usePendingHumanRequests, useRespondToHumanRequest, useRunHumanRequests };