@agent-native/core 0.141.2 → 0.141.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
  3. package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
  4. package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
  5. package/corpus/templates/analytics/changelog/2026-08-05-durable-a2a-workers-avoid-startup-migration-contention.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
  7. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
  8. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  9. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  10. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
  12. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
  13. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
  14. package/corpus/templates/analytics/server/lib/gong.ts +132 -33
  15. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  16. package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
  17. package/corpus/templates/analytics/server/plugins/db.ts +35 -10
  18. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  19. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
  20. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  21. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  22. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  23. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  24. package/corpus/templates/clips/actions/invite-member.ts +39 -16
  25. package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
  26. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  27. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
  28. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  29. package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
  31. package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
  32. package/corpus/templates/clips/desktop/src/styles.css +0 -3
  33. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  34. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  35. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  36. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
  37. package/corpus/templates/clips/shared/email-preview.ts +61 -0
  38. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  39. package/corpus/templates/content/server/lib/emails.ts +70 -0
  40. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  41. package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
  42. package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
  43. package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
  44. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  45. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  46. package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
  47. package/corpus/templates/factory/AGENTS.md +18 -17
  48. package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
  49. package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
  50. package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
  51. package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
  52. package/corpus/templates/factory/app/routes/factory.tsx +376 -206
  53. package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
  54. package/corpus/templates/factory/server/db/schema.ts +6 -0
  55. package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
  56. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
  57. package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
  58. package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
  59. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  60. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  61. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  62. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  63. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  64. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  65. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  66. package/corpus/templates/slides/actions/import-from-url.ts +1 -0
  67. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
  68. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
  69. package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
  70. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
  71. package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
  72. package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
  73. package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
  74. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
  75. package/corpus/templates/slides/app/pages/Index.tsx +127 -137
  76. package/corpus/templates/slides/app/root.tsx +8 -0
  77. package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
  79. package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
  80. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  81. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  82. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  83. package/dist/brand-kit/index.d.ts +1 -0
  84. package/dist/brand-kit/index.js +1 -0
  85. package/dist/brand-kit/tokens.d.ts +91 -0
  86. package/dist/brand-kit/tokens.js +279 -0
  87. package/dist/brand-kit/types.d.ts +25 -0
  88. package/dist/client/AssistantChat.js +7 -7
  89. package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
  90. package/dist/client/onboarding/first-run-registry.d.ts +25 -0
  91. package/dist/client/onboarding/first-run-registry.js +18 -0
  92. package/dist/client/onboarding/index.d.ts +2 -0
  93. package/dist/client/onboarding/index.js +1 -0
  94. package/dist/collab/awareness.d.ts +2 -2
  95. package/dist/collab/routes.d.ts +1 -1
  96. package/dist/collab/struct-routes.d.ts +1 -1
  97. package/dist/db/index.d.ts +1 -1
  98. package/dist/db/index.js +1 -1
  99. package/dist/db/migrations.d.ts +13 -3
  100. package/dist/db/migrations.js +19 -4
  101. package/dist/deploy/build.d.ts +8 -0
  102. package/dist/deploy/build.js +92 -3
  103. package/dist/deploy/workspace-deploy.js +86 -3
  104. package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
  105. package/dist/email-catalog/actions/list-email-activity.js +19 -0
  106. package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
  107. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  108. package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
  109. package/dist/email-catalog/actions/list-email-log.js +23 -0
  110. package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
  111. package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
  112. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
  113. package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
  114. package/dist/email-catalog/authorize.d.ts +1 -0
  115. package/dist/email-catalog/authorize.js +9 -0
  116. package/dist/email-catalog/log.d.ts +60 -0
  117. package/dist/email-catalog/log.js +142 -0
  118. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  119. package/dist/email-catalog/provider-metrics.js +182 -0
  120. package/dist/email-catalog/registry.d.ts +78 -0
  121. package/dist/email-catalog/registry.js +70 -0
  122. package/dist/email-catalog/schema.d.ts +231 -0
  123. package/dist/email-catalog/schema.js +53 -0
  124. package/dist/email-catalog/system-emails.d.ts +13 -0
  125. package/dist/email-catalog/system-emails.js +67 -0
  126. package/dist/jobs/index.d.ts +2 -0
  127. package/dist/jobs/index.js +2 -0
  128. package/dist/jobs/run-history.js +48 -1
  129. package/dist/jobs/scheduler-dispatch.d.ts +7 -0
  130. package/dist/jobs/scheduler-dispatch.js +7 -0
  131. package/dist/jobs/scheduler-health.d.ts +22 -0
  132. package/dist/jobs/scheduler-health.js +172 -0
  133. package/dist/jobs/scheduler.js +67 -2
  134. package/dist/localization/default-messages.d.ts +45 -0
  135. package/dist/localization/default-messages.js +45 -0
  136. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  137. package/dist/notifications/routes.d.ts +3 -3
  138. package/dist/observability/routes.d.ts +3 -3
  139. package/dist/org/handlers.js +9 -1
  140. package/dist/progress/routes.d.ts +1 -1
  141. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  142. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  143. package/dist/server/action-discovery.js +22 -0
  144. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
  145. package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
  146. package/dist/server/agent-chat-plugin.d.ts +2 -1
  147. package/dist/server/agent-chat-plugin.js +69 -22
  148. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  149. package/dist/server/better-auth-instance.js +17 -2
  150. package/dist/server/design-token-utils.d.ts +6 -1
  151. package/dist/server/design-token-utils.js +252 -59
  152. package/dist/server/email.d.ts +11 -0
  153. package/dist/server/email.js +54 -3
  154. package/dist/server/index.d.ts +3 -2
  155. package/dist/server/index.js +3 -2
  156. package/dist/vite/client.js +20 -14
  157. package/package.json +2 -1
@@ -102,6 +102,12 @@ type TriageConfig = {
102
102
  sentryProjectSlug?: string | null;
103
103
  sentryEnvironment?: string | null;
104
104
  repository?: string | null;
105
+ automationFailureAlertsEnabled?: boolean;
106
+ automationFailureAlertEmail?: string | null;
107
+ emailReadiness?: {
108
+ status: "ready" | "not-configured" | "misconfigured" | "unavailable";
109
+ provider: string;
110
+ };
105
111
  };
106
112
 
107
113
  type Verdict = "correct" | "incorrect" | "uncertain";
@@ -113,6 +119,16 @@ type FactoryAutomationRun = {
113
119
  startedAt?: string | number | null;
114
120
  finishedAt?: string | number | null;
115
121
  error?: string | null;
122
+ runId?: string | null;
123
+ threadId?: string | null;
124
+ };
125
+
126
+ type FactoryAutomationHealth = {
127
+ status: "healthy" | "stale" | "error" | "no-data";
128
+ lastCheckedAt?: number | null;
129
+ lastDispatchedAt?: number | null;
130
+ lastError?: string | null;
131
+ runtime?: string | null;
116
132
  };
117
133
 
118
134
  type FactoryAutomation = {
@@ -570,6 +586,11 @@ function AutomationsView({
570
586
  "list-factory-automations",
571
587
  { factoryId },
572
588
  );
589
+ const healthQuery = useActionQuery<FactoryAutomationHealth>(
590
+ "get-factory-automation-health",
591
+ {},
592
+ { refetchInterval: 60_000 },
593
+ );
573
594
  const saveMutation = useActionMutation("save-factory-automation");
574
595
  const runMutation = useActionMutation("run-factory-automation");
575
596
  const {
@@ -645,245 +666,328 @@ function AutomationsView({
645
666
  schedule: draft.schedule ?? "",
646
667
  enabled: draft.enabled,
647
668
  });
648
- await automationsQuery.refetch();
669
+ await Promise.all([automationsQuery.refetch(), healthQuery.refetch()]);
649
670
  }
650
671
 
651
672
  async function runAutomation() {
652
673
  if (!draft) return;
653
674
  await runMutation.mutateAsync({ factoryId, automationId: draft.id });
654
- await automationsQuery.refetch();
675
+ await Promise.all([automationsQuery.refetch(), healthQuery.refetch()]);
655
676
  }
656
677
 
678
+ const health = healthQuery.data;
679
+ const healthLabel = health
680
+ ? {
681
+ healthy: t("factoryRoute.automationHealthHealthy"),
682
+ stale: t("factoryRoute.automationHealthStale"),
683
+ error: t("factoryRoute.automationHealthError"),
684
+ "no-data": t("factoryRoute.automationHealthNoData"),
685
+ }[health.status]
686
+ : t("factoryRoute.automationHealthNoData");
687
+
657
688
  return (
658
- <div className="grid gap-4 p-4 lg:grid-cols-[minmax(220px,.35fr)_minmax(0,1fr)] lg:p-6">
689
+ <div className="space-y-4 p-4 lg:p-6">
659
690
  <Card>
660
- <CardHeader>
691
+ <CardHeader className="pb-3">
661
692
  <CardTitle className="text-base">
662
- {t("factoryRoute.automationsTitle")}
693
+ {t("factoryRoute.automationHealthTitle")}
663
694
  </CardTitle>
664
695
  <p className="text-sm text-muted-foreground">
665
- {t("factoryRoute.automationsDescription")}
696
+ {t("factoryRoute.automationHealthDescription")}
666
697
  </p>
667
698
  </CardHeader>
668
- <CardContent className="p-0">
669
- {automationsQuery.isLoading ? (
670
- <p className="p-4 text-sm text-muted-foreground">
671
- {t("factoryRoute.automationsLoading")}
699
+ <CardContent className="space-y-2 pt-0 text-sm">
700
+ <div className="flex flex-wrap items-center gap-2">
701
+ <span className="rounded-full border px-2 py-1 font-medium">
702
+ {healthLabel}
703
+ </span>
704
+ {health?.lastCheckedAt && (
705
+ <span className="text-muted-foreground">
706
+ {t("factoryRoute.automationLastCheck")}:{" "}
707
+ {formatAutomationDate(health.lastCheckedAt)}
708
+ </span>
709
+ )}
710
+ {health?.lastDispatchedAt && (
711
+ <span className="text-muted-foreground">
712
+ {t("factoryRoute.automationLastDispatch")}:{" "}
713
+ {formatAutomationDate(health.lastDispatchedAt)}
714
+ </span>
715
+ )}
716
+ {health?.runtime && (
717
+ <span className="text-muted-foreground">
718
+ {t("factoryRoute.automationRuntime")}: {health.runtime}
719
+ </span>
720
+ )}
721
+ </div>
722
+ {!health?.lastCheckedAt && (
723
+ <p className="text-muted-foreground">
724
+ {t("factoryRoute.automationHealthNoDataHint")}
672
725
  </p>
673
- ) : automations.length === 0 ? (
674
- <p className="p-4 text-sm text-muted-foreground">
675
- {t("factoryRoute.automationsEmpty")}
726
+ )}
727
+ {health?.status === "stale" && (
728
+ <p className="text-destructive">
729
+ {t("factoryRoute.automationHealthStaleHint")}
730
+ </p>
731
+ )}
732
+ {health?.lastError && (
733
+ <p className="text-destructive">
734
+ {t("factoryRoute.automationHealthErrorDetail")}:{" "}
735
+ {health.lastError}
736
+ </p>
737
+ )}
738
+ {healthQuery.isError && (
739
+ <p className="text-destructive">
740
+ {t("factoryRoute.automationDiagnosticsLoadError")}{" "}
741
+ {healthQuery.error instanceof Error
742
+ ? healthQuery.error.message
743
+ : String(healthQuery.error)}
676
744
  </p>
677
- ) : (
678
- <div
679
- className="divide-y"
680
- role="tablist"
681
- aria-label={t("factoryRoute.automationsTitle")}
682
- >
683
- {automations.map((automation) => (
684
- <button
685
- key={automation.id}
686
- type="button"
687
- id={`factory-automation-tab-${automation.id}`}
688
- role="tab"
689
- aria-selected={activeAutomationId === automation.id}
690
- aria-controls="factory-automation-panel"
691
- className={`w-full cursor-pointer p-4 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${activeAutomationId === automation.id ? "bg-muted/60" : ""}`}
692
- onClick={() => selectAutomation(automation.id)}
693
- >
694
- <span className="block truncate text-sm font-medium">
695
- {automation.name}
696
- </span>
697
- <span className="mt-1 block text-xs text-muted-foreground">
698
- {automation.enabled
699
- ? t("factoryRoute.automationEnabled")
700
- : t("factoryRoute.automationDisabled")}
701
- </span>
702
- </button>
703
- ))}
704
- </div>
705
745
  )}
706
746
  </CardContent>
707
747
  </Card>
708
-
709
- <Card
710
- id="factory-automation-panel"
711
- role="tabpanel"
712
- aria-labelledby={
713
- activeAutomationId
714
- ? `factory-automation-tab-${activeAutomationId}`
715
- : undefined
716
- }
717
- >
718
- <CardHeader className="border-b sm:flex-row sm:items-center sm:justify-between">
719
- <div>
748
+ <div className="grid gap-4 lg:grid-cols-[minmax(220px,.35fr)_minmax(0,1fr)]">
749
+ <Card>
750
+ <CardHeader>
720
751
  <CardTitle className="text-base">
721
- {t("factoryRoute.automationEditorTitle")}
752
+ {t("factoryRoute.automationsTitle")}
722
753
  </CardTitle>
723
- <p className="mt-1 text-sm text-muted-foreground">
724
- {t("factoryRoute.automationEditorDescription")}
725
- </p>
726
- </div>
727
- {draft && (
728
- <div className="flex gap-2">
729
- <Button
730
- type="button"
731
- variant="outline"
732
- size="sm"
733
- onClick={() => void runAutomation()}
734
- disabled={runMutation.isPending || draft.canUpdate === false}
735
- >
736
- {runMutation.isPending && (
737
- <IconLoader2 className="animate-spin" />
738
- )}
739
- <IconPlayerPlay className="size-4" />
740
- {t("factoryRoute.runNow")}
741
- </Button>
742
- <Button
743
- type="button"
744
- size="sm"
745
- onClick={() => void saveAutomation()}
746
- disabled={saveMutation.isPending || draft.canUpdate === false}
747
- >
748
- {saveMutation.isPending && (
749
- <IconLoader2 className="animate-spin" />
750
- )}
751
- {t("factoryRoute.saveAutomation")}
752
- </Button>
753
- </div>
754
- )}
755
- </CardHeader>
756
- <CardContent className="space-y-5 pt-5">
757
- {!draft ? (
758
754
  <p className="text-sm text-muted-foreground">
759
- {t("factoryRoute.selectAutomation")}
755
+ {t("factoryRoute.automationsDescription")}
760
756
  </p>
761
- ) : (
762
- <>
763
- <div className="grid gap-4 md:grid-cols-2">
764
- <div className="grid gap-1.5">
765
- <Label htmlFor="factory-automation-model">
766
- {t("factoryRoute.automationModel")}
767
- </Label>
768
- <select
769
- id="factory-automation-model"
770
- value={draft.model ?? ""}
771
- onChange={(event) =>
772
- setDraft({ ...draft, model: event.target.value })
773
- }
774
- disabled={modelsLoading && modelOptions.length === 0}
775
- className="h-9 rounded-md border bg-background px-3 text-sm"
757
+ </CardHeader>
758
+ <CardContent className="p-0">
759
+ {automationsQuery.isLoading ? (
760
+ <p className="p-4 text-sm text-muted-foreground">
761
+ {t("factoryRoute.automationsLoading")}
762
+ </p>
763
+ ) : automations.length === 0 ? (
764
+ <p className="p-4 text-sm text-muted-foreground">
765
+ {t("factoryRoute.automationsEmpty")}
766
+ </p>
767
+ ) : (
768
+ <div
769
+ className="divide-y"
770
+ role="tablist"
771
+ aria-label={t("factoryRoute.automationsTitle")}
772
+ >
773
+ {automations.map((automation) => (
774
+ <button
775
+ key={automation.id}
776
+ type="button"
777
+ id={`factory-automation-tab-${automation.id}`}
778
+ role="tab"
779
+ aria-selected={activeAutomationId === automation.id}
780
+ aria-controls="factory-automation-panel"
781
+ className={`w-full cursor-pointer p-4 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${activeAutomationId === automation.id ? "bg-muted/60" : ""}`}
782
+ onClick={() => selectAutomation(automation.id)}
776
783
  >
777
- <option value="auto">{autoModelLabel}</option>
778
- {draft.model &&
779
- draft.model !== "auto" &&
780
- !modelOptions.some(
781
- (option) => option.value === draft.model,
782
- ) && (
783
- <option value={draft.model}>
784
- Configured / {formatModelName(draft.model)}
784
+ <span className="block truncate text-sm font-medium">
785
+ {automation.name}
786
+ </span>
787
+ <span className="mt-1 block text-xs text-muted-foreground">
788
+ {automation.enabled
789
+ ? t("factoryRoute.automationEnabled")
790
+ : t("factoryRoute.automationDisabled")}
791
+ </span>
792
+ </button>
793
+ ))}
794
+ </div>
795
+ )}
796
+ </CardContent>
797
+ </Card>
798
+
799
+ <Card
800
+ id="factory-automation-panel"
801
+ role="tabpanel"
802
+ aria-labelledby={
803
+ activeAutomationId
804
+ ? `factory-automation-tab-${activeAutomationId}`
805
+ : undefined
806
+ }
807
+ >
808
+ <CardHeader className="border-b sm:flex-row sm:items-center sm:justify-between">
809
+ <div>
810
+ <CardTitle className="text-base">
811
+ {t("factoryRoute.automationEditorTitle")}
812
+ </CardTitle>
813
+ <p className="mt-1 text-sm text-muted-foreground">
814
+ {t("factoryRoute.automationEditorDescription")}
815
+ </p>
816
+ </div>
817
+ {draft && (
818
+ <div className="flex gap-2">
819
+ <Button
820
+ type="button"
821
+ variant="outline"
822
+ size="sm"
823
+ onClick={() => void runAutomation()}
824
+ disabled={runMutation.isPending || draft.canUpdate === false}
825
+ >
826
+ {runMutation.isPending && (
827
+ <IconLoader2 className="animate-spin" />
828
+ )}
829
+ <IconPlayerPlay className="size-4" />
830
+ {t("factoryRoute.runNow")}
831
+ </Button>
832
+ <Button
833
+ type="button"
834
+ size="sm"
835
+ onClick={() => void saveAutomation()}
836
+ disabled={saveMutation.isPending || draft.canUpdate === false}
837
+ >
838
+ {saveMutation.isPending && (
839
+ <IconLoader2 className="animate-spin" />
840
+ )}
841
+ {t("factoryRoute.saveAutomation")}
842
+ </Button>
843
+ </div>
844
+ )}
845
+ </CardHeader>
846
+ <CardContent className="space-y-5 pt-5">
847
+ {!draft ? (
848
+ <p className="text-sm text-muted-foreground">
849
+ {t("factoryRoute.selectAutomation")}
850
+ </p>
851
+ ) : (
852
+ <>
853
+ <div className="grid gap-4 md:grid-cols-2">
854
+ <div className="grid gap-1.5">
855
+ <Label htmlFor="factory-automation-model">
856
+ {t("factoryRoute.automationModel")}
857
+ </Label>
858
+ <select
859
+ id="factory-automation-model"
860
+ value={draft.model ?? ""}
861
+ onChange={(event) =>
862
+ setDraft({ ...draft, model: event.target.value })
863
+ }
864
+ disabled={modelsLoading && modelOptions.length === 0}
865
+ className="h-9 rounded-md border bg-background px-3 text-sm"
866
+ >
867
+ <option value="auto">{autoModelLabel}</option>
868
+ {draft.model &&
869
+ draft.model !== "auto" &&
870
+ !modelOptions.some(
871
+ (option) => option.value === draft.model,
872
+ ) && (
873
+ <option value={draft.model}>
874
+ Configured / {formatModelName(draft.model)}
875
+ </option>
876
+ )}
877
+ {modelOptions.map((option) => (
878
+ <option key={option.value} value={option.value}>
879
+ {option.label}
785
880
  </option>
881
+ ))}
882
+ </select>
883
+ </div>
884
+ <div className="grid gap-1.5">
885
+ <Label htmlFor="factory-automation-schedule">
886
+ {t("factoryRoute.automationSchedule")}
887
+ </Label>
888
+ <Input
889
+ id="factory-automation-schedule"
890
+ value={draft.schedule ?? ""}
891
+ onChange={(event) =>
892
+ setDraft({ ...draft, schedule: event.target.value })
893
+ }
894
+ placeholder={t(
895
+ "factoryRoute.automationSchedulePlaceholder",
786
896
  )}
787
- {modelOptions.map((option) => (
788
- <option key={option.value} value={option.value}>
789
- {option.label}
790
- </option>
791
- ))}
792
- </select>
897
+ />
898
+ </div>
899
+ </div>
900
+ <label className="flex items-center gap-2 text-sm">
901
+ <Checkbox
902
+ checked={draft.enabled}
903
+ onCheckedChange={(checked) =>
904
+ setDraft({ ...draft, enabled: checked === true })
905
+ }
906
+ disabled={draft.canUpdate === false}
907
+ />
908
+ {t("factoryRoute.automationEnabledLabel")}
909
+ </label>
910
+ <div className="grid gap-2 rounded-md bg-muted/60 p-3 text-sm md:grid-cols-3">
911
+ <div>
912
+ <span className="block text-xs text-muted-foreground">
913
+ {t("factoryRoute.automationTrigger")}
914
+ </span>
915
+ <span>{draft.triggerType ?? "-"}</span>
916
+ </div>
917
+ <div>
918
+ <span className="block text-xs text-muted-foreground">
919
+ {t("factoryRoute.automationEvent")}
920
+ </span>
921
+ <span>{draft.event ?? "-"}</span>
922
+ </div>
923
+ <div>
924
+ <span className="block text-xs text-muted-foreground">
925
+ {t("factoryRoute.automationTimezone")}
926
+ </span>
927
+ <span>{draft.timezone ?? "-"}</span>
928
+ </div>
793
929
  </div>
794
930
  <div className="grid gap-1.5">
795
- <Label htmlFor="factory-automation-schedule">
796
- {t("factoryRoute.automationSchedule")}
797
- </Label>
798
- <Input
799
- id="factory-automation-schedule"
800
- value={draft.schedule ?? ""}
931
+ <Label>{t("factoryRoute.automationPrompt")}</Label>
932
+ <Textarea
933
+ value={draft.prompt ?? draft.body ?? ""}
801
934
  onChange={(event) =>
802
- setDraft({ ...draft, schedule: event.target.value })
935
+ setDraft({ ...draft, prompt: event.target.value })
803
936
  }
804
- placeholder={t(
805
- "factoryRoute.automationSchedulePlaceholder",
806
- )}
937
+ placeholder={t("factoryRoute.automationPromptPlaceholder")}
938
+ rows={12}
807
939
  />
940
+ <p className="text-right text-xs text-muted-foreground">
941
+ {t("factoryRoute.promptEditorHint")}
942
+ </p>
808
943
  </div>
809
- </div>
810
- <label className="flex items-center gap-2 text-sm">
811
- <Checkbox
812
- checked={draft.enabled}
813
- onCheckedChange={(checked) =>
814
- setDraft({ ...draft, enabled: checked === true })
815
- }
816
- disabled={draft.canUpdate === false}
817
- />
818
- {t("factoryRoute.automationEnabledLabel")}
819
- </label>
820
- <div className="grid gap-2 rounded-md bg-muted/60 p-3 text-sm md:grid-cols-3">
821
- <div>
822
- <span className="block text-xs text-muted-foreground">
823
- {t("factoryRoute.automationTrigger")}
824
- </span>
825
- <span>{draft.triggerType ?? "-"}</span>
826
- </div>
827
- <div>
828
- <span className="block text-xs text-muted-foreground">
829
- {t("factoryRoute.automationEvent")}
830
- </span>
831
- <span>{draft.event ?? "-"}</span>
832
- </div>
833
- <div>
834
- <span className="block text-xs text-muted-foreground">
835
- {t("factoryRoute.automationTimezone")}
836
- </span>
837
- <span>{draft.timezone ?? "-"}</span>
944
+ <div className="border-t pt-5">
945
+ <h3 className="text-sm font-medium">
946
+ {t("factoryRoute.pastRuns")}
947
+ </h3>
948
+ {(draft.runs ?? draft.pastRuns ?? []).length === 0 ? (
949
+ <p className="mt-2 text-sm text-muted-foreground">
950
+ {t("factoryRoute.pastRunsEmpty")}
951
+ </p>
952
+ ) : (
953
+ <div className="mt-3 divide-y rounded-md border">
954
+ {(draft.runs ?? draft.pastRuns ?? []).map(
955
+ (run, index) => (
956
+ <div
957
+ key={run.id ?? `${draft.id}-run-${index}`}
958
+ className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm"
959
+ >
960
+ <span className="font-medium">
961
+ {run.status ?? "-"}
962
+ </span>
963
+ <span className="text-xs text-muted-foreground">
964
+ {formatAutomationDate(run.startedAt)}
965
+ </span>
966
+ {run.threadId && (
967
+ <a
968
+ className="inline-flex items-center gap-1 text-xs text-muted-foreground underline-offset-4 hover:underline"
969
+ href={`/chat/${encodeURIComponent(run.threadId)}`}
970
+ >
971
+ {t("factoryRoute.automationOpenThread")}
972
+ <IconExternalLink className="size-3" />
973
+ </a>
974
+ )}
975
+ {run.error && (
976
+ <span className="basis-full text-xs text-destructive">
977
+ {run.error}
978
+ </span>
979
+ )}
980
+ </div>
981
+ ),
982
+ )}
983
+ </div>
984
+ )}
838
985
  </div>
839
- </div>
840
- <div className="grid gap-1.5">
841
- <Label>{t("factoryRoute.automationPrompt")}</Label>
842
- <Textarea
843
- value={draft.prompt ?? draft.body ?? ""}
844
- onChange={(event) =>
845
- setDraft({ ...draft, prompt: event.target.value })
846
- }
847
- placeholder={t("factoryRoute.automationPromptPlaceholder")}
848
- rows={12}
849
- />
850
- <p className="text-right text-xs text-muted-foreground">
851
- {t("factoryRoute.promptEditorHint")}
852
- </p>
853
- </div>
854
- <div className="border-t pt-5">
855
- <h3 className="text-sm font-medium">
856
- {t("factoryRoute.pastRuns")}
857
- </h3>
858
- {(draft.runs ?? draft.pastRuns ?? []).length === 0 ? (
859
- <p className="mt-2 text-sm text-muted-foreground">
860
- {t("factoryRoute.pastRunsEmpty")}
861
- </p>
862
- ) : (
863
- <div className="mt-3 divide-y rounded-md border">
864
- {(draft.runs ?? draft.pastRuns ?? []).map((run, index) => (
865
- <div
866
- key={run.id ?? `${draft.id}-run-${index}`}
867
- className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm"
868
- >
869
- <span className="font-medium">{run.status ?? "-"}</span>
870
- <span className="text-xs text-muted-foreground">
871
- {formatAutomationDate(run.startedAt)}
872
- </span>
873
- {run.error && (
874
- <span className="basis-full text-xs text-destructive">
875
- {run.error}
876
- </span>
877
- )}
878
- </div>
879
- ))}
880
- </div>
881
- )}
882
- </div>
883
- </>
884
- )}
885
- </CardContent>
886
- </Card>
986
+ </>
987
+ )}
988
+ </CardContent>
989
+ </Card>
990
+ </div>
887
991
  </div>
888
992
  );
889
993
  }
@@ -1280,6 +1384,10 @@ function SettingsView({ t }: { t: ReturnType<typeof useT> }) {
1280
1384
  const [sentryOrgSlug, setSentryOrgSlug] = useState("");
1281
1385
  const [sentryProjectSlug, setSentryProjectSlug] = useState("");
1282
1386
  const [sentryEnvironment, setSentryEnvironment] = useState("");
1387
+ const [automationFailureAlertsEnabled, setAutomationFailureAlertsEnabled] =
1388
+ useState(true);
1389
+ const [automationFailureAlertEmail, setAutomationFailureAlertEmail] =
1390
+ useState("");
1283
1391
  const query = useActionQuery("get-triage-config", {});
1284
1392
  const mutation = useActionMutation("save-triage-config");
1285
1393
  useEffect(() => {
@@ -1295,6 +1403,10 @@ function SettingsView({ t }: { t: ReturnType<typeof useT> }) {
1295
1403
  setSentryOrgSlug(data.sentryOrgSlug ?? "");
1296
1404
  setSentryProjectSlug(data.sentryProjectSlug ?? "");
1297
1405
  setSentryEnvironment(data.sentryEnvironment ?? "");
1406
+ setAutomationFailureAlertsEnabled(
1407
+ data.automationFailureAlertsEnabled ?? true,
1408
+ );
1409
+ setAutomationFailureAlertEmail(data.automationFailureAlertEmail ?? "");
1298
1410
  }, [query.data]);
1299
1411
  return (
1300
1412
  <div className="max-w-4xl p-4 lg:p-6">
@@ -1409,6 +1521,57 @@ function SettingsView({ t }: { t: ReturnType<typeof useT> }) {
1409
1521
  />
1410
1522
  {t("triage.enableSentryPolling")}
1411
1523
  </label>
1524
+ <div className="space-y-3 border-t pt-4 sm:col-span-2">
1525
+ <div>
1526
+ <h3 className="text-sm font-medium">
1527
+ {t("factoryRoute.automationFailureAlertsTitle")}
1528
+ </h3>
1529
+ <p className="mt-1 text-sm text-muted-foreground">
1530
+ {t("factoryRoute.automationFailureAlertsDescription")}
1531
+ </p>
1532
+ </div>
1533
+ <label className="flex items-center gap-2 text-sm">
1534
+ <Checkbox
1535
+ checked={automationFailureAlertsEnabled}
1536
+ onCheckedChange={(checked) =>
1537
+ setAutomationFailureAlertsEnabled(checked === true)
1538
+ }
1539
+ />
1540
+ {t("factoryRoute.automationFailureAlertsEnabled")}
1541
+ </label>
1542
+ <div className="grid gap-1.5 sm:max-w-md">
1543
+ <Label htmlFor="factory-automation-failure-email">
1544
+ {t("factoryRoute.automationFailureAlertEmail")}
1545
+ </Label>
1546
+ <Input
1547
+ id="factory-automation-failure-email"
1548
+ type="email"
1549
+ value={automationFailureAlertEmail}
1550
+ onChange={(event) =>
1551
+ setAutomationFailureAlertEmail(event.target.value)
1552
+ }
1553
+ placeholder={t(
1554
+ "factoryRoute.automationFailureAlertEmailPlaceholder",
1555
+ )}
1556
+ />
1557
+ </div>
1558
+ <p className="text-xs text-muted-foreground">
1559
+ {t("factoryRoute.automationFailureEmailReadiness")}:{" "}
1560
+ {(query.data as TriageConfig | undefined)?.emailReadiness
1561
+ ?.status ?? "unknown"}
1562
+ </p>
1563
+ <p className="text-xs text-muted-foreground">
1564
+ {t("factoryRoute.automationEmailReadinessHint")}
1565
+ </p>
1566
+ {query.isError && (
1567
+ <p className="text-xs text-destructive">
1568
+ {t("factoryRoute.automationDiagnosticsLoadError")}{" "}
1569
+ {query.error instanceof Error
1570
+ ? query.error.message
1571
+ : String(query.error)}
1572
+ </p>
1573
+ )}
1574
+ </div>
1412
1575
  <div className="sm:col-span-2">
1413
1576
  <Button
1414
1577
  onClick={() =>
@@ -1423,6 +1586,13 @@ function SettingsView({ t }: { t: ReturnType<typeof useT> }) {
1423
1586
  sentryOrgSlug,
1424
1587
  sentryProjectSlug,
1425
1588
  sentryEnvironment,
1589
+ automationFailureAlertsEnabled,
1590
+ ...(automationFailureAlertEmail.trim()
1591
+ ? {
1592
+ automationFailureAlertEmail:
1593
+ automationFailureAlertEmail.trim(),
1594
+ }
1595
+ : {}),
1426
1596
  })
1427
1597
  }
1428
1598
  disabled={mutation.isPending}