@sanity/workflow-studio-plugin 0.27.0 → 0.29.0

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.
@@ -8,22 +8,28 @@ import { useState, useEffect, useMemo, useRef, useSyncExternalStore, useCallback
8
8
 
9
9
  import { useRouter } from "sanity/router";
10
10
 
11
- import { isLiveEntry, AbortWorkflowDialog, isEvaluationStale, useSpaceToken, DocPreviewLink, SpinnerSlot, useWorkflowInstanceEntry, useLogEventOnMount, WorkflowInstanceDetailViewed, LoadingRow, InvalidDocNotice, useDefinition, useContainerToken, openActivityGone, InstanceSnapshotBody, ActivityLog, ActivityDetailDialog, instanceBreadcrumb, CodeChip, instanceTitle, namesNoDeployedDefinition, mappingIssueDetail, formatTimeAgo, formatShortDateTime, useWorkflowContext, readDeployedDefinitions, describeError, EmptyState, CautionNote, HoverHint, DismissablePopover, createSubscribers, useDelayedFlag, definitionSnapshotOf, findActivity, stageTitle, stateByActivity, findActivityNode, rowDateControlState, rowAssignControlState, isActivityAssignedTo, assigneesOf, dueDatesOf, datesOf, isOpenActivityStatus, committedRowFace, activityRowProps, gdrLocality, TOAST_ID, WORKFLOW_API_VERSION, useWorkflowToast, openableSchemaType, formatDate, parseStoredDateValue, useUserDisplays, useProjectMembers, WorkflowTaskFiltersApplied, DocRefFace, Hairline, LinkChip, BreadcrumbTail, StageFace, StaleLock, ActivityRow, WorkflowTitleSeedDrifted, UnreadableDocsNote, ForMeEmptyState, useBadgeCapTrim, CountedLabel, MetaRow, TabSwitch, WORKFLOW_PAGE_TABS, definitionFingerprint, LogEventOnMount, WorkflowBoardWorkflowSelected, WorkflowDefinitionDetailViewed, toolRoute, landingState, TOOL_TABS, tabSelectionNavigates, WorkflowToolOpened, toolTabState, workflowPageState, instanceState, backToTabLabel, useAssignmentIdentity, workflowState } from "./index.js";
11
+ import { isLiveEntry, AbortWorkflowDialog, useSpaceToken, isEvaluationStale, DocPreviewLink, SpinnerSlot, useWorkflowInstanceEntry, useLogEventOnMount, WorkflowInstanceDetailViewed, LoadingRow, InvalidDocNotice, useDefinition, useContainerToken, openActivityGone, InstanceFaultNote, InstanceSnapshotBody, ActivityLog, ActivityDetailDialog, instanceBreadcrumb, CodeChip, instanceTitle, dueDatesOf, isDueDatePast, isOpenActivityStatus, definitionSnapshotOf, stageTitle, isActivityAssignedTo, namesNoDeployedDefinition, mappingIssueDetail, formatTimeAgo, formatShortDateTime, useWorkflowContext, readDeployedDefinitions, describeError, gdrLocality, openableSchemaType, identityOf, useMinuteClock, NoteBanner, EmptyState, CountedHeading, AlertGlyph, MetricPair, formatShortAgo, InlineEmphasis, DismissablePopover, createSubscribers, findActivity, stateByActivity, rowDateControlState, rowAssignControlState, isRoleAssignedTo, assigneesOf, datesOf, committedRowFace, activityRowProps, TOAST_ID, WORKFLOW_API_VERSION, useWorkflowToast, formatDate, parseStoredDateValue, useUserDisplays, useProjectMembers, HoverHint, WorkflowTaskFiltersApplied, DocRefFace, Hairline, LinkChip, BreadcrumbTail, StageFace, StaleLock, ActivityRow, WorkflowTitleSeedDrifted, UnreadableDocsNote, RoleAssignedNotice, ForMeEmptyState, MetaRow, TabSwitch, WORKFLOW_PAGE_TABS, definitionFingerprint, LogEventOnMount, WorkflowBoardWorkflowSelected, WorkflowDefinitionDetailViewed, toolRoute, landingState, TOOL_TABS, tabSelectionNavigates, WorkflowToolOpened, toolTabState, workflowPageState, instanceState, backToTabLabel, useAssignmentIdentity, workflowState } from "./index.js";
12
12
 
13
- import { entryDocRefs, startKindOf, isSubjectEntry, isInputSourced, latestDeployedDefinitions, terminalState, isStartableDefinition, toBareId, findOpenStageEntry } from "@sanity/workflow-engine";
13
+ import { entryDocRefs, toBareId, findOpenStageEntry, terminalState, documentStuckCause, startKindOf, isSubjectEntry, isInputSourced, errorMessage, latestDeployedDefinitions, isStartableDefinition, findActivityNode } from "@sanity/workflow-engine";
14
14
 
15
15
  import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
16
16
 
17
- import { TransferIcon } from "@sanity/icons/Transfer";
17
+ import { CalendarIcon } from "@sanity/icons/Calendar";
18
18
 
19
- import { WarningOutlineIcon } from "@sanity/icons/WarningOutline";
19
+ import { ClockIcon } from "@sanity/icons/Clock";
20
20
 
21
- import pluralize from "pluralize-esm";
21
+ import { DocumentIcon } from "@sanity/icons/Document";
22
22
 
23
- import { useSchema, useClient, useDocumentPreviewStore, usePerspective } from "sanity";
23
+ import { ErrorOutlineIcon } from "@sanity/icons/ErrorOutline";
24
+
25
+ import { TransferIcon } from "@sanity/icons/Transfer";
26
+
27
+ import { useDocumentPreviewStore, useSchema, usePerspective, useClient } from "sanity";
24
28
 
25
29
  import { useWorkflowInstances } from "@sanity/workflow-studio";
26
30
 
31
+ import pluralize from "pluralize-esm";
32
+
27
33
  import { AddIcon } from "@sanity/icons/Add";
28
34
 
29
35
  import { SearchIcon } from "@sanity/icons/Search";
@@ -46,6 +52,10 @@ import { ChevronRightIcon } from "@sanity/icons/ChevronRight";
46
52
 
47
53
  import { isReferenceSchemaType } from "@sanity/types";
48
54
 
55
+ import { EmptyIcon } from "@sanity/icons/Empty";
56
+
57
+ import { WarningOutlineIcon } from "@sanity/icons/WarningOutline";
58
+
49
59
  import { WorkflowDiagram } from "@sanity/workflow-diagram";
50
60
 
51
61
  import { useReducedMotion, motion, useSpring, useMotionValueEvent } from "motion/react";
@@ -89,11 +99,22 @@ function documentRefsOf(instance) {
89
99
  return [ ...seen.values() ];
90
100
  }
91
101
 
102
+ function rootInstanceOf(instance, byId) {
103
+ const rootRef = instance.ancestors[0];
104
+ return rootRef ? byId.get(toBareId(rootRef.id)) ?? instance : instance;
105
+ }
106
+
107
+ const TOOL_PANEL_PADDING = 3, TOOL_HEADING_PADDING = 2, PANEL_STEP = 3;
108
+
92
109
  function TabStatusRow({children: children}) {
110
+ const firstLineOffset = useSpaceToken(PANEL_STEP) + useSpaceToken(TOOL_HEADING_PADDING);
93
111
  /* @__PURE__ */
94
112
  return jsx(Box, {
95
- paddingLeft: 2,
96
- paddingY: 3,
113
+ paddingBottom: PANEL_STEP,
114
+ paddingLeft: TOOL_HEADING_PADDING,
115
+ style: {
116
+ paddingTop: firstLineOffset
117
+ },
97
118
  children: children
98
119
  });
99
120
  }
@@ -344,14 +365,24 @@ function InstanceDetailBody({entry: entry, framed: framed, subject: subject}) {
344
365
  },
345
366
  children: /* @__PURE__ */ jsxs(Stack, {
346
367
  gap: 5,
347
- children: [ subject === "preview" ? /* @__PURE__ */ jsx(SubjectPreview, {
368
+ children: [
369
+ /* @__PURE__ */ jsx(InstanceFaultNote, {
370
+ entry: entry,
371
+ scope: {
372
+ kind: "run",
373
+ onOpenActivity: activityName => setOpenActivity({
374
+ stage: instance.currentStage,
375
+ activityName: activityName
376
+ })
377
+ }
378
+ }), subject === "preview" ? /* @__PURE__ */ jsx(SubjectPreview, {
348
379
  instance: instance
349
380
  }) : null,
350
381
  /* @__PURE__ */ jsxs(Stack, {
351
382
  gap: 4,
352
383
  children: [
353
384
  /* @__PURE__ */ jsx(SectionHeading, {
354
- children: "Stages / Tasks"
385
+ children: "Stages"
355
386
  }),
356
387
  /* @__PURE__ */ jsx(InstanceSnapshotBody, {
357
388
  entry: entry,
@@ -433,12 +464,140 @@ function byResolvedTitle(titleOf, keyOf) {
433
464
  };
434
465
  }
435
466
 
467
+ function openActivitiesOf(instance) {
468
+ return (findOpenStageEntry(instance)?.activities ?? []).filter(activity => isOpenActivityStatus(activity.status));
469
+ }
470
+
471
+ function overdueTaskCount(instance, now) {
472
+ return openActivitiesOf(instance).filter(activity => dueDatesOf(activity.fields ?? []).some(due => isDueDatePast(due, now))).length;
473
+ }
474
+
475
+ const NO_SIGNALS = {
476
+ activeCount: 0,
477
+ assignedToMe: 0,
478
+ documentCount: 0,
479
+ documentlessCount: 0,
480
+ initialStageDocumentCount: 0,
481
+ lastTransition: void 0,
482
+ overdueDocumentCount: 0,
483
+ overdueDocumentlessCount: 0,
484
+ stuckCount: 0
485
+ };
486
+
487
+ function noDefinitionSignals() {
488
+ return NO_SIGNALS;
489
+ }
490
+
491
+ function holdsWorkFor(instance, who) {
492
+ return openActivitiesOf(instance).some(activity => isActivityAssignedTo({
493
+ state: activity.fields ?? [],
494
+ who: who
495
+ }));
496
+ }
497
+
498
+ function arrivalOf(instance, byId) {
499
+ const stage = findOpenStageEntry(instance);
500
+ if (stage !== void 0) return {
501
+ at: stage.enteredAt,
502
+ document: documentRefOf(instance, byId),
503
+ stage: stageTitle(definitionSnapshotOf(instance), stage.name)
504
+ };
505
+ }
506
+
507
+ function laterArrival(held, next) {
508
+ return next === void 0 ? held : held === void 0 || next.at > held.at ? next : held;
509
+ }
510
+
511
+ function documentRefOf(instance, byId) {
512
+ return documentRefsOf(rootInstanceOf(instance, byId))[0];
513
+ }
514
+
515
+ const NO_DRAFT = {
516
+ ...NO_SIGNALS,
517
+ documents: /* @__PURE__ */ new Set,
518
+ initialStageDocuments: /* @__PURE__ */ new Set,
519
+ overdueDocuments: /* @__PURE__ */ new Set
520
+ };
521
+
522
+ function withDocument(held, document2) {
523
+ return document2 === void 0 ? held : new Set(held).add(document2);
524
+ }
525
+
526
+ function atInitialStage(instance) {
527
+ const snapshot = definitionSnapshotOf(instance);
528
+ return snapshot !== void 0 && instance.currentStage === snapshot.initialStage;
529
+ }
530
+
531
+ function fold(args) {
532
+ const {byId: byId, held: held, instance: instance, now: now, who: who} = args, stuck = documentStuckCause({
533
+ instance: instance,
534
+ definition: definitionSnapshotOf(instance)
535
+ }), document2 = documentRefOf(instance, byId)?.id, overdue = overdueTaskCount(instance, now) > 0;
536
+ return {
537
+ activeCount: held.activeCount + 1,
538
+ assignedToMe: held.assignedToMe + (who !== void 0 && holdsWorkFor(instance, who) ? 1 : 0),
539
+ documentlessCount: held.documentlessCount + (document2 === void 0 ? 1 : 0),
540
+ documents: withDocument(held.documents, document2),
541
+ initialStageDocuments: atInitialStage(instance) ? withDocument(held.initialStageDocuments, document2) : held.initialStageDocuments,
542
+ lastTransition: laterArrival(held.lastTransition, arrivalOf(instance, byId)),
543
+ overdueDocumentlessCount: held.overdueDocumentlessCount + (overdue && document2 === void 0 ? 1 : 0),
544
+ overdueDocuments: overdue ? withDocument(held.overdueDocuments, document2) : held.overdueDocuments,
545
+ stuckCount: held.stuckCount + (stuck === void 0 ? 0 : 1)
546
+ };
547
+ }
548
+
549
+ function settle(draft) {
550
+ const {documents: documents, initialStageDocuments: initialStageDocuments, overdueDocuments: overdueDocuments, ...rest} = draft;
551
+ return {
552
+ ...rest,
553
+ documentCount: documents.size,
554
+ initialStageDocumentCount: initialStageDocuments.size,
555
+ overdueDocumentCount: overdueDocuments.size
556
+ };
557
+ }
558
+
559
+ function definitionSignals(args) {
560
+ const byId = new Map(args.instances.map(instance => [ instance._id, instance ])), drafts = /* @__PURE__ */ new Map;
561
+ for (const instance of args.instances) {
562
+ if (terminalState(instance) !== "in-flight") continue;
563
+ const held = drafts.get(instance.definition) ?? NO_DRAFT;
564
+ drafts.set(instance.definition, fold({
565
+ byId: byId,
566
+ held: held,
567
+ instance: instance,
568
+ now: args.now,
569
+ who: args.identity
570
+ }));
571
+ }
572
+ return new Map([ ...drafts ].map(([name, draft]) => [ name, settle(draft) ]));
573
+ }
574
+
575
+ function catalogBands(rows) {
576
+ const assigned = rows.filter(row => row.signals.assignedToMe > 0);
577
+ if (assigned.length === 0) return [ {
578
+ rows: rows,
579
+ title: void 0
580
+ } ];
581
+ const rest = rows.filter(row => row.signals.assignedToMe === 0);
582
+ return [ {
583
+ rows: assigned,
584
+ title: "Assigned to you"
585
+ }, {
586
+ rows: rest,
587
+ title: "Other workflows"
588
+ } ].filter(band => band.rows.length > 0);
589
+ }
590
+
436
591
  function definitionCatalog(args) {
437
- const rejected = [ ...args.issues.values() ];
592
+ const rejected = [ ...args.issues.values() ], signals = definitionSignals({
593
+ identity: args.identity,
594
+ instances: args.instances,
595
+ now: args.now
596
+ });
438
597
  return args.definitions.map(definition => ({
439
598
  name: definition.name,
440
599
  title: definition.title ?? definition.name,
441
- activeCount: args.inFlight.filter(instance => instance.definition === definition.name).length,
600
+ signals: signals.get(definition.name) ?? noDefinitionSignals(),
442
601
  docTypes: [ ...new Set(args.mappings.filter(mapping => mapping.definition === definition.name).map(mapping => mapping.docType)) ],
443
602
  brokenBindings: rejected.filter(issue => issue.definition === definition.name && issue.version === definition.version).map(issue => ({
444
603
  docType: issue.docType,
@@ -476,28 +635,60 @@ function deployedPhrase(definition) {
476
635
  return ago === "" ? formatShortDateTime(deployedAt) : `${formatShortDateTime(deployedAt)} (${ago})`;
477
636
  }
478
637
 
479
- const TOOL_PANEL_PADDING = 3, TOOL_HEADING_PADDING = 2;
638
+ const logged = /* @__PURE__ */ new WeakSet;
480
639
 
481
- function catalogReadError(error) {
482
- return `Could not load the deployed workflows: ${error}`;
640
+ function logReadFailure(what, error) {
641
+ if (typeof error == "object" && error !== null) {
642
+ if (logged.has(error)) return;
643
+ logged.add(error);
644
+ }
645
+ console.error(`[workflow-studio-plugin] could not load ${what}:`, error);
646
+ }
647
+
648
+ function useReadFailureLog(what, error) {
649
+ useEffect(() => {
650
+ error !== void 0 && logReadFailure(what, error);
651
+ }, [ what, error ]);
652
+ }
653
+
654
+ const INSTANCE_CAP = 200, INSTANCES_READ_FAILED = "Couldn’t load workflow data. Try again later.";
655
+
656
+ function useCappedInstances(filter) {
657
+ const {engine: engine} = useWorkflowContext(), capped = useMemo(() => ({
658
+ ...filter,
659
+ limit: INSTANCE_CAP
660
+ }), [ filter ]), {instances: instances, loading: loading, unreadable: unreadable, error: error} = useWorkflowInstances({
661
+ engine: engine,
662
+ filter: capped
663
+ });
664
+ return useReadFailureLog("workflow data", error), useMemo(() => {
665
+ const rows = instances ?? [];
666
+ return {
667
+ rows: rows,
668
+ truncated: rows.length + unreadable.length === INSTANCE_CAP,
669
+ loading: loading,
670
+ unreadable: unreadable,
671
+ error: error === void 0 ? void 0 : errorMessage(error)
672
+ };
673
+ }, [ instances, loading, unreadable, error ]);
483
674
  }
484
675
 
485
- const lastRead = /* @__PURE__ */ new WeakMap, issuedReads = /* @__PURE__ */ new WeakMap;
676
+ const CATALOG_READ_FAILED = "Couldn’t load the deployed workflows. Try again later.", lastRead$1 = /* @__PURE__ */ new WeakMap, issuedReads = /* @__PURE__ */ new WeakMap;
486
677
 
487
678
  function useDeployedDefinitions() {
488
- const {engine: engine} = useWorkflowContext(), [definitions, setDefinitions] = useState(() => lastRead.get(engine)?.rows), [error, setError] = useState(void 0);
679
+ const {engine: engine} = useWorkflowContext(), [definitions, setDefinitions] = useState(() => lastRead$1.get(engine)?.rows), [error, setError] = useState(void 0);
489
680
  return useEffect(() => {
490
681
  let cancelled = !1;
491
- setDefinitions(lastRead.get(engine)?.rows), setError(void 0);
682
+ setDefinitions(lastRead$1.get(engine)?.rows), setError(void 0);
492
683
  const seq = (issuedReads.get(engine) ?? 0) + 1;
493
684
  return issuedReads.set(engine, seq), (async () => {
494
- const rows = await readDeployedDefinitions(engine), latest = latestDeployedDefinitions(rows), held = lastRead.get(engine);
495
- (held === void 0 || seq > held.seq) && lastRead.set(engine, {
685
+ const rows = await readDeployedDefinitions(engine), latest = latestDeployedDefinitions(rows), held = lastRead$1.get(engine);
686
+ (held === void 0 || seq > held.seq) && lastRead$1.set(engine, {
496
687
  seq: seq,
497
688
  rows: latest
498
689
  }), cancelled || (setDefinitions(latest), setError(void 0));
499
690
  })().catch(err => {
500
- cancelled || setError(describeError(err));
691
+ logReadFailure("the deployed workflows", err), cancelled || setError(describeError(err));
501
692
  }), () => {
502
693
  cancelled = !0;
503
694
  };
@@ -507,75 +698,197 @@ function useDeployedDefinitions() {
507
698
  };
508
699
  }
509
700
 
510
- const INSTANCE_CAP = 200;
701
+ const NO_TITLES = {
702
+ titles: /* @__PURE__ */ new Map,
703
+ resolved: /* @__PURE__ */ new Set
704
+ };
511
705
 
512
- function useCappedInstances(filter) {
513
- const {engine: engine} = useWorkflowContext(), capped = useMemo(() => ({
514
- ...filter,
515
- limit: INSTANCE_CAP
516
- }), [ filter ]), {instances: instances, loading: loading, unreadable: unreadable} = useWorkflowInstances({
517
- engine: engine,
518
- filter: capped
519
- });
520
- return useMemo(() => {
521
- const rows = instances ?? [];
522
- return {
523
- rows: rows,
524
- truncated: rows.length + unreadable.length === INSTANCE_CAP,
525
- loading: loading,
526
- unreadable: unreadable
706
+ function useDocPreviewTitles(documents) {
707
+ const previewStore = useDocumentPreviewStore(), schema = useSchema(), {perspectiveStack: perspectiveStack} = usePerspective(), {actualTypes: actualTypes, binding: binding} = useWorkflowContext(), [state, setState] = useState(NO_TITLES), key = documents.map(doc => doc.id).join("|");
708
+ return useEffect(() => {
709
+ const titles = /* @__PURE__ */ new Map, resolved = /* @__PURE__ */ new Set, previews = /* @__PURE__ */ new Map, publish = () => setState({
710
+ titles: new Map(titles),
711
+ resolved: new Set(resolved)
712
+ }), localDocs = documents.flatMap(doc => {
713
+ const {parsed: parsed, local: local} = gdrLocality(doc.id, binding.contentResource);
714
+ return !parsed || !local ? (resolved.add(doc.id), []) : [ {
715
+ doc: doc,
716
+ bareId: parsed.documentId
717
+ } ];
718
+ }), untracks = localDocs.map(({bareId: bareId}) => actualTypes.track(bareId)), dropPreview = docId => {
719
+ previews.get(docId)?.unsubscribe(), previews.delete(docId), titles.delete(docId);
720
+ }, observe = ({docId: docId, bareId: bareId, typeName: typeName}) => {
721
+ const schemaType = openableSchemaType(schema, typeName);
722
+ if (!schemaType) return !1;
723
+ if (previews.get(docId)?.typeName === typeName) return !0;
724
+ dropPreview(docId), resolved.delete(docId);
725
+ const subscription = previewStore.observeForPreview({
726
+ _id: bareId,
727
+ _type: schemaType.name
728
+ }, schemaType, {
729
+ perspective: perspectiveStack
730
+ }).subscribe({
731
+ next: event => {
732
+ resolved.add(docId);
733
+ const title = event.snapshot?.title;
734
+ typeof title == "string" ? titles.set(docId, title) : titles.delete(docId), publish();
735
+ },
736
+ error: err => {
737
+ console.error(`[workflow-studio-plugin] preview for "${docId}" failed:`, err), resolved.add(docId),
738
+ publish();
739
+ }
740
+ });
741
+ return previews.set(docId, {
742
+ typeName: typeName,
743
+ unsubscribe: () => subscription.unsubscribe()
744
+ }), !0;
745
+ }, evaluate = () => {
746
+ for (const {doc: doc, bareId: bareId} of localDocs) {
747
+ const probe = actualTypes.read(bareId);
748
+ if (probe === null) {
749
+ dropPreview(doc.id), resolved.add(doc.id);
750
+ continue;
751
+ }
752
+ !observe({
753
+ docId: doc.id,
754
+ bareId: bareId,
755
+ typeName: probe ?? doc.type
756
+ }) && typeof probe == "string" && (dropPreview(doc.id), resolved.add(doc.id));
757
+ }
758
+ publish();
759
+ }, unsubscribeProbes = actualTypes.subscribe(evaluate);
760
+ return evaluate(), () => {
761
+ unsubscribeProbes(), untracks.forEach(untrack => untrack()), previews.forEach(preview => preview.unsubscribe());
527
762
  };
528
- }, [ instances, loading, unreadable ]);
763
+ }, [ key, previewStore, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
764
+ state;
529
765
  }
530
766
 
531
767
  const INSTANCE_CAP_COUNT_NOTE = `Counts from the latest ${INSTANCE_CAP} workflows — older ones aren’t counted.`, NEWEST_INSTANCES = {
532
768
  includeCompleted: !0
533
- };
769
+ }, lastRead = /* @__PURE__ */ new WeakMap;
534
770
 
535
771
  function useToolInstances() {
536
- const {rows: rows, truncated: truncated, loading: loading, unreadable: unreadable} = useCappedInstances(NEWEST_INSTANCES);
537
- return useMemo(() => ({
772
+ const {engine: engine} = useWorkflowContext(), {rows: rows, truncated: truncated, loading: loading, unreadable: unreadable, error: error} = useCappedInstances(NEWEST_INSTANCES), fresh = useMemo(() => ({
538
773
  inFlight: rows.filter(instance => terminalState(instance) === "in-flight"),
539
774
  settled: rows.filter(instance => terminalState(instance) !== "in-flight"),
540
775
  truncated: truncated,
541
776
  loading: loading,
542
- unreadable: unreadable
543
- }), [ rows, truncated, loading, unreadable ]);
777
+ unreadable: unreadable,
778
+ error: error
779
+ }), [ rows, truncated, loading, unreadable, error ]);
780
+ return useEffect(() => {
781
+ !fresh.loading && fresh.error === void 0 && lastRead.set(engine, fresh);
782
+ }, [ engine, fresh ]), loading ? lastRead.get(engine) ?? fresh : fresh;
783
+ }
784
+
785
+ function documentPhrase(count) {
786
+ return count === 0 ? "No documents" : pluralize("document", count, !0);
787
+ }
788
+
789
+ function documentlessTail(count) {
790
+ return count === 0 ? "" : ` (${pluralize("instance", count, !0)})`;
791
+ }
792
+
793
+ function documentlessNote(work) {
794
+ return work.documentCount > 0 ? "" : documentlessTail(work.documentlessCount);
795
+ }
796
+
797
+ function documentFace(work, atStart) {
798
+ const lead = `${documentPhrase(work.documentCount)} with an active workflow${documentlessNote(work)}`;
799
+ return atStart === 0 ? {
800
+ lead: lead,
801
+ startLine: void 0
802
+ } : atStart === work.documentCount ? {
803
+ lead: `${lead}, yet to leave the starting stage`,
804
+ startLine: void 0
805
+ } : {
806
+ lead: lead,
807
+ startLine: `${atStart} still at the starting stage`
808
+ };
809
+ }
810
+
811
+ function overdueHint(work) {
812
+ return `${documentPhrase(work.documentCount)} with overdue tasks${documentlessTail(work.documentlessCount)}`;
813
+ }
814
+
815
+ const UNTITLED_DOCUMENT = "Untitled", TRANSITION_HINT_PENDING = "Loading…";
816
+
817
+ function transitionHeading(at) {
818
+ return `Last stage change at ${formatShortDateTime(at)}`;
819
+ }
820
+
821
+ function transitionFace(args) {
822
+ return args.documentPending ? {
823
+ state: "pending"
824
+ } : {
825
+ state: "ready",
826
+ document: args.documentTitle,
827
+ heading: transitionHeading(args.at),
828
+ lead: args.documentTitle === void 0 ? "Changed to" : "changed to",
829
+ stage: args.stage
830
+ };
831
+ }
832
+
833
+ function catalogView(args) {
834
+ if (args.error !== void 0) return {
835
+ kind: "unreadable"
836
+ };
837
+ if (args.definitions === void 0 || args.instances.loading || args.identity.kind === "pending") return {
838
+ kind: "loading"
839
+ };
840
+ if (args.definitions.length === 0) return {
841
+ kind: "empty"
842
+ };
843
+ const rows = definitionCatalog({
844
+ definitions: args.definitions,
845
+ identity: identityOf(args.identity),
846
+ instances: [ ...args.instances.inFlight, ...args.instances.settled ],
847
+ issues: args.issues,
848
+ mappings: args.mappings,
849
+ now: args.now
850
+ });
851
+ return {
852
+ kind: "bands",
853
+ bands: catalogBands(rows)
854
+ };
544
855
  }
545
856
 
546
- function activeSummary(active) {
547
- return active === 0 ? "No active instances" : pluralize("active instance", active, !0);
857
+ function definitionAlert(row) {
858
+ if (row.signals.stuckCount !== 0) return {
859
+ hint: `${pluralize("run", row.signals.stuckCount, !0)} here can’t continue without help`
860
+ };
548
861
  }
549
862
 
550
- const CARD_MIN_WIDTH = 280, CARD_RADIUS$1 = 3, CARD_PADDING = 4, CARD_MIN_HEIGHT = 180;
863
+ const CARD_MIN_WIDTH = 280, CARD_RADIUS$1 = 3, CARD_PADDING = 4, HINT_DOCUMENT_MAX_WIDTH = 200, BAND_GAP = 6, CARD_MIN_HEIGHT = 120;
551
864
 
552
- function OverviewTab({onOpenWorkflow: onOpenWorkflow}) {
553
- const {definitions: definitions, error: error} = useDeployedDefinitions(), {mappingIssues: mappingIssues, mappings: mappings} = useWorkflowContext(), schema = useSchema(), instances = useToolInstances(), undeployed = /* @__PURE__ */ jsx(UndeployedWorkflows, {
865
+ function OverviewTab({identity: identity, onOpenWorkflow: onOpenWorkflow}) {
866
+ const {definitions: definitions, error: error} = useDeployedDefinitions(), {mappingIssues: mappingIssues, mappings: mappings} = useWorkflowContext(), schema = useSchema(), instances = useToolInstances(), now = useMinuteClock(), view = catalogView({
867
+ definitions: definitions,
868
+ error: error,
869
+ identity: identity,
870
+ instances: instances,
871
+ issues: mappingIssues,
872
+ mappings: mappings,
873
+ now: now
874
+ }), undeployed = /* @__PURE__ */ jsx(UndeployedWorkflows, {
554
875
  notices: undeployedWorkflowNotices({
555
876
  issues: mappingIssues,
556
877
  titleOf: name => schema.get(name)?.title
557
878
  })
558
879
  });
559
- if (error !== void 0) /* @__PURE__ */
560
- return jsx(TabStatusRow, {
561
- children: /* @__PURE__ */ jsx(Text, {
562
- muted: !0,
563
- size: 1,
564
- children: catalogReadError(error)
565
- })
566
- });
567
- if (definitions === void 0) /* @__PURE__ */
568
- return jsx(TabStatusRow, {
880
+ return view.kind === "unreadable" ? /* @__PURE__ */ jsx(NoteBanner, {
881
+ label: CATALOG_READ_FAILED,
882
+ tone: "critical"
883
+ }) : view.kind === "loading" ? /* @__PURE__ */ jsx(TabStatusRow, {
569
884
  children: /* @__PURE__ */ jsx(LoadingRow, {
570
885
  label: "Loading workflows…"
571
886
  })
572
- });
573
- if (definitions.length === 0) /* @__PURE__ */
574
- return jsxs(Flex, {
887
+ }) : view.kind === "empty" ? /* @__PURE__ */ jsxs(Flex, {
575
888
  direction: "column",
576
889
  flex: 1,
577
890
  gap: 3,
578
- paddingBottom: 4,
891
+ paddingBottom: 3,
579
892
  paddingTop: 3,
580
893
  children: [ undeployed,
581
894
  /* @__PURE__ */ jsx(EmptyState, {
@@ -583,67 +896,166 @@ function OverviewTab({onOpenWorkflow: onOpenWorkflow}) {
583
896
  icon: /* @__PURE__ */ jsx(TransferIcon, {}),
584
897
  title: "No workflows deployed"
585
898
  }) ]
586
- });
587
- const rows = definitionCatalog({
588
- definitions: definitions,
589
- mappings: mappings,
590
- inFlight: instances.inFlight,
591
- issues: mappingIssues
592
- }), active = rows.reduce((sum, row) => sum + row.activeCount, 0);
593
- /* @__PURE__ */
594
- return jsxs(Stack, {
899
+ }) : /* @__PURE__ */ jsxs(Stack, {
595
900
  gap: 3,
596
901
  paddingBottom: 4,
597
902
  paddingTop: 3,
598
- children: [ undeployed,
599
- /* @__PURE__ */ jsxs(Flex, {
600
- align: "center",
601
- gap: 2,
602
- padding: TOOL_HEADING_PADDING,
603
- children: [
604
- /* @__PURE__ */ jsx(Text, {
605
- size: 1,
606
- weight: "semibold",
607
- children: pluralize("workflow", rows.length, !0)
608
- }),
609
- /* @__PURE__ */ jsx(Text, {
610
- muted: !0,
611
- size: 1,
612
- children: instances.loading ? "counting active instances…" : activeSummary(active)
613
- }) ]
903
+ children: [ undeployed, instances.error === void 0 ? null : /* @__PURE__ */ jsx(NoteBanner, {
904
+ label: INSTANCES_READ_FAILED,
905
+ tone: "critical"
614
906
  }),
615
- /* @__PURE__ */ jsx(Grid, {
616
- gap: 3,
617
- style: {
618
- gridAutoRows: `minmax(${CARD_MIN_HEIGHT}px, auto)`,
619
- gridTemplateColumns: `repeat(auto-fill, minmax(${CARD_MIN_WIDTH}px, 1fr))`
620
- },
621
- children: rows.map(row => /* @__PURE__ */ jsx(WorkflowCard, {
622
- countPending: instances.loading,
623
- onOpen: () => onOpenWorkflow(row.name),
624
- row: row
625
- }, row.name))
907
+ /* @__PURE__ */ jsx(Stack, {
908
+ gap: BAND_GAP,
909
+ children: view.bands.map(band => /* @__PURE__ */ jsxs(Stack, {
910
+ gap: 3,
911
+ children: [ band.title === void 0 ? null : /* @__PURE__ */ jsx(Box, {
912
+ padding: TOOL_HEADING_PADDING,
913
+ children: /* @__PURE__ */ jsx(CountedHeading, {
914
+ count: band.rows.length,
915
+ title: band.title
916
+ })
917
+ }),
918
+ /* @__PURE__ */ jsx(CardGrid, {
919
+ countKnown: instances.error === void 0,
920
+ now: now,
921
+ onOpenWorkflow: onOpenWorkflow,
922
+ rows: band.rows
923
+ }) ]
924
+ }, band.title ?? UNBANDED_KEY))
626
925
  }), instances.truncated ? /* @__PURE__ */ jsx(MutedNote, {
627
926
  text: INSTANCE_CAP_COUNT_NOTE
628
927
  }) : null ]
629
928
  });
630
929
  }
631
930
 
632
- const CARD_ISSUES_HINT = "This workflow has issues";
931
+ const UNBANDED_KEY = "all";
932
+
933
+ function CardGrid({countKnown: countKnown, now: now, onOpenWorkflow: onOpenWorkflow, rows: rows}) {
934
+ /* @__PURE__ */
935
+ return jsx(Grid, {
936
+ gap: 3,
937
+ style: {
938
+ gridAutoRows: `minmax(${CARD_MIN_HEIGHT}px, auto)`,
939
+ gridTemplateColumns: `repeat(auto-fill, minmax(${CARD_MIN_WIDTH}px, 1fr))`
940
+ },
941
+ children: rows.map(row => /* @__PURE__ */ jsx(WorkflowCard, {
942
+ countKnown: countKnown,
943
+ now: now,
944
+ onOpen: () => onOpenWorkflow(row.name),
945
+ row: row
946
+ }, row.name))
947
+ });
948
+ }
633
949
 
634
950
  function UndeployedWorkflows({notices: notices}) {
635
951
  return notices.length === 0 ? null :
636
952
  /* @__PURE__ */ jsx(Stack, {
637
953
  gap: 2,
638
954
  paddingBottom: 1,
639
- children: notices.map(notice => /* @__PURE__ */ jsx(CautionNote, {
955
+ children: notices.map(notice => /* @__PURE__ */ jsx(NoteBanner, {
640
956
  label: notice
641
957
  }, notice))
642
958
  });
643
959
  }
644
960
 
645
- function WorkflowCard({countPending: countPending, onOpen: onOpen, row: row}) {
646
- const flagged = row.brokenBindings.length > 0, card = /* @__PURE__ */ jsx(Card, {
961
+ function LastTransitionHint({transition: transition}) {
962
+ const documents = useMemo(() => transition.document === void 0 ? [] : [ transition.document ], [ transition.document ]), {resolved: resolved, titles: titles} = useDocPreviewTitles(documents), id = transition.document?.id, face = transitionFace({
963
+ at: transition.at,
964
+ documentPending: id !== void 0 && !resolved.has(id),
965
+ documentTitle: id === void 0 ? void 0 : titles.get(id) ?? UNTITLED_DOCUMENT,
966
+ stage: transition.stage
967
+ });
968
+ return face.state === "pending" ? /* @__PURE__ */ jsx(Text, {
969
+ muted: !0,
970
+ size: 1,
971
+ children: TRANSITION_HINT_PENDING
972
+ }) :
973
+ /* @__PURE__ */ jsxs(Stack, {
974
+ gap: 3,
975
+ children: [
976
+ /* @__PURE__ */ jsx(Text, {
977
+ muted: !0,
978
+ size: 1,
979
+ children: face.heading
980
+ }),
981
+ /* @__PURE__ */ jsxs(Text, {
982
+ size: 1,
983
+ textOverflow: "ellipsis",
984
+ children: [ face.document === void 0 ? null : /* @__PURE__ */ jsx(HintDocument, {
985
+ children: face.document
986
+ }), face.document === void 0 ? "" : " ", face.lead, " ",
987
+ /* @__PURE__ */ jsx(InlineEmphasis, {
988
+ children: face.stage
989
+ }) ]
990
+ }) ]
991
+ });
992
+ }
993
+
994
+ function HintDocument({children: children}) {
995
+ /* @__PURE__ */
996
+ return jsx("span", {
997
+ style: {
998
+ display: "inline-block",
999
+ maxWidth: HINT_DOCUMENT_MAX_WIDTH,
1000
+ overflow: "hidden",
1001
+ textOverflow: "ellipsis",
1002
+ verticalAlign: "bottom",
1003
+ whiteSpace: "nowrap"
1004
+ },
1005
+ children: /* @__PURE__ */ jsx(InlineEmphasis, {
1006
+ children: children
1007
+ })
1008
+ });
1009
+ }
1010
+
1011
+ function overdueWork(signals) {
1012
+ return {
1013
+ documentCount: signals.overdueDocumentCount,
1014
+ documentlessCount: signals.overdueDocumentlessCount
1015
+ };
1016
+ }
1017
+
1018
+ function CardMetrics$1({now: now, signals: signals}) {
1019
+ const overdue = overdueWork(signals), overdueTotal = overdue.documentCount + overdue.documentlessCount, documents = documentFace(signals, signals.initialStageDocumentCount);
1020
+ /* @__PURE__ */
1021
+ return jsxs(Flex, {
1022
+ align: "center",
1023
+ gap: 3,
1024
+ children: [
1025
+ /* @__PURE__ */ jsx(MetricPair, {
1026
+ Icon: DocumentIcon,
1027
+ empty: signals.documentCount === 0 && signals.documentlessCount === 0,
1028
+ face: {
1029
+ text: documents.lead,
1030
+ ...documents.startLine === void 0 ? {} : {
1031
+ description: documents.startLine
1032
+ }
1033
+ },
1034
+ value: String(signals.documentCount)
1035
+ }), signals.lastTransition === void 0 ? null : /* @__PURE__ */ jsx(MetricPair, {
1036
+ Icon: ClockIcon,
1037
+ face: {
1038
+ content: /* @__PURE__ */ jsx(LastTransitionHint, {
1039
+ transition: signals.lastTransition
1040
+ }),
1041
+ wide: !0
1042
+ },
1043
+ value: formatShortAgo(signals.lastTransition.at, now)
1044
+ }), overdueTotal === 0 ? null : /* @__PURE__ */ jsx(MetricPair, {
1045
+ Icon: CalendarIcon,
1046
+ face: {
1047
+ text: overdueHint(overdue)
1048
+ },
1049
+ tone: "caution",
1050
+ value: String(overdueTotal)
1051
+ }) ]
1052
+ });
1053
+ }
1054
+
1055
+ function WorkflowCard({countKnown: countKnown, now: now, onOpen: onOpen, row: row}) {
1056
+ const alert = definitionAlert(row);
1057
+ /* @__PURE__ */
1058
+ return jsxs(Card, {
647
1059
  as: "button",
648
1060
  onClick: onOpen,
649
1061
  padding: CARD_PADDING,
@@ -656,50 +1068,44 @@ function WorkflowCard({countPending: countPending, onOpen: onOpen, row: row}) {
656
1068
  textAlign: "left",
657
1069
  width: "100%"
658
1070
  },
659
- ...flagged ? {
660
- tone: "caution"
661
- } : {},
662
- children: /* @__PURE__ */ jsxs(Flex, {
1071
+ children: [
1072
+ /* @__PURE__ */ jsxs(Flex, {
663
1073
  align: "flex-start",
664
- gap: 3,
665
- children: [ flagged ? /* @__PURE__ */ jsx(Text, {
666
- size: 1,
667
- children: /* @__PURE__ */ jsx(WarningOutlineIcon, {})
668
- }) : null,
669
- /* @__PURE__ */ jsxs(Stack, {
1074
+ gap: 2,
1075
+ style: {
670
1076
  flex: 1,
671
- gap: 3,
1077
+ minWidth: 0
1078
+ },
1079
+ children: [
1080
+ /* @__PURE__ */ jsx(Text, {
1081
+ size: 1,
672
1082
  style: {
1083
+ flex: 1,
673
1084
  minWidth: 0
674
1085
  },
675
- children: [
676
- /* @__PURE__ */ jsx(Text, {
677
- size: 1,
678
- weight: "semibold",
679
- children: row.title
680
- }), countPending ? null : /* @__PURE__ */ jsx(Text, {
681
- muted: !0,
682
- size: 1,
683
- children: activeSummary(row.activeCount)
684
- }) ]
1086
+ weight: "medium",
1087
+ children: row.title
1088
+ }), alert === void 0 ? null : /* @__PURE__ */ jsx(AlertGlyph, {
1089
+ Icon: ErrorOutlineIcon,
1090
+ hint: alert.hint,
1091
+ tone: "critical"
685
1092
  }) ]
686
- })
1093
+ }), countKnown ? /* @__PURE__ */ jsx(CardMetrics$1, {
1094
+ now: now,
1095
+ signals: row.signals
1096
+ }) : null ]
687
1097
  });
688
- return flagged ? /* @__PURE__ */ jsx(HoverHint, {
689
- fill: !0,
690
- text: CARD_ISSUES_HINT,
691
- children: card
692
- }) : card;
693
1098
  }
694
1099
 
695
1100
  function useStartableDefinitions() {
696
- const {definitions: definitions, error: error} = useDeployedDefinitions();
1101
+ const {definitions: definitions, error: error} = useDeployedDefinitions(), startable = useMemo(() => definitions?.filter(row => isStartableDefinition(row)).map(row => ({
1102
+ name: row.name,
1103
+ title: row.title ?? row.name,
1104
+ description: row.description
1105
+ })), [ definitions ]);
697
1106
  return {
698
- definitions: useMemo(() => definitions?.filter(row => isStartableDefinition(row)).map(row => ({
699
- name: row.name,
700
- title: row.title ?? row.name,
701
- description: row.description
702
- })), [ definitions ]),
1107
+ catalogEmpty: definitions?.length === 0,
1108
+ definitions: startable,
703
1109
  error: error
704
1110
  };
705
1111
  }
@@ -731,16 +1137,45 @@ function NewWorkflowButton() {
731
1137
  });
732
1138
  }
733
1139
 
1140
+ const PANEL_WIDTH = 340;
1141
+
1142
+ function NothingToStart({catalogEmpty: catalogEmpty}) {
1143
+ /* @__PURE__ */
1144
+ return jsx(Box, {
1145
+ padding: 4,
1146
+ style: {
1147
+ width: PANEL_WIDTH
1148
+ },
1149
+ children: /* @__PURE__ */ jsxs(Stack, {
1150
+ gap: 3,
1151
+ children: [
1152
+ /* @__PURE__ */ jsx(Text, {
1153
+ align: "center",
1154
+ size: 1,
1155
+ weight: "medium",
1156
+ children: catalogEmpty ? "No workflows deployed" : "Nothing to start by hand"
1157
+ }),
1158
+ /* @__PURE__ */ jsx(Text, {
1159
+ align: "center",
1160
+ muted: !0,
1161
+ size: 1,
1162
+ children: catalogEmpty ? "Deploy a workflow to this project to start one from here" : "Every deployed workflow here is started by a parent workflow, never by hand"
1163
+ }) ]
1164
+ })
1165
+ });
1166
+ }
1167
+
734
1168
  function DefinitionPickerPanel({onPick: onPick}) {
735
- const {definitions: definitions, error: error} = useStartableDefinitions(), [query, setQuery] = useState(""), visible = useMemo(() => {
1169
+ const {catalogEmpty: catalogEmpty, definitions: definitions, error: error} = useStartableDefinitions(), [query, setQuery] = useState(""), visible = useMemo(() => {
736
1170
  const q = query.trim().toLowerCase();
737
1171
  return definitions ? q ? definitions.filter(d => `${d.title} ${d.name}`.toLowerCase().includes(q)) : definitions : [];
738
- }, [ definitions, query ]);
739
- /* @__PURE__ */
740
- return jsx(Box, {
1172
+ }, [ definitions, query ]), settled = error === void 0 ? definitions : void 0;
1173
+ return settled?.length === 0 ? /* @__PURE__ */ jsx(NothingToStart, {
1174
+ catalogEmpty: catalogEmpty
1175
+ }) : /* @__PURE__ */ jsx(Box, {
741
1176
  padding: 2,
742
1177
  style: {
743
- width: 340
1178
+ width: PANEL_WIDTH
744
1179
  },
745
1180
  children: /* @__PURE__ */ jsxs(Stack, {
746
1181
  gap: 2,
@@ -752,13 +1187,12 @@ function DefinitionPickerPanel({onPick: onPick}) {
752
1187
  onChange: event => setQuery(event.currentTarget.value),
753
1188
  placeholder: "Filter workflows…",
754
1189
  value: query
755
- }), error ? /* @__PURE__ */ jsx(MutedNote, {
756
- text: catalogReadError(error)
1190
+ }), error ? /* @__PURE__ */ jsx(NoteBanner, {
1191
+ label: CATALOG_READ_FAILED,
1192
+ tone: "critical"
757
1193
  }) : null, !definitions && !error ? /* @__PURE__ */ jsx(LoadingRow, {
758
1194
  label: "Loading workflows…"
759
- }) : null, definitions && definitions.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
760
- text: "No startable workflows are deployed"
761
- }) : null, definitions && definitions.length > 0 && visible.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
1195
+ }) : null, settled !== void 0 && settled.length > 0 && visible.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
762
1196
  text: "No workflows match"
763
1197
  }) : null,
764
1198
  /* @__PURE__ */ jsx(Stack, {
@@ -1036,13 +1470,20 @@ function useBandOpen(bands, key) {
1036
1470
  };
1037
1471
  }
1038
1472
 
1039
- const REVEAL_MAX_WAIT_MS = 2e3;
1473
+ const REVEAL_STALL_MS = 2e3, REVEAL_COLD_MS = 15e3;
1040
1474
 
1041
1475
  function useRevealGate(args) {
1042
- const {loading: loading, ready: ready} = args, settled = !loading && ready, expired = useDelayedFlag(!loading && !ready, REVEAL_MAX_WAIT_MS), [revealed, setRevealed] = useState(!1);
1476
+ const {loading: loading, progress: progress, ready: ready} = args, settled = !loading && ready, [stalled, setStalled] = useState(!1);
1477
+ useEffect(() => {
1478
+ if (loading || ready) return;
1479
+ setStalled(!1);
1480
+ const budget = progress > 0 ? REVEAL_STALL_MS : REVEAL_COLD_MS, timer = setTimeout(() => setStalled(!0), budget);
1481
+ return () => clearTimeout(timer);
1482
+ }, [ loading, progress, ready ]);
1483
+ const [revealed, setRevealed] = useState(!1);
1043
1484
  return useEffect(() => {
1044
- !revealed && (settled || expired) && setRevealed(!0);
1045
- }, [ expired, revealed, settled ]), revealed || settled || expired;
1485
+ !revealed && (settled || stalled) && setRevealed(!0);
1486
+ }, [ revealed, settled, stalled ]), revealed || settled || stalled;
1046
1487
  }
1047
1488
 
1048
1489
  function spawnOrigins(instances) {
@@ -1055,11 +1496,6 @@ function spawnOrigins(instances) {
1055
1496
  return origins;
1056
1497
  }
1057
1498
 
1058
- function rootOf(instance, byId) {
1059
- const rootRef = instance.ancestors[0];
1060
- return rootRef ? byId.get(toBareId(rootRef.id)) ?? instance : instance;
1061
- }
1062
-
1063
1499
  function taskRowsOf(args) {
1064
1500
  const {instance: instance, identity: identity} = args, open = findOpenStageEntry(instance);
1065
1501
  if (!open) return [];
@@ -1089,6 +1525,10 @@ function taskRowsOf(args) {
1089
1525
  state: fields,
1090
1526
  who: identity
1091
1527
  }),
1528
+ roleMatched: identity !== void 0 && isRoleAssignedTo({
1529
+ state: fields,
1530
+ who: identity
1531
+ }),
1092
1532
  assignControl: rowAssignControlState(controlArgs),
1093
1533
  dateControl: rowDateControlState(controlArgs)
1094
1534
  };
@@ -1115,7 +1555,7 @@ function deriveTaskGroups(args) {
1115
1555
  activityName: origin.activityName
1116
1556
  }).title : void 0,
1117
1557
  rows: rows
1118
- }, root = rootOf(instance, byId), documents = documentRefsOf(root), document2 = documents[0];
1558
+ }, root = rootInstanceOf(instance, byId), documents = documentRefsOf(root), document2 = documents[0];
1119
1559
  if (!document2) {
1120
1560
  withoutDocuments.push(entry);
1121
1561
  continue;
@@ -1171,6 +1611,11 @@ function onlyMyTasks(groups) {
1171
1611
  });
1172
1612
  }
1173
1613
 
1614
+ function hasRoleMatchedTasks(groups) {
1615
+ const bandHasOne = band => band.rows.some(row => row.roleMatched);
1616
+ return groups.withDocuments.some(group => group.instances.some(bandHasOne)) || groups.withoutDocuments.some(bandHasOne);
1617
+ }
1618
+
1174
1619
  function rowActivityEval(entry, row) {
1175
1620
  const evaluation = entry?.evaluation;
1176
1621
  if (!(evaluation === void 0 || evaluation.currentStage.stage.name !== row.stage)) return evaluation.currentStage.activities.find(a => a.activity.name === row.activityName);
@@ -1314,7 +1759,7 @@ function OrphanedWorkflowsNote({orphans: orphans}) {
1314
1759
  /* @__PURE__ */
1315
1760
  return jsxs(Fragment, {
1316
1761
  children: [
1317
- /* @__PURE__ */ jsx(CautionNote, {
1762
+ /* @__PURE__ */ jsx(NoteBanner, {
1318
1763
  action: /* @__PURE__ */ jsx(Button, {
1319
1764
  fontSize: 1,
1320
1765
  mode: "ghost",
@@ -1416,72 +1861,6 @@ function SettleOrphansDialog({workflows: workflows, onClose: onClose}) {
1416
1861
  });
1417
1862
  }
1418
1863
 
1419
- const NO_TITLES = {
1420
- titles: /* @__PURE__ */ new Map,
1421
- resolved: /* @__PURE__ */ new Set
1422
- };
1423
-
1424
- function useDocPreviewTitles(documents) {
1425
- const previewStore = useDocumentPreviewStore(), schema = useSchema(), {perspectiveStack: perspectiveStack} = usePerspective(), {actualTypes: actualTypes, binding: binding} = useWorkflowContext(), [state, setState] = useState(NO_TITLES), key = documents.map(doc => doc.id).join("|");
1426
- return useEffect(() => {
1427
- const titles = /* @__PURE__ */ new Map, resolved = /* @__PURE__ */ new Set, previews = /* @__PURE__ */ new Map, publish = () => setState({
1428
- titles: new Map(titles),
1429
- resolved: new Set(resolved)
1430
- }), localDocs = documents.flatMap(doc => {
1431
- const {parsed: parsed, local: local} = gdrLocality(doc.id, binding.contentResource);
1432
- return !parsed || !local ? (resolved.add(doc.id), []) : [ {
1433
- doc: doc,
1434
- bareId: parsed.documentId
1435
- } ];
1436
- }), untracks = localDocs.map(({bareId: bareId}) => actualTypes.track(bareId)), dropPreview = docId => {
1437
- previews.get(docId)?.unsubscribe(), previews.delete(docId), titles.delete(docId);
1438
- }, observe = ({docId: docId, bareId: bareId, typeName: typeName}) => {
1439
- const schemaType = openableSchemaType(schema, typeName);
1440
- if (!schemaType) return !1;
1441
- if (previews.get(docId)?.typeName === typeName) return !0;
1442
- dropPreview(docId), resolved.delete(docId);
1443
- const subscription = previewStore.observeForPreview({
1444
- _id: bareId,
1445
- _type: schemaType.name
1446
- }, schemaType, {
1447
- perspective: perspectiveStack
1448
- }).subscribe({
1449
- next: event => {
1450
- resolved.add(docId);
1451
- const title = event.snapshot?.title;
1452
- typeof title == "string" ? titles.set(docId, title) : titles.delete(docId), publish();
1453
- },
1454
- error: err => {
1455
- console.error(`[workflow-studio-plugin] preview for "${docId}" failed:`, err), resolved.add(docId),
1456
- publish();
1457
- }
1458
- });
1459
- return previews.set(docId, {
1460
- typeName: typeName,
1461
- unsubscribe: () => subscription.unsubscribe()
1462
- }), !0;
1463
- }, evaluate = () => {
1464
- for (const {doc: doc, bareId: bareId} of localDocs) {
1465
- const probe = actualTypes.read(bareId);
1466
- if (probe === null) {
1467
- dropPreview(doc.id), resolved.add(doc.id);
1468
- continue;
1469
- }
1470
- !observe({
1471
- docId: doc.id,
1472
- bareId: bareId,
1473
- typeName: probe ?? doc.type
1474
- }) && typeof probe == "string" && (dropPreview(doc.id), resolved.add(doc.id));
1475
- }
1476
- publish();
1477
- }, unsubscribeProbes = actualTypes.subscribe(evaluate);
1478
- return evaluate(), () => {
1479
- unsubscribeProbes(), untracks.forEach(untrack => untrack()), previews.forEach(preview => preview.unsubscribe());
1480
- };
1481
- }, [ key, previewStore, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
1482
- state;
1483
- }
1484
-
1485
1864
  function taskFilterOptions(groups) {
1486
1865
  const workflows = /* @__PURE__ */ new Map, documents = /* @__PURE__ */ new Map;
1487
1866
  for (const group of groups.withDocuments) {
@@ -2650,7 +3029,7 @@ function useOrphanedWorkflows(groups) {
2650
3029
  }), [ groups, contentResource, missing ]);
2651
3030
  }
2652
3031
 
2653
- function TasksTab({filterSlot: filterSlot, instances: instances, loading: loading, identity: identity, onOpenInstance: onOpenInstance, onOpenTask: onOpenTask, tab: tab, truncated: truncated, unreadable: unreadable}) {
3032
+ function TasksTab({error: error, filterSlot: filterSlot, instances: instances, loading: loading, identity: identity, onOpenInstance: onOpenInstance, onOpenTask: onOpenTask, tab: tab, truncated: truncated, unreadable: unreadable}) {
2654
3033
  const mineOnly = tab === "for-me", {binding: binding} = useWorkflowContext(), bands = useBandOpenState({
2655
3034
  segment: TASK_BAND_SEGMENTS[tab],
2656
3035
  scope: binding.engineResource.id
@@ -2659,6 +3038,7 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
2659
3038
  identity: identity
2660
3039
  }), [ instances, identity ]), orphans = useOrphanedWorkflows(groups), listed = useMemo(() => hideOrphanedGroups(groups, orphans.groupKeys), [ groups, orphans.groupKeys ]), options = useMemo(() => taskFilterOptions(listed), [ listed ]), titles = useGroupTitles(listed), revealed = useRevealGate({
2661
3040
  loading: loading,
3041
+ progress: titles.resolved.size,
2662
3042
  ready: titles.ready
2663
3043
  }), visible = useMemo(() => {
2664
3044
  const filtered = applyTaskFilters({
@@ -2668,6 +3048,10 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
2668
3048
  }), cut = mineOnly ? onlyMyTasks(filtered) : filtered;
2669
3049
  return orderGroupsByPreviewTitle(cut, titles.titles);
2670
3050
  }, [ listed, filters, mineOnly, titles ]);
3051
+ if (error !== void 0) /* @__PURE__ */ return jsx(NoteBanner, {
3052
+ label: INSTANCES_READ_FAILED,
3053
+ tone: "critical"
3054
+ });
2671
3055
  if (loading || !revealed) /* @__PURE__ */
2672
3056
  return jsx(TabStatusRow, {
2673
3057
  children: /* @__PURE__ */ jsx(LoadingRow, {
@@ -2707,13 +3091,37 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
2707
3091
  }),
2708
3092
  /* @__PURE__ */ jsx(OrphanedWorkflowsNote, {
2709
3093
  orphans: orphans
2710
- }), body, truncated ? /* @__PURE__ */ jsx(MutedNote, {
3094
+ }), mineOnly && hasRoleMatchedTasks(visible) ? /* @__PURE__ */ jsx(RoleAssignedNotice, {}) : null, body, truncated ? /* @__PURE__ */ jsx(MutedNote, {
2711
3095
  text: `Tasks from the latest ${INSTANCE_CAP} workflows — older ones aren’t listed.`
2712
3096
  }) : null ]
2713
3097
  }) ]
2714
3098
  });
2715
3099
  }
2716
3100
 
3101
+ function cardFacts(args) {
3102
+ return new Map(args.instances.map(instance => [ instance._id, {
3103
+ pending: openActivitiesOf(instance).length,
3104
+ overdue: overdueTaskCount(instance, args.now),
3105
+ stuck: documentStuckCause({
3106
+ instance: instance,
3107
+ definition: definitionSnapshotOf(instance)
3108
+ }) !== void 0,
3109
+ settled: terminalState(instance) !== "in-flight"
3110
+ } ]));
3111
+ }
3112
+
3113
+ function laneTotals(cards, facts) {
3114
+ let overdue = 0, stuck = 0;
3115
+ for (const card of cards) {
3116
+ const held = facts.get(card.instanceId);
3117
+ held !== void 0 && (held.overdue > 0 && (overdue += 1), held.stuck && (stuck += 1));
3118
+ }
3119
+ return {
3120
+ overdue: overdue,
3121
+ stuck: stuck
3122
+ };
3123
+ }
3124
+
2717
3125
  const BOARD_EMPTY_STAGE_NOTE = "No items";
2718
3126
 
2719
3127
  function faceOf(instance) {
@@ -2772,6 +3180,24 @@ function boardDocuments(board) {
2772
3180
  return board.groups.flatMap(group => group.cards.flatMap(card => card.face.kind === "document" ? [ card.face.document ] : []));
2773
3181
  }
2774
3182
 
3183
+ function pendingHint(pending) {
3184
+ return pending === 0 ? "All tasks done" : `${pluralize("task", pending, !0)} still open`;
3185
+ }
3186
+
3187
+ function overdueTasksHint(overdue) {
3188
+ return `${pluralize("task", overdue, !0)} past ${overdue === 1 ? "its" : "their"} due date`;
3189
+ }
3190
+
3191
+ const STUCK_RUN_HINT = "This run can’t continue without help";
3192
+
3193
+ function laneOverdueHint(overdue) {
3194
+ return `${overdue} of these ${overdue === 1 ? "has" : "have"} overdue tasks`;
3195
+ }
3196
+
3197
+ function laneStuckHint(stuck) {
3198
+ return `${stuck} of these can’t continue without help`;
3199
+ }
3200
+
2775
3201
  function CardTitleFace({title: title}) {
2776
3202
  return title.kind === "title" ? /* @__PURE__ */ jsx(Fragment, {
2777
3203
  children: title.title
@@ -2798,23 +3224,10 @@ function cardTitle(args) {
2798
3224
  };
2799
3225
  }
2800
3226
 
2801
- function ownOpenTasks(instance) {
2802
- const open = findOpenStageEntry(instance);
2803
- return open ? (open.activities ?? []).filter(activity => isOpenActivityStatus(activity.status)).length : 0;
2804
- }
2805
-
2806
- function pendingTaskCounts(instances) {
2807
- return new Map(instances.map(instance => [ instance._id, ownOpenTasks(instance) ]));
2808
- }
2809
-
2810
- function pendingTasksPhrase(pending) {
2811
- if (pending !== void 0) return pending === 0 ? "All tasks done" : pluralize("pending task", pending, !0);
2812
- }
2813
-
2814
3227
  const CARD_RADIUS = 3;
2815
3228
 
2816
3229
  function BoardCard({card: card, wiring: wiring}) {
2817
- const {onSelect: onSelect, pending: pending, selectedInstanceId: selectedInstanceId, titles: titles} = wiring, {color: color} = useTheme_v2(), selected = card.instanceId === selectedInstanceId;
3230
+ const {facts: facts, onSelect: onSelect, selectedInstanceId: selectedInstanceId, titles: titles} = wiring, {color: color} = useTheme_v2(), selected = card.instanceId === selectedInstanceId, held = facts.get(card.instanceId);
2818
3231
  /* @__PURE__ */
2819
3232
  return jsx(Card, {
2820
3233
  "aria-pressed": selected,
@@ -2832,54 +3245,93 @@ function BoardCard({card: card, wiring: wiring}) {
2832
3245
  children: /* @__PURE__ */ jsxs(Stack, {
2833
3246
  gap: 3,
2834
3247
  children: [
2835
- /* @__PURE__ */ jsx(Text, {
2836
- size: 1,
2837
- weight: "medium",
2838
- children: /* @__PURE__ */ jsx(CardTitleFace, {
2839
- title: cardTitle({
2840
- face: card.face,
2841
- titles: titles
3248
+ /* @__PURE__ */ jsxs(Flex, {
3249
+ align: "flex-start",
3250
+ gap: 2,
3251
+ children: [
3252
+ /* @__PURE__ */ jsx(Text, {
3253
+ size: 1,
3254
+ style: {
3255
+ flex: 1,
3256
+ minWidth: 0
3257
+ },
3258
+ weight: "medium",
3259
+ children: /* @__PURE__ */ jsx(CardTitleFace, {
3260
+ title: cardTitle({
3261
+ face: card.face,
3262
+ titles: titles
3263
+ })
2842
3264
  })
2843
- })
2844
- }),
2845
- /* @__PURE__ */ jsx(PendingTasks, {
2846
- pending: pending.get(card.instanceId)
3265
+ }), held?.stuck === !0 ? /* @__PURE__ */ jsx(AlertGlyph, {
3266
+ Icon: ErrorOutlineIcon,
3267
+ hint: STUCK_RUN_HINT,
3268
+ tone: "critical"
3269
+ }) : null ]
3270
+ }), held === void 0 ? null : /* @__PURE__ */ jsx(CardMetrics, {
3271
+ facts: held
2847
3272
  }) ]
2848
3273
  })
2849
3274
  });
2850
3275
  }
2851
3276
 
2852
- function PendingTasks({pending: pending}) {
2853
- const badgeTrim = useBadgeCapTrim(), phrase = pendingTasksPhrase(pending);
2854
- return phrase === void 0 ? null : /* @__PURE__ */ jsx(Flex, {
3277
+ function CardMetrics({facts: facts}) {
3278
+ return facts.settled ? null : /* @__PURE__ */ jsxs(Flex, {
2855
3279
  align: "center",
2856
- style: badgeTrim,
2857
- children: /* @__PURE__ */ jsx(Card, {
2858
- padding: 2,
2859
- radius: 2,
2860
- tone: "transparent",
2861
- children: /* @__PURE__ */ jsx(Text, {
2862
- muted: pending === 0,
2863
- size: 1,
2864
- children: phrase
2865
- })
2866
- })
3280
+ gap: 3,
3281
+ children: [
3282
+ /* @__PURE__ */ jsx(MetricPair, {
3283
+ Icon: EmptyIcon,
3284
+ empty: facts.pending === 0,
3285
+ face: {
3286
+ text: pendingHint(facts.pending)
3287
+ },
3288
+ value: String(facts.pending)
3289
+ }), facts.overdue === 0 ? null : /* @__PURE__ */ jsx(MetricPair, {
3290
+ Icon: CalendarIcon,
3291
+ face: {
3292
+ text: overdueTasksHint(facts.overdue)
3293
+ },
3294
+ tone: "caution",
3295
+ value: String(facts.overdue)
3296
+ }) ]
2867
3297
  });
2868
3298
  }
2869
3299
 
2870
- function StageGroupHeading({group: group}) {
3300
+ function StageGroupHeading({facts: facts, group: group}) {
3301
+ const totals = laneTotals(group.cards, facts);
2871
3302
  /* @__PURE__ */
2872
3303
  return jsxs(Flex, {
2873
3304
  align: "center",
2874
3305
  gap: 2,
2875
3306
  children: [
2876
- /* @__PURE__ */ jsx(CountedLabel, {
3307
+ /* @__PURE__ */ jsx(CountedHeading, {
2877
3308
  count: group.cards.length,
2878
- label: /* @__PURE__ */ jsx(Text, {
2879
- size: 1,
2880
- weight: "semibold",
2881
- children: group.title
2882
- })
3309
+ title: group.title
3310
+ }), totals.overdue === 0 && totals.stuck === 0 ? null : /* @__PURE__ */ jsxs(Fragment, {
3311
+ children: [
3312
+ /* @__PURE__ */ jsx(Flex, {
3313
+ flex: 1
3314
+ }),
3315
+ /* @__PURE__ */ jsxs(Flex, {
3316
+ align: "center",
3317
+ flex: "none",
3318
+ gap: 3,
3319
+ children: [ totals.overdue === 0 ? null : /* @__PURE__ */ jsx(MetricPair, {
3320
+ Icon: CalendarIcon,
3321
+ face: {
3322
+ text: laneOverdueHint(totals.overdue)
3323
+ },
3324
+ tone: "caution",
3325
+ value: String(totals.overdue)
3326
+ }), totals.stuck === 0 ? null : /* @__PURE__ */ jsx(MetricPair, {
3327
+ Icon: ErrorOutlineIcon,
3328
+ face: {
3329
+ text: laneStuckHint(totals.stuck)
3330
+ },
3331
+ tone: "critical",
3332
+ value: String(totals.stuck)
3333
+ }) ]
3334
+ }) ]
2883
3335
  }), group.retired ?
2884
3336
  /* @__PURE__ */ jsx(Text, {
2885
3337
  muted: !0,
@@ -2928,6 +3380,7 @@ function StageColumn({group: group, leadingRule: leadingRule, trailingRule: trai
2928
3380
  paddingTop: 4,
2929
3381
  paddingX: LANE_PADDING,
2930
3382
  children: /* @__PURE__ */ jsx(StageGroupHeading, {
3383
+ facts: wiring.facts,
2931
3384
  group: group
2932
3385
  })
2933
3386
  }), group.cards.length === 0 ? /* @__PURE__ */ jsx(Flex, {
@@ -3068,7 +3521,7 @@ function useBoard(args) {
3068
3521
  }), {titles: titles, ready: ready} = seeded, board = useMemo(() => derived ? orderBoardByPreviewTitle(derived, titles) : void 0, [ derived, titles ]);
3069
3522
  return useMemo(() => ({
3070
3523
  board: board,
3071
- ready: ready,
3524
+ ready: board !== void 0 && ready,
3072
3525
  titles: seeded
3073
3526
  }), [ board, ready, seeded ]);
3074
3527
  }
@@ -3085,6 +3538,16 @@ function useBoardInstances(definitionName) {
3085
3538
  return useCappedInstances(filter);
3086
3539
  }
3087
3540
 
3541
+ function activeCountFace(args) {
3542
+ return args.error !== void 0 || args.loading ? {
3543
+ count: void 0,
3544
+ note: void 0
3545
+ } : {
3546
+ count: args.count,
3547
+ note: args.truncated ? INSTANCE_CAP_COUNT_NOTE : void 0
3548
+ };
3549
+ }
3550
+
3088
3551
  function DefinitionPanel({definition: definition}) {
3089
3552
  /* @__PURE__ */
3090
3553
  return jsx(Box, {
@@ -3147,12 +3610,31 @@ function SetupIssues({bindings: bindings}) {
3147
3610
  });
3148
3611
  }
3149
3612
 
3613
+ function ActiveCount({face: face}) {
3614
+ /* @__PURE__ */
3615
+ return jsxs(Stack, {
3616
+ gap: 2,
3617
+ children: [
3618
+ /* @__PURE__ */ jsx(Text, {
3619
+ muted: face.count === void 0,
3620
+ size: 1,
3621
+ children: face.count ?? "—"
3622
+ }), face.note === void 0 ? null : /* @__PURE__ */ jsx(Text, {
3623
+ muted: !0,
3624
+ size: 1,
3625
+ children: face.note
3626
+ }) ]
3627
+ });
3628
+ }
3629
+
3150
3630
  function DefinitionFacts({definition: definition}) {
3151
- const {mappingIssues: mappingIssues, mappings: mappings} = useWorkflowContext(), schema = useSchema(), instances = useToolInstances(), snapshotWidth = useContainerToken(2), [row] = definitionCatalog({
3631
+ const {mappingIssues: mappingIssues, mappings: mappings} = useWorkflowContext(), schema = useSchema(), instances = useToolInstances(), now = useMinuteClock(), snapshotWidth = useContainerToken(2), [row] = definitionCatalog({
3152
3632
  definitions: [ definition ],
3633
+ identity: void 0,
3153
3634
  mappings: mappings,
3154
- inFlight: instances.inFlight,
3155
- issues: mappingIssues
3635
+ instances: [ ...instances.inFlight, ...instances.settled ],
3636
+ issues: mappingIssues,
3637
+ now: now
3156
3638
  }), runsOn = docTypeLabels(row?.docTypes ?? [], name => schema.get(name)?.title).map(docType => docType.title);
3157
3639
  /* @__PURE__ */
3158
3640
  return jsxs(Flex, {
@@ -3164,6 +3646,12 @@ function DefinitionFacts({definition: definition}) {
3164
3646
  children: /* @__PURE__ */ jsx(SetupIssues, {
3165
3647
  bindings: row.brokenBindings
3166
3648
  })
3649
+ }), instances.error === void 0 ? null : /* @__PURE__ */ jsx(Box, {
3650
+ flex: "none",
3651
+ children: /* @__PURE__ */ jsx(NoteBanner, {
3652
+ label: INSTANCES_READ_FAILED,
3653
+ tone: "critical"
3654
+ })
3167
3655
  }),
3168
3656
  /* @__PURE__ */ jsx(Box, {
3169
3657
  flex: "none",
@@ -3182,21 +3670,13 @@ function DefinitionFacts({definition: definition}) {
3182
3670
  }),
3183
3671
  /* @__PURE__ */ jsx(MetaRow, {
3184
3672
  label: "Active instances",
3185
- children: instances.loading ? /* @__PURE__ */ jsx(Text, {
3186
- muted: !0,
3187
- size: 1,
3188
- children: "—"
3189
- }) : /* @__PURE__ */ jsxs(Stack, {
3190
- gap: 2,
3191
- children: [
3192
- /* @__PURE__ */ jsx(Text, {
3193
- size: 1,
3194
- children: row?.activeCount ?? 0
3195
- }), instances.truncated ? /* @__PURE__ */ jsx(Text, {
3196
- muted: !0,
3197
- size: 1,
3198
- children: INSTANCE_CAP_COUNT_NOTE
3199
- }) : null ]
3673
+ children: /* @__PURE__ */ jsx(ActiveCount, {
3674
+ face: activeCountFace({
3675
+ count: row?.signals.activeCount ?? 0,
3676
+ error: instances.error,
3677
+ loading: instances.loading,
3678
+ truncated: instances.truncated
3679
+ })
3200
3680
  })
3201
3681
  }),
3202
3682
  /* @__PURE__ */ jsx(MetaRow, {
@@ -3260,7 +3740,7 @@ function pickerLabel(face) {
3260
3740
  }
3261
3741
 
3262
3742
  function WorkflowPicker({face: face, onSelect: onSelect, options: options}) {
3263
- const empty = options.length === 0;
3743
+ const empty = options.length === 0, reclaimed = -useSpaceToken(TOOL_HEADING_PADDING);
3264
3744
  /* @__PURE__ */
3265
3745
  return jsx(MenuButton, {
3266
3746
  button: /* @__PURE__ */ jsx(Button, {
@@ -3270,6 +3750,9 @@ function WorkflowPicker({face: face, onSelect: onSelect, options: options}) {
3270
3750
  iconRight: ChevronDownIcon,
3271
3751
  mode: "bleed",
3272
3752
  padding: TOOL_HEADING_PADDING,
3753
+ style: {
3754
+ margin: reclaimed
3755
+ },
3273
3756
  text: pickerLabel(face),
3274
3757
  textWeight: "semibold"
3275
3758
  }),
@@ -3300,6 +3783,7 @@ function WorkflowPageHeader({face: face, onSelectPage: onSelectPage, onSelectWor
3300
3783
  children: [
3301
3784
  /* @__PURE__ */ jsx(Flex, {
3302
3785
  align: "center",
3786
+ padding: TOOL_HEADING_PADDING,
3303
3787
  children: /* @__PURE__ */ jsx(WorkflowPicker, {
3304
3788
  face: face,
3305
3789
  onSelect: onSelectWorkflow,
@@ -3733,30 +4217,38 @@ function usePageSelection(page) {
3733
4217
  }
3734
4218
 
3735
4219
  function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPage, page: page, workflowName: workflowName}) {
3736
- const {definitions: definitions, error: error} = useDeployedDefinitions(), selection = resolveWorkflowSelection({
4220
+ const {definitions: definitions, error: catalogError} = useDeployedDefinitions(), selection = resolveWorkflowSelection({
3737
4221
  definitions: definitions,
3738
4222
  routeName: workflowName
3739
4223
  }), selected = resolvedDefinition(selection), view = workflowPageView({
3740
- error: error,
4224
+ error: catalogError,
3741
4225
  page: page,
3742
4226
  selection: selection
3743
4227
  }), instances = useBoardInstances(view.streamName), {board: board, ready: ready, titles: titles} = useBoard({
3744
4228
  definition: selected,
3745
4229
  instances: instances.rows
3746
- }), {select: select, selectedInstanceId: selectedInstanceId} = usePageSelection(page), panel = usePanelPresence(panelSubject({
4230
+ }), {select: select, selectedInstanceId: selectedInstanceId} = usePageSelection(page), now = useMinuteClock(), panel = usePanelPresence(panelSubject({
3747
4231
  board: board,
3748
4232
  selectedInstanceId: selectedInstanceId
3749
4233
  })), revealed = useRevealGate({
3750
4234
  loading: instances.loading,
4235
+ progress: titles.resolved.size,
3751
4236
  ready: ready
3752
- }), waiting = useDelayedFlag(selection.kind === "loading" && error === void 0, WAIT_CUE_DELAY_MS);
4237
+ });
3753
4238
  if (selection.kind === "loading") /* @__PURE__ */ return jsx(UnresolvedPage, {
3754
- error: error,
3755
- waiting: waiting
4239
+ error: catalogError
4240
+ });
4241
+ const catalogEmpty = (definitions ?? []).length === 0;
4242
+ if (selection.kind === "unknown" && catalogEmpty) /* @__PURE__ */
4243
+ return jsx(EmptyCatalogPage, {
4244
+ name: selection.name
3756
4245
  });
3757
4246
  const wiring = {
3758
4247
  onSelect: select,
3759
- pending: pendingTaskCounts(instances.rows),
4248
+ facts: cardFacts({
4249
+ instances: instances.rows,
4250
+ now: now
4251
+ }),
3760
4252
  selectedInstanceId: selectedInstanceId,
3761
4253
  titles: titles
3762
4254
  };
@@ -3809,8 +4301,9 @@ function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPag
3809
4301
  }),
3810
4302
  /* @__PURE__ */ jsx(PageBody, {
3811
4303
  board: board,
3812
- catalogEmpty: (definitions ?? []).length === 0,
3813
- error: error,
4304
+ catalogEmpty: catalogEmpty,
4305
+ catalogError: catalogError,
4306
+ instancesError: instances.error,
3814
4307
  loading: !revealed,
3815
4308
  page: page,
3816
4309
  selection: selection,
@@ -3838,45 +4331,66 @@ function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPag
3838
4331
  });
3839
4332
  }
3840
4333
 
3841
- const WAIT_CUE_DELAY_MS = 300;
3842
-
3843
- function UnresolvedPage({error: error, waiting: waiting}) {
4334
+ function UnresolvedPage({error: error}) {
3844
4335
  /* @__PURE__ */
3845
4336
  return jsx(Flex, {
3846
4337
  direction: "column",
3847
4338
  height: "fill",
3848
4339
  overflow: "hidden",
3849
- children: error === void 0 ? /* @__PURE__ */ jsx(WaitingFace, {
3850
- waiting: waiting
3851
- }) : /* @__PURE__ */ jsx(CatalogErrorFace, {
3852
- error: error
4340
+ children: error === void 0 ? /* @__PURE__ */ jsx(WaitingFace, {}) : /* @__PURE__ */ jsx(ReadErrorFace, {
4341
+ text: CATALOG_READ_FAILED
3853
4342
  })
3854
4343
  });
3855
4344
  }
3856
4345
 
3857
- function WaitingFace({waiting: waiting}) {
4346
+ function WaitingFace() {
3858
4347
  /* @__PURE__ */
3859
4348
  return jsx(StatusBox, {
3860
- children: waiting ? /* @__PURE__ */ jsx(TabStatusRow, {
4349
+ children: /* @__PURE__ */ jsx(TabStatusRow, {
3861
4350
  children: /* @__PURE__ */ jsx(LoadingRow, {
3862
4351
  label: "Loading workflow…"
3863
4352
  })
3864
- }) : null
4353
+ })
4354
+ });
4355
+ }
4356
+
4357
+ function EmptyCatalogPage({name: name}) {
4358
+ const face = unknownWorkflowFace({
4359
+ catalogEmpty: !0,
4360
+ name: name
4361
+ });
4362
+ /* @__PURE__ */
4363
+ return jsx(Flex, {
4364
+ direction: "column",
4365
+ height: "fill",
4366
+ overflow: "hidden",
4367
+ padding: LANE_PADDING,
4368
+ children: /* @__PURE__ */ jsx(EmptyState, {
4369
+ description: face.description,
4370
+ icon: /* @__PURE__ */ jsx(SearchIcon, {}),
4371
+ title: face.title
4372
+ })
3865
4373
  });
3866
4374
  }
3867
4375
 
3868
- function CatalogErrorFace({error: error}) {
4376
+ const BANNER_LANE_INSET = 2;
4377
+
4378
+ function ReadErrorFace({text: text}) {
3869
4379
  /* @__PURE__ */
3870
4380
  return jsx(StatusBox, {
3871
- children: /* @__PURE__ */ jsx(MutedNote, {
3872
- text: catalogReadError(error)
4381
+ children: /* @__PURE__ */ jsx(Box, {
4382
+ paddingTop: BANNER_LANE_INSET,
4383
+ children: /* @__PURE__ */ jsx(NoteBanner, {
4384
+ label: text,
4385
+ tone: "critical"
4386
+ })
3873
4387
  })
3874
4388
  });
3875
4389
  }
3876
4390
 
3877
- function PageBody({board: board, catalogEmpty: catalogEmpty, error: error, loading: loading, page: page, selection: selection, wiring: wiring}) {
3878
- if (error !== void 0) /* @__PURE__ */ return jsx(CatalogErrorFace, {
3879
- error: error
4391
+ function PageBody({board: board, catalogEmpty: catalogEmpty, catalogError: catalogError, instancesError: instancesError, loading: loading, page: page, selection: selection, wiring: wiring}) {
4392
+ if (catalogError !== void 0) /* @__PURE__ */ return jsx(ReadErrorFace, {
4393
+ text: CATALOG_READ_FAILED
3880
4394
  });
3881
4395
  if (selection.kind === "unknown") {
3882
4396
  const face = unknownWorkflowFace({
@@ -3894,6 +4408,8 @@ function PageBody({board: board, catalogEmpty: catalogEmpty, error: error, loadi
3894
4408
  }
3895
4409
  return page === "definition" ? /* @__PURE__ */ jsx(DefinitionPanel, {
3896
4410
  definition: selection.definition
4411
+ }) : instancesError !== void 0 ? /* @__PURE__ */ jsx(ReadErrorFace, {
4412
+ text: INSTANCES_READ_FAILED
3897
4413
  }) : loading || board === void 0 ? /* @__PURE__ */ jsx(StatusBox, {
3898
4414
  children: /* @__PURE__ */ jsx(Box, {
3899
4415
  paddingTop: LANE_PADDING,
@@ -4058,10 +4574,12 @@ function WorkflowsHome({filterSlot: filterSlot, onOpenInstance: onOpenInstance,
4058
4574
 
4059
4575
  function HomePanel({filterSlot: filterSlot, identity: identity, instances: instances, onOpenInstance: onOpenInstance, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, tab: tab}) {
4060
4576
  return tab === "overview" ? /* @__PURE__ */ jsx(OverviewTab, {
4577
+ identity: identity,
4061
4578
  onOpenWorkflow: onOpenWorkflow
4062
4579
  }) : /* @__PURE__ */ jsx(TasksTab, {
4580
+ error: instances.error,
4063
4581
  filterSlot: filterSlot,
4064
- identity: identity,
4582
+ identity: identityOf(identity),
4065
4583
  instances: [ ...instances.inFlight, ...instances.settled ],
4066
4584
  loading: instances.loading,
4067
4585
  onOpenInstance: onOpenInstance,