@agent-native/core 0.136.4 → 0.137.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.
Files changed (170) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +53 -0
  3. package/corpus/templates/clips/actions/import-loom-recording.ts +7 -0
  4. package/corpus/templates/clips/actions/lib/loom-import-job.ts +24 -4
  5. package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
  6. package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
  7. package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
  8. package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
  9. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
  10. package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
  11. package/corpus/templates/clips/changelog/2026-08-03-restart-during-a-recording-now-immediately-starts-a-fresh-ta.md +6 -0
  12. package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
  13. package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
  14. package/corpus/templates/clips/desktop/src/app.tsx +81 -30
  15. package/corpus/templates/clips/desktop/src/lib/recorder.ts +436 -158
  16. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +14 -1
  17. package/corpus/templates/clips/server/plugins/auth.ts +9 -0
  18. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +13 -0
  19. package/corpus/templates/content/actions/_database-utils.ts +6 -0
  20. package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
  21. package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
  22. package/corpus/templates/content/actions/configure-document-property.ts +113 -32
  23. package/corpus/templates/content/actions/delete-content-database.ts +5 -7
  24. package/corpus/templates/content/actions/delete-document-property.ts +109 -62
  25. package/corpus/templates/content/actions/delete-document.ts +178 -36
  26. package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
  27. package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
  28. package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
  29. package/corpus/templates/content/actions/remove-database-items.ts +13 -0
  30. package/corpus/templates/content/actions/set-document-property.ts +69 -1
  31. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
  32. package/corpus/templates/content/parity/matrix.md +1 -1
  33. package/corpus/templates/content/parity/matrix.ts +1 -0
  34. package/corpus/templates/content/server/db/schema.ts +27 -0
  35. package/corpus/templates/content/server/plugins/db.ts +24 -0
  36. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
  37. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  38. package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
  39. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
  40. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
  41. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
  42. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
  43. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
  44. package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
  45. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  46. package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
  47. package/corpus/templates/design/app/i18n-data.ts +8 -0
  48. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
  49. package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
  50. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
  51. package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
  52. package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
  53. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
  54. package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
  55. package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
  56. package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
  57. package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
  58. package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
  59. package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
  60. package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
  61. package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
  62. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
  63. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
  64. package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
  65. package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
  66. package/corpus/templates/factory/.env.example +17 -0
  67. package/corpus/templates/factory/AGENTS.md +27 -17
  68. package/corpus/templates/factory/README.md +19 -9
  69. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
  70. package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
  71. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
  72. package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
  73. package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
  74. package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
  75. package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
  76. package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
  77. package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
  78. package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
  79. package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
  80. package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
  81. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
  82. package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
  83. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
  84. package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
  85. package/corpus/templates/factory/app/routes/factory.tsx +462 -9
  86. package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
  87. package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
  88. package/corpus/templates/factory/netlify.toml +2 -2
  89. package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
  90. package/corpus/templates/factory/server/connectors/slack.ts +133 -5
  91. package/corpus/templates/factory/server/db/schema.ts +6 -0
  92. package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
  93. package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
  94. package/corpus/templates/factory/server/plugins/auth.ts +1 -0
  95. package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
  96. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
  97. package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
  98. package/corpus/templates/factory/server/triage/contracts.ts +14 -1
  99. package/corpus/templates/factory/server/triage/github-client.ts +404 -0
  100. package/corpus/templates/factory/server/triage/metadata.ts +44 -0
  101. package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
  102. package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
  103. package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
  104. package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
  105. package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
  106. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
  107. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
  108. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
  109. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
  110. package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
  111. package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
  112. package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
  113. package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
  114. package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
  115. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
  116. package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
  117. package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
  118. package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
  119. package/corpus/templates/plan/shared/plan-content.ts +17 -2
  120. package/dist/cli/skills-content/canvas.d.ts +3 -3
  121. package/dist/cli/skills-content/canvas.js +16 -5
  122. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  123. package/dist/cli/skills-content/visual-plan-skill.js +27 -0
  124. package/dist/cli/skills-content/wireframe.d.ts +2 -2
  125. package/dist/cli/skills-content/wireframe.js +7 -0
  126. package/dist/client/AgentPanel.js +23 -8
  127. package/dist/client/api-path.d.ts +6 -0
  128. package/dist/client/api-path.js +55 -0
  129. package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
  130. package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
  131. package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
  132. package/dist/client/onboarding/first-run-enabled.js +9 -0
  133. package/dist/client/onboarding/index.d.ts +2 -1
  134. package/dist/client/onboarding/index.js +1 -0
  135. package/dist/client/onboarding/use-onboarding.d.ts +6 -1
  136. package/dist/client/onboarding/use-onboarding.js +32 -1
  137. package/dist/client/route-state.js +10 -0
  138. package/dist/client/settings/SecretsSection.js +32 -8
  139. package/dist/collab/struct-routes.d.ts +1 -1
  140. package/dist/connections/catalog.d.ts +4 -4
  141. package/dist/connections/catalog.js +3 -3
  142. package/dist/db/index.d.ts +1 -1
  143. package/dist/db/index.js +1 -1
  144. package/dist/localization/default-messages.d.ts +5 -0
  145. package/dist/localization/default-messages.js +5 -0
  146. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  147. package/dist/observability/routes.d.ts +3 -3
  148. package/dist/onboarding/app-profile.d.ts +4 -0
  149. package/dist/onboarding/app-profile.js +378 -0
  150. package/dist/onboarding/index.d.ts +2 -1
  151. package/dist/onboarding/index.js +1 -0
  152. package/dist/onboarding/plugin.d.ts +2 -0
  153. package/dist/onboarding/plugin.js +21 -1
  154. package/dist/onboarding/types.d.ts +19 -0
  155. package/dist/progress/routes.d.ts +1 -1
  156. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  157. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  158. package/dist/secrets/routes.d.ts +2 -2
  159. package/dist/secrets/routes.js +41 -17
  160. package/dist/server/auth.js +21 -1
  161. package/dist/server/better-auth-instance.d.ts +3 -2
  162. package/dist/server/better-auth-instance.js +9 -7
  163. package/dist/server/email-markdown.js +3 -8
  164. package/dist/server/transcribe-voice.d.ts +1 -1
  165. package/dist/shared/first-run-onboarding.d.ts +3 -0
  166. package/dist/shared/first-run-onboarding.js +3 -0
  167. package/dist/triggers/index.d.ts +3 -0
  168. package/dist/triggers/index.js +5 -0
  169. package/docs/content/cross-app-sso.mdx +26 -0
  170. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { useChatModels } from "@agent-native/core/client/agent-chat";
1
2
  import {
2
3
  useActionMutation,
3
4
  useActionQuery,
@@ -26,6 +27,7 @@ import {
26
27
  import { TriageStatusPill } from "@/components/triage/triage-status-pill";
27
28
  import { Button } from "@/components/ui/button";
28
29
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
30
+ import { Checkbox } from "@/components/ui/checkbox";
29
31
  import { Input } from "@/components/ui/input";
30
32
  import { Label } from "@/components/ui/label";
31
33
  import { Textarea } from "@/components/ui/textarea";
@@ -94,11 +96,41 @@ type TriageConfig = {
94
96
  slackChannelId?: string | null;
95
97
  slackChannelName?: string | null;
96
98
  pollingEnabled?: boolean;
99
+ githubPollingEnabled?: boolean;
100
+ sentryPollingEnabled?: boolean;
101
+ sentryOrgSlug?: string | null;
102
+ sentryProjectSlug?: string | null;
103
+ sentryEnvironment?: string | null;
97
104
  repository?: string | null;
98
105
  };
99
106
 
100
107
  type Verdict = "correct" | "incorrect" | "uncertain";
101
- type WorkspaceTab = "map" | "inbox" | "rules" | "settings";
108
+ type WorkspaceTab = "map" | "inbox" | "rules" | "automations" | "settings";
109
+
110
+ type FactoryAutomationRun = {
111
+ id?: string;
112
+ status?: string | null;
113
+ startedAt?: string | number | null;
114
+ finishedAt?: string | number | null;
115
+ error?: string | null;
116
+ };
117
+
118
+ type FactoryAutomation = {
119
+ id: string;
120
+ name: string;
121
+ prompt?: string | null;
122
+ body?: string | null;
123
+ model?: string | null;
124
+ schedule?: string | null;
125
+ enabled: boolean;
126
+ triggerType?: string | null;
127
+ event?: string | null;
128
+ timezone?: string | null;
129
+ condition?: string | null;
130
+ canUpdate?: boolean;
131
+ runs?: FactoryAutomationRun[] | null;
132
+ pastRuns?: FactoryAutomationRun[] | null;
133
+ };
102
134
 
103
135
  const DEFAULT_FACTORY_ID = "product-feedback";
104
136
 
@@ -387,7 +419,13 @@ export default function FactoryRoute() {
387
419
  active={activeTab === "rules"}
388
420
  onClick={() => setActiveTab("rules")}
389
421
  >
390
- Rules
422
+ {t("factoryRoute.rulesTab")}
423
+ </TabButton>
424
+ <TabButton
425
+ active={activeTab === "automations"}
426
+ onClick={() => setActiveTab("automations")}
427
+ >
428
+ {t("factoryRoute.automationsTab")}
391
429
  </TabButton>
392
430
  <TabButton
393
431
  active={activeTab === "settings"}
@@ -467,6 +505,8 @@ export default function FactoryRoute() {
467
505
  <InboxView t={t} />
468
506
  ) : activeTab === "rules" ? (
469
507
  <RulesView t={t} />
508
+ ) : activeTab === "automations" ? (
509
+ <AutomationsView factoryId={factoryId} t={t} />
470
510
  ) : (
471
511
  <SettingsView t={t} />
472
512
  )}
@@ -497,7 +537,10 @@ function TabButton({
497
537
  }
498
538
 
499
539
  function parseWorkspaceTab(value: string | null): WorkspaceTab {
500
- return value === "inbox" || value === "rules" || value === "settings"
540
+ return value === "inbox" ||
541
+ value === "rules" ||
542
+ value === "automations" ||
543
+ value === "settings"
501
544
  ? value
502
545
  : "map";
503
546
  }
@@ -513,6 +556,356 @@ function Metric({ label, value }: { label: string; value: number }) {
513
556
  );
514
557
  }
515
558
 
559
+ function AutomationsView({
560
+ factoryId,
561
+ t,
562
+ }: {
563
+ factoryId: string;
564
+ t: ReturnType<typeof useT>;
565
+ }) {
566
+ const [searchParams, setSearchParams] = useSearchParams();
567
+ const [draft, setDraft] = useState<FactoryAutomation | null>(null);
568
+ const selectedId = searchParams.get("automationId");
569
+ const automationsQuery = useActionQuery<FactoryAutomation[]>(
570
+ "list-factory-automations",
571
+ { factoryId },
572
+ );
573
+ const saveMutation = useActionMutation("save-factory-automation");
574
+ const runMutation = useActionMutation("run-factory-automation");
575
+ const {
576
+ availableModels,
577
+ defaultModel,
578
+ isLoading: modelsLoading,
579
+ } = useChatModels({ storageKey: null });
580
+ const response = automationsQuery.data;
581
+ const automations = response ?? [];
582
+ const selected =
583
+ automations.find((automation) => automation.id === selectedId) ??
584
+ automations[0] ??
585
+ null;
586
+ const modelOptions = useMemo(() => {
587
+ const configuredGroups = availableModels.filter(
588
+ (group) => group.configured,
589
+ );
590
+ const groups =
591
+ configuredGroups.length > 0 ? configuredGroups : availableModels;
592
+ const seen = new Set<string>();
593
+ return groups.flatMap((group) =>
594
+ group.models.flatMap((model) => {
595
+ if (model === "auto" || seen.has(model)) return [];
596
+ seen.add(model);
597
+ return [
598
+ { value: model, label: `${group.label} / ${formatModelName(model)}` },
599
+ ];
600
+ }),
601
+ );
602
+ }, [availableModels]);
603
+ const autoModelLabel = `Auto (currently ${formatModelName(defaultModel)})`;
604
+ const activeAutomationId = selected?.id ?? null;
605
+
606
+ function draftForAutomation(automation: FactoryAutomation) {
607
+ return { ...automation, model: automation.model?.trim() || "auto" };
608
+ }
609
+
610
+ function selectAutomation(id: string) {
611
+ const nextAutomation = automations.find(
612
+ (automation) => automation.id === id,
613
+ );
614
+ if (nextAutomation) setDraft(draftForAutomation(nextAutomation));
615
+ setSearchParams(
616
+ (current) => {
617
+ const next = new URLSearchParams(current);
618
+ next.set("automationId", id);
619
+ return next;
620
+ },
621
+ { replace: true },
622
+ );
623
+ }
624
+
625
+ useEffect(() => {
626
+ if (!selected) {
627
+ setDraft(null);
628
+ return;
629
+ }
630
+ if (selected.id !== selectedId) {
631
+ selectAutomation(selected.id);
632
+ return;
633
+ }
634
+ setDraft(draftForAutomation(selected));
635
+ }, [selected, selectedId]);
636
+
637
+ async function saveAutomation() {
638
+ if (!draft) return;
639
+ await saveMutation.mutateAsync({
640
+ factoryId,
641
+ automationId: draft.id,
642
+ name: draft.name,
643
+ prompt: draft.prompt ?? draft.body ?? "",
644
+ model: draft.model ?? "",
645
+ schedule: draft.schedule ?? "",
646
+ enabled: draft.enabled,
647
+ });
648
+ await automationsQuery.refetch();
649
+ }
650
+
651
+ async function runAutomation() {
652
+ if (!draft) return;
653
+ await runMutation.mutateAsync({ factoryId, automationId: draft.id });
654
+ await automationsQuery.refetch();
655
+ }
656
+
657
+ return (
658
+ <div className="grid gap-4 p-4 lg:grid-cols-[minmax(220px,.35fr)_minmax(0,1fr)] lg:p-6">
659
+ <Card>
660
+ <CardHeader>
661
+ <CardTitle className="text-base">
662
+ {t("factoryRoute.automationsTitle")}
663
+ </CardTitle>
664
+ <p className="text-sm text-muted-foreground">
665
+ {t("factoryRoute.automationsDescription")}
666
+ </p>
667
+ </CardHeader>
668
+ <CardContent className="p-0">
669
+ {automationsQuery.isLoading ? (
670
+ <p className="p-4 text-sm text-muted-foreground">
671
+ {t("factoryRoute.automationsLoading")}
672
+ </p>
673
+ ) : automations.length === 0 ? (
674
+ <p className="p-4 text-sm text-muted-foreground">
675
+ {t("factoryRoute.automationsEmpty")}
676
+ </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
+ )}
706
+ </CardContent>
707
+ </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>
720
+ <CardTitle className="text-base">
721
+ {t("factoryRoute.automationEditorTitle")}
722
+ </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
+ <p className="text-sm text-muted-foreground">
759
+ {t("factoryRoute.selectAutomation")}
760
+ </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"
776
+ >
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)}
785
+ </option>
786
+ )}
787
+ {modelOptions.map((option) => (
788
+ <option key={option.value} value={option.value}>
789
+ {option.label}
790
+ </option>
791
+ ))}
792
+ </select>
793
+ </div>
794
+ <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 ?? ""}
801
+ onChange={(event) =>
802
+ setDraft({ ...draft, schedule: event.target.value })
803
+ }
804
+ placeholder={t(
805
+ "factoryRoute.automationSchedulePlaceholder",
806
+ )}
807
+ />
808
+ </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>
838
+ </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>
887
+ </div>
888
+ );
889
+ }
890
+
891
+ function formatAutomationDate(value: string | number | null | undefined) {
892
+ if (!value) return "-";
893
+ const date = new Date(value);
894
+ return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
895
+ }
896
+
897
+ function formatModelName(model: string | null | undefined) {
898
+ const value = model?.trim();
899
+ if (!value) return "the app default";
900
+ const match = value.match(/^(?:openai\/)?gpt-5[.-]6[.-](sol|terra|luna)$/i);
901
+ if (match) return `GPT-5.6 ${match[1][0].toUpperCase()}${match[1].slice(1)}`;
902
+ return value
903
+ .split(/[./_-]+/)
904
+ .filter(Boolean)
905
+ .map((part) => part[0].toUpperCase() + part.slice(1))
906
+ .join(" ");
907
+ }
908
+
516
909
  function InboxView({ t }: { t: ReturnType<typeof useT> }) {
517
910
  const [status, setStatus] = useState("");
518
911
  const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -882,6 +1275,11 @@ function SettingsView({ t }: { t: ReturnType<typeof useT> }) {
882
1275
  const [channelName, setChannelName] = useState("");
883
1276
  const [repository, setRepository] = useState("");
884
1277
  const [polling, setPolling] = useState(false);
1278
+ const [githubPolling, setGithubPolling] = useState(false);
1279
+ const [sentryPolling, setSentryPolling] = useState(false);
1280
+ const [sentryOrgSlug, setSentryOrgSlug] = useState("");
1281
+ const [sentryProjectSlug, setSentryProjectSlug] = useState("");
1282
+ const [sentryEnvironment, setSentryEnvironment] = useState("");
885
1283
  const query = useActionQuery("get-triage-config", {});
886
1284
  const mutation = useActionMutation("save-triage-config");
887
1285
  useEffect(() => {
@@ -892,6 +1290,11 @@ function SettingsView({ t }: { t: ReturnType<typeof useT> }) {
892
1290
  setChannelName(data.slackChannelName ?? "");
893
1291
  setRepository(data.repository ?? "");
894
1292
  setPolling(data.pollingEnabled ?? false);
1293
+ setGithubPolling(data.githubPollingEnabled ?? false);
1294
+ setSentryPolling(data.sentryPollingEnabled ?? false);
1295
+ setSentryOrgSlug(data.sentryOrgSlug ?? "");
1296
+ setSentryProjectSlug(data.sentryProjectSlug ?? "");
1297
+ setSentryEnvironment(data.sentryEnvironment ?? "");
895
1298
  }, [query.data]);
896
1299
  return (
897
1300
  <div className="max-w-4xl p-4 lg:p-6">
@@ -944,7 +1347,7 @@ function SettingsView({ t }: { t: ReturnType<typeof useT> }) {
944
1347
  />
945
1348
  </div>
946
1349
  <div className="grid gap-1.5">
947
- <Label htmlFor="factory-repository">Repository</Label>
1350
+ <Label htmlFor="factory-repository">{t("triage.repository")}</Label>
948
1351
  <Input
949
1352
  id="factory-repository"
950
1353
  value={repository}
@@ -953,14 +1356,59 @@ function SettingsView({ t }: { t: ReturnType<typeof useT> }) {
953
1356
  />
954
1357
  </div>
955
1358
  <label className="flex items-center gap-2 text-sm sm:col-span-2">
956
- <input
957
- type="checkbox"
1359
+ <Checkbox
958
1360
  checked={polling}
959
- onChange={(event) => setPolling(event.target.checked)}
960
- className="size-4 rounded border"
1361
+ onCheckedChange={(checked) => setPolling(checked === true)}
961
1362
  />
962
1363
  {t("triage.enablePolling")}
963
1364
  </label>
1365
+ <label className="flex items-center gap-2 text-sm sm:col-span-2">
1366
+ <Checkbox
1367
+ checked={githubPolling}
1368
+ onCheckedChange={(checked) => setGithubPolling(checked === true)}
1369
+ />
1370
+ {t("triage.enableGithubPolling")}
1371
+ </label>
1372
+ <div className="grid gap-1.5">
1373
+ <Label htmlFor="factory-sentry-org">
1374
+ {t("triage.sentryOrgSlug")}
1375
+ </Label>
1376
+ <Input
1377
+ id="factory-sentry-org"
1378
+ value={sentryOrgSlug}
1379
+ onChange={(event) => setSentryOrgSlug(event.target.value)}
1380
+ placeholder={t("triage.sentryOrgPlaceholder")}
1381
+ />
1382
+ </div>
1383
+ <div className="grid gap-1.5">
1384
+ <Label htmlFor="factory-sentry-project">
1385
+ {t("triage.sentryProjectSlug")}
1386
+ </Label>
1387
+ <Input
1388
+ id="factory-sentry-project"
1389
+ value={sentryProjectSlug}
1390
+ onChange={(event) => setSentryProjectSlug(event.target.value)}
1391
+ placeholder={t("triage.sentryProjectPlaceholder")}
1392
+ />
1393
+ </div>
1394
+ <div className="grid gap-1.5">
1395
+ <Label htmlFor="factory-sentry-environment">
1396
+ {t("triage.sentryEnvironment")}
1397
+ </Label>
1398
+ <Input
1399
+ id="factory-sentry-environment"
1400
+ value={sentryEnvironment}
1401
+ onChange={(event) => setSentryEnvironment(event.target.value)}
1402
+ placeholder={t("triage.sentryEnvironmentPlaceholder")}
1403
+ />
1404
+ </div>
1405
+ <label className="flex items-center gap-2 text-sm sm:col-span-2">
1406
+ <Checkbox
1407
+ checked={sentryPolling}
1408
+ onCheckedChange={(checked) => setSentryPolling(checked === true)}
1409
+ />
1410
+ {t("triage.enableSentryPolling")}
1411
+ </label>
964
1412
  <div className="sm:col-span-2">
965
1413
  <Button
966
1414
  onClick={() =>
@@ -970,12 +1418,17 @@ function SettingsView({ t }: { t: ReturnType<typeof useT> }) {
970
1418
  slackChannelName: channelName,
971
1419
  repository,
972
1420
  pollingEnabled: polling,
1421
+ githubPollingEnabled: githubPolling,
1422
+ sentryPollingEnabled: sentryPolling,
1423
+ sentryOrgSlug,
1424
+ sentryProjectSlug,
1425
+ sentryEnvironment,
973
1426
  })
974
1427
  }
975
1428
  disabled={mutation.isPending}
976
1429
  >
977
1430
  {mutation.isPending && <IconLoader2 className="animate-spin" />}
978
- Save settings
1431
+ {t("triage.saveSettings")}
979
1432
  </Button>
980
1433
  </div>
981
1434
  </CardContent>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-04
4
+ ---
5
+
6
+ Added organization-owned Factory automations for Slack bug triage, Builder handoffs, Sentry/GitHub intake, and governed PR approval.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-04
4
+ ---
5
+
6
+ Improved Factory automation editing with reliable tab navigation, a model picker that explains Auto, and map selection that no longer creates false unsaved changes.
@@ -1,7 +1,7 @@
1
1
  [build]
2
2
  command = "NITRO_PRESET=netlify pnpm --filter factory build"
3
- publish = "templates/factory/dist"
4
- functions = "templates/factory/.netlify/functions-internal"
3
+ publish = "dist"
4
+ functions = ".netlify/functions-internal"
5
5
 
6
6
  [build.environment]
7
7
  GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
@@ -112,11 +112,6 @@ export async function resolveConnectorSecret(
112
112
  if (legacySecret?.trim()) return legacySecret.trim();
113
113
  }
114
114
 
115
- // Deployment-managed connector configuration is the last resort inside the
116
- // resolver, never a parallel path in a provider client.
117
- const environmentSecret = process.env[key]?.trim(); // guard:allow-env-credential - deploy-level connector fallback
118
- if (environmentSecret) return environmentSecret;
119
-
120
115
  const vaultOrgId = getVaultOrgId();
121
116
  if (vaultOrgId && !orgIds.includes(vaultOrgId)) {
122
117
  for (const scope of ["org", "workspace"] as const) {
@@ -125,5 +120,11 @@ export async function resolveConnectorSecret(
125
120
  }
126
121
  }
127
122
 
123
+ // Deployment-managed connector configuration is the last resort inside the
124
+ // resolver, never a parallel path in a provider client. A designated
125
+ // Dispatch vault must win over a stale deployment copy of the same key.
126
+ const environmentSecret = process.env[key]?.trim(); // guard:allow-env-credential - deploy-level connector fallback
127
+ if (environmentSecret) return environmentSecret;
128
+
128
129
  return undefined;
129
130
  }