@agent-native/core 0.164.12 → 0.164.17

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 (90) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/netlify.toml +4 -0
  3. package/corpus/templates/clips/actions/request-transcript.ts +10 -6
  4. package/corpus/templates/content/actions/_database-source-utils.ts +25 -2
  5. package/corpus/templates/content/actions/_database-utils.ts +2 -0
  6. package/corpus/templates/content/actions/_local-folder-source.ts +186 -2
  7. package/corpus/templates/content/actions/connect-local-folder-source.ts +23 -8
  8. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +3 -3
  9. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +2 -2
  10. package/corpus/templates/content/actions/sync-local-folder-source.ts +130 -12
  11. package/corpus/templates/content/app/components/LocalFolderLiveSync.tsx +77 -0
  12. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +298 -3
  13. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +354 -8
  14. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +23 -1
  15. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +81 -1
  16. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +36 -0
  17. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +18 -4
  18. package/corpus/templates/content/app/components/editor/local-source-write-state.ts +52 -0
  19. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +244 -6
  20. package/corpus/templates/content/app/components/sidebar/local-source-hierarchy.ts +189 -0
  21. package/corpus/templates/content/app/hooks/use-create-page.ts +26 -4
  22. package/corpus/templates/content/app/hooks/use-documents.ts +83 -0
  23. package/corpus/templates/content/app/i18n-data.ts +239 -0
  24. package/corpus/templates/content/app/lib/desktop-content-files.ts +57 -1
  25. package/corpus/templates/content/app/lib/local-content-source-files.ts +210 -34
  26. package/corpus/templates/content/app/lib/local-folder-live-sync.ts +191 -0
  27. package/corpus/templates/content/app/root.tsx +2 -1
  28. package/corpus/templates/content/app/routes/_app.local-files.tsx +103 -38
  29. package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +2 -2
  30. package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +3 -3
  31. package/corpus/templates/content/docs/product/roadmap.md +2 -2
  32. package/corpus/templates/content/docs/solutions/2026-08-18-colored-text-highlighting-shape.md +693 -0
  33. package/corpus/templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md +482 -0
  34. package/corpus/templates/content/package.json +1 -0
  35. package/corpus/templates/content/shared/api.ts +25 -0
  36. package/corpus/templates/content/shared/nfm.ts +14 -3
  37. package/corpus/templates/factory/AGENTS.md +24 -29
  38. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +8 -28
  39. package/corpus/templates/factory/actions/get-triage-config.ts +1 -0
  40. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +67 -59
  41. package/corpus/templates/factory/actions/list-triage-items.ts +6 -1
  42. package/corpus/templates/factory/actions/reconcile-triage-run.ts +7 -1
  43. package/corpus/templates/factory/actions/save-triage-config.ts +27 -7
  44. package/corpus/templates/factory/actions/start-builder-for-item.ts +113 -31
  45. package/corpus/templates/factory/actions/view-screen.ts +12 -0
  46. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +457 -0
  47. package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +53 -0
  48. package/corpus/templates/factory/app/components/layout/Layout.tsx +2 -0
  49. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +3 -1
  50. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +10 -7
  51. package/corpus/templates/factory/app/i18n/en-US.ts +3 -0
  52. package/corpus/templates/factory/app/routes/agents.tsx +22 -1
  53. package/corpus/templates/factory/app/routes/factory-settings.tsx +34 -0
  54. package/corpus/templates/factory/app/routes/factory.tsx +33 -323
  55. package/corpus/templates/factory/server/connectors/slack.ts +62 -0
  56. package/corpus/templates/factory/server/db/schema.ts +1 -0
  57. package/corpus/templates/factory/server/lib/slack-feedback-prompt.ts +30 -0
  58. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -8
  59. package/corpus/templates/factory/server/plugins/factory-migrations.ts +7 -0
  60. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +43 -25
  61. package/corpus/templates/factory/server/triage/ai-services-git.ts +2 -0
  62. package/corpus/templates/factory/server/triage/pr-monitor.ts +2 -1
  63. package/corpus/templates/factory/server/triage/pr-policy.ts +197 -19
  64. package/corpus/templates/factory/server/triage/review-skill-alignment.ts +127 -0
  65. package/corpus/templates/factory/server/triage/slack-client.ts +79 -6
  66. package/dist/app-config/auth.d.ts +4 -0
  67. package/dist/app-config/auth.js +7 -0
  68. package/dist/app-config/schema.d.ts +3 -0
  69. package/dist/app-config/schema.js +2 -0
  70. package/dist/cli/code-agent-executor.js +6 -1
  71. package/dist/cli/code-agent-mcp-config.d.ts +2 -2
  72. package/dist/cli/code-agent-mcp-config.js +34 -9
  73. package/dist/client/MultiTabAssistantChat.js +28 -1
  74. package/dist/client/org/OrgSwitcher.js +8 -4
  75. package/dist/client/use-chat-threads.d.ts +1 -0
  76. package/dist/client/use-chat-threads.js +13 -3
  77. package/dist/collab/awareness.d.ts +2 -2
  78. package/dist/collab/routes.d.ts +1 -1
  79. package/dist/mcp-client/config.d.ts +11 -0
  80. package/dist/mcp-client/config.js +33 -26
  81. package/dist/notifications/routes.d.ts +3 -3
  82. package/dist/observability/routes.d.ts +5 -5
  83. package/dist/progress/routes.d.ts +1 -1
  84. package/dist/resources/handlers.d.ts +2 -2
  85. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  86. package/dist/server/auth.js +17 -3
  87. package/dist/server/realtime-token.d.ts +1 -1
  88. package/dist/server/transcribe-voice.d.ts +1 -1
  89. package/dist/vite/client.js +2 -0
  90. package/package.json +1 -1
@@ -1,5 +1,4 @@
1
1
  import {
2
- AgentToggleButton,
3
2
  requestAgentChatThreadOpen,
4
3
  useChatModels,
5
4
  } from "@agent-native/core/client/agent-chat";
@@ -8,8 +7,6 @@ import {
8
7
  useActionQuery,
9
8
  } from "@agent-native/core/client/hooks";
10
9
  import { useT } from "@agent-native/core/client/i18n";
11
- import { buildSettingsRoute } from "@agent-native/core/client/navigation";
12
- import { SettingsGroup, SettingsRow } from "@agent-native/core/client/settings";
13
10
  import {
14
11
  IconAlertCircle,
15
12
  IconArrowLeft,
@@ -20,7 +17,7 @@ import {
20
17
  IconRefresh,
21
18
  } from "@tabler/icons-react";
22
19
  import { useEffect, useMemo, useRef, useState } from "react";
23
- import { Link, useSearchParams } from "react-router";
20
+ import { Link, Navigate, useSearchParams } from "react-router";
24
21
  import { toast } from "sonner";
25
22
 
26
23
  import { FactoryAgentsView } from "@/components/factory/FactoryAgentsView";
@@ -33,13 +30,13 @@ import {
33
30
  } from "@/components/factory/FactoryCanvas";
34
31
  import { FactoryHistoryView } from "@/components/factory/FactoryHistoryView";
35
32
  import { FactoryInspector } from "@/components/factory/FactoryInspector";
33
+ import { FactoryWorkspaceActions } from "@/components/factory/FactoryWorkspaceActions";
36
34
  import { TriageStatusPill } from "@/components/triage/triage-status-pill";
37
35
  import { Button } from "@/components/ui/button";
38
36
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
39
37
  import { Checkbox } from "@/components/ui/checkbox";
40
38
  import { Input } from "@/components/ui/input";
41
39
  import { Label } from "@/components/ui/label";
42
- import { Switch } from "@/components/ui/switch";
43
40
  import { Textarea } from "@/components/ui/textarea";
44
41
 
45
42
  type FactoryGraphResponse = {
@@ -104,25 +101,6 @@ type TriageRule = {
104
101
  promptVersion: number;
105
102
  };
106
103
 
107
- type TriageConfig = {
108
- slackWorkspace?: "primary" | "secondary";
109
- slackChannelId?: string | null;
110
- slackChannelName?: string | null;
111
- pollingEnabled?: boolean;
112
- githubPollingEnabled?: boolean;
113
- sentryPollingEnabled?: boolean;
114
- sentryOrgSlug?: string | null;
115
- sentryProjectSlug?: string | null;
116
- sentryEnvironment?: string | null;
117
- repository?: string | null;
118
- automationFailureAlertsEnabled?: boolean;
119
- automationFailureAlertEmail?: string | null;
120
- emailReadiness?: {
121
- status: "ready" | "not-configured" | "misconfigured" | "unavailable";
122
- provider: string;
123
- };
124
- };
125
-
126
104
  type Verdict = "correct" | "incorrect" | "uncertain";
127
105
  type WorkspaceTab =
128
106
  | "overview"
@@ -132,8 +110,7 @@ type WorkspaceTab =
132
110
  | "automations"
133
111
  | "agents"
134
112
  | "audit"
135
- | "history"
136
- | "settings";
113
+ | "history";
137
114
 
138
115
  type FactoryAutomationRun = {
139
116
  id?: string;
@@ -145,14 +122,6 @@ type FactoryAutomationRun = {
145
122
  threadId?: string | null;
146
123
  };
147
124
 
148
- type FactoryAutomationHealth = {
149
- status: "healthy" | "stale" | "error" | "no-data";
150
- lastCheckedAt?: number | null;
151
- lastDispatchedAt?: number | null;
152
- lastError?: string | null;
153
- runtime?: string | null;
154
- };
155
-
156
125
  type FactoryAutomation = {
157
126
  id: string;
158
127
  name: string;
@@ -229,6 +198,7 @@ export default function FactoryRoute() {
229
198
  next.delete("itemId");
230
199
  next.delete("automationId");
231
200
  next.delete("auditRunId");
201
+ next.delete("new");
232
202
  if (options?.tab) {
233
203
  if (options.tab === "overview") next.delete("tab");
234
204
  else next.set("tab", options.tab);
@@ -250,6 +220,7 @@ export default function FactoryRoute() {
250
220
  next.delete("itemId");
251
221
  next.delete("automationId");
252
222
  next.delete("auditRunId");
223
+ next.delete("new");
253
224
  return next;
254
225
  },
255
226
  { replace: true },
@@ -408,6 +379,11 @@ export default function FactoryRoute() {
408
379
  setSelectedEdgeId(null);
409
380
  }
410
381
 
382
+ useEffect(() => {
383
+ if (searchParams.get("new") !== "1") return;
384
+ startNewFactory();
385
+ }, [searchParams]);
386
+
411
387
  async function saveGraph() {
412
388
  if (!graph || !selectedFactoryId) return;
413
389
  const submittedDraftRevision = draftRevisionRef.current;
@@ -493,22 +469,28 @@ export default function FactoryRoute() {
493
469
  }
494
470
  }
495
471
 
472
+ if (searchParams.get("tab") === "settings") {
473
+ return <Navigate to="/factory-settings" replace />;
474
+ }
475
+ if (selectedFactoryId && searchParams.get("tab") === "agents") {
476
+ return <Navigate to="/factory?tab=agents" replace />;
477
+ }
478
+
496
479
  if (!selectedFactoryId && activeTab === "agents") {
497
480
  return (
498
481
  <div className="flex h-full min-h-0 flex-col overflow-y-auto bg-background">
499
482
  <div className="flex items-center gap-3 px-4 py-4 lg:px-6">
500
- <Button
501
- type="button"
502
- variant="ghost"
503
- size="icon"
504
- aria-label={t("factoryRoute.backToFactories")}
505
- onClick={() => setActiveTab("overview")}
506
- >
507
- <IconArrowLeft className="size-4" />
483
+ <Button asChild type="button" variant="ghost" size="icon">
484
+ <Link to="/factory" aria-label={t("factoryRoute.backToFactories")}>
485
+ <IconArrowLeft className="size-4" />
486
+ </Link>
508
487
  </Button>
509
488
  <h1 className="text-sm font-medium sm:text-base">
510
489
  {t("factoryRoute.agentsTitle")}
511
490
  </h1>
491
+ <div className="ms-auto">
492
+ <FactoryWorkspaceActions onNewFactory={startNewFactory} />
493
+ </div>
512
494
  </div>
513
495
  <FactoryAgentsView />
514
496
  </div>
@@ -521,21 +503,7 @@ export default function FactoryRoute() {
521
503
  <section className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-6 p-4 lg:p-6">
522
504
  <div className="flex flex-wrap items-end justify-between gap-4">
523
505
  <h1 className="text-2xl font-semibold tracking-tight">Factories</h1>
524
- <div className="flex items-center gap-2">
525
- <Button
526
- type="button"
527
- variant="outline"
528
- size="sm"
529
- onClick={() => setActiveTab("agents")}
530
- >
531
- {t("factoryRoute.agentsTab")}
532
- </Button>
533
- <Button type="button" size="sm" onClick={startNewFactory}>
534
- <IconPlus className="size-4" />
535
- {t("factoryRoute.newFactory")}
536
- </Button>
537
- <AgentToggleButton />
538
- </div>
506
+ <FactoryWorkspaceActions onNewFactory={startNewFactory} />
539
507
  </div>
540
508
 
541
509
  {factoryListQuery.isError ? (
@@ -631,10 +599,7 @@ export default function FactoryRoute() {
631
599
  </Button>
632
600
  <div className="h-5 w-48 animate-pulse rounded bg-muted" />
633
601
  </div>
634
- <div className="flex items-center gap-2">
635
- <div className="h-9 w-28 animate-pulse rounded bg-muted" />
636
- <AgentToggleButton />
637
- </div>
602
+ <FactoryWorkspaceActions onNewFactory={startNewFactory} />
638
603
  </div>
639
604
  </header>
640
605
  <main className="flex flex-1 items-center justify-center p-6">
@@ -681,16 +646,7 @@ export default function FactoryRoute() {
681
646
  </span>
682
647
  </div>
683
648
  <div className="flex items-center gap-2">
684
- <Button
685
- type="button"
686
- variant="outline"
687
- size="sm"
688
- onClick={startNewFactory}
689
- >
690
- <IconPlus className="size-4" />
691
- {t("factoryRoute.newFactory")}
692
- </Button>
693
- <AgentToggleButton />
649
+ <FactoryWorkspaceActions onNewFactory={startNewFactory} />
694
650
  </div>
695
651
  </div>
696
652
  <div className="flex items-center gap-2 overflow-x-auto px-2 py-2 sm:px-4 lg:px-6">
@@ -728,12 +684,6 @@ export default function FactoryRoute() {
728
684
  >
729
685
  {t("factoryRoute.automationsTab")}
730
686
  </TabButton>
731
- <TabButton
732
- active={activeTab === "agents"}
733
- onClick={() => setActiveTab("agents")}
734
- >
735
- {t("factoryRoute.agentsTab")}
736
- </TabButton>
737
687
  <TabButton
738
688
  active={activeTab === "audit"}
739
689
  onClick={() => setActiveTab("audit")}
@@ -746,12 +696,6 @@ export default function FactoryRoute() {
746
696
  >
747
697
  {t("factoryRoute.historyTab")}
748
698
  </TabButton>
749
- <TabButton
750
- active={activeTab === "settings"}
751
- onClick={() => setActiveTab("settings")}
752
- >
753
- Settings
754
- </TabButton>
755
699
  </nav>
756
700
  {activeTab === "audit" && (
757
701
  <Button
@@ -779,7 +723,6 @@ export default function FactoryRoute() {
779
723
  onOpenReview={() => setActiveTab("inbox")}
780
724
  onOpenAutomations={() => setActiveTab("automations")}
781
725
  onOpenActivity={() => setActiveTab("audit")}
782
- onOpenSettings={() => setActiveTab("settings")}
783
726
  onOpenFlow={() => setActiveTab("map")}
784
727
  />
785
728
  ) : activeTab === "map" ? (
@@ -832,8 +775,6 @@ export default function FactoryRoute() {
832
775
  <RulesView t={t} />
833
776
  ) : activeTab === "automations" ? (
834
777
  <AutomationsView factoryId={factoryId} t={t} />
835
- ) : activeTab === "agents" ? (
836
- <FactoryAgentsView />
837
778
  ) : activeTab === "audit" ? (
838
779
  <FactoryAuditView
839
780
  factoryId={factoryId}
@@ -846,9 +787,7 @@ export default function FactoryRoute() {
846
787
  hasUnsavedChanges={dirty}
847
788
  onRestored={handleFactoryRestored}
848
789
  />
849
- ) : (
850
- <SettingsView t={t} />
851
- )}
790
+ ) : null}
852
791
  </main>
853
792
  </div>
854
793
  );
@@ -882,8 +821,7 @@ function parseWorkspaceTab(value: string | null): WorkspaceTab {
882
821
  value === "automations" ||
883
822
  value === "agents" ||
884
823
  value === "audit" ||
885
- value === "history" ||
886
- value === "settings"
824
+ value === "history"
887
825
  ? value
888
826
  : "overview";
889
827
  }
@@ -896,7 +834,6 @@ function OverviewView({
896
834
  onOpenReview,
897
835
  onOpenAutomations,
898
836
  onOpenActivity,
899
- onOpenSettings,
900
837
  onOpenFlow,
901
838
  }: {
902
839
  graph: FactoryCanvasGraph;
@@ -906,7 +843,6 @@ function OverviewView({
906
843
  onOpenReview: () => void;
907
844
  onOpenAutomations: () => void;
908
845
  onOpenActivity: () => void;
909
- onOpenSettings: () => void;
910
846
  onOpenFlow: () => void;
911
847
  }) {
912
848
  return (
@@ -984,8 +920,10 @@ function OverviewView({
984
920
  <Button type="button" variant="outline" onClick={onOpenActivity}>
985
921
  Activity
986
922
  </Button>
987
- <Button type="button" variant="outline" onClick={onOpenSettings}>
988
- Settings
923
+ <Button asChild type="button" variant="outline">
924
+ <Link to="/factory-settings">
925
+ {t("factoryRoute.factorySettings")}
926
+ </Link>
989
927
  </Button>
990
928
  <Button type="button" onClick={onOpenFlow}>
991
929
  {t("factoryRoute.editFlow")}
@@ -1910,234 +1848,6 @@ function RulesView({ t }: { t: ReturnType<typeof useT> }) {
1910
1848
  );
1911
1849
  }
1912
1850
 
1913
- function SettingsView({ t }: { t: ReturnType<typeof useT> }) {
1914
- const [automationFailureAlertsEnabled, setAutomationFailureAlertsEnabled] =
1915
- useState(true);
1916
- const [automationFailureAlertEmail, setAutomationFailureAlertEmail] =
1917
- useState("");
1918
- const query = useActionQuery("get-triage-config", {});
1919
- const schedulerHealthQuery = useActionQuery<FactoryAutomationHealth>(
1920
- "get-factory-automation-health",
1921
- {},
1922
- { refetchInterval: 60_000 },
1923
- );
1924
- const mutation = useActionMutation("save-triage-config");
1925
-
1926
- useEffect(() => {
1927
- const data = query.data as TriageConfig | undefined;
1928
- if (!data) return;
1929
- setAutomationFailureAlertsEnabled(
1930
- data.automationFailureAlertsEnabled ?? true,
1931
- );
1932
- setAutomationFailureAlertEmail(data.automationFailureAlertEmail ?? "");
1933
- }, [query.data]);
1934
-
1935
- const saveSettings = () => {
1936
- mutation.mutate({
1937
- automationFailureAlertsEnabled,
1938
- ...(automationFailureAlertEmail.trim()
1939
- ? { automationFailureAlertEmail: automationFailureAlertEmail.trim() }
1940
- : {}),
1941
- });
1942
- };
1943
-
1944
- if (query.isLoading) {
1945
- return (
1946
- <div className="mx-auto w-full max-w-3xl space-y-6 p-4 lg:p-6">
1947
- <FactorySettingsSkeleton t={t} />
1948
- </div>
1949
- );
1950
- }
1951
-
1952
- return (
1953
- <div className="mx-auto w-full max-w-3xl space-y-6 p-4 lg:p-6">
1954
- <SettingsGroup variant="soft">
1955
- <SettingsRow
1956
- label={t("factoryRoute.workspaceIntegrations")}
1957
- description={t("settings.workspaceDescription")}
1958
- control={
1959
- <Button asChild type="button" variant="outline">
1960
- <Link to={buildSettingsRoute("integrations")}>Manage</Link>
1961
- </Button>
1962
- }
1963
- />
1964
- <SettingsRow
1965
- label={t("factoryRoute.agentAccess")}
1966
- description={t("settings.agentDescription")}
1967
- control={
1968
- <Button asChild type="button" variant="outline">
1969
- <Link to={buildSettingsRoute("agent")}>Manage</Link>
1970
- </Button>
1971
- }
1972
- />
1973
- </SettingsGroup>
1974
-
1975
- <SettingsGroup variant="soft">
1976
- <SettingsRow
1977
- label={t("factoryRoute.automationFailureAlertsTitle")}
1978
- description={t("factoryRoute.automationFailureAlertsDescription")}
1979
- control={
1980
- <Switch
1981
- aria-label={t("factoryRoute.automationFailureAlertsEnabled")}
1982
- checked={automationFailureAlertsEnabled}
1983
- onCheckedChange={(checked) =>
1984
- setAutomationFailureAlertsEnabled(checked === true)
1985
- }
1986
- />
1987
- }
1988
- />
1989
- <SettingsRow
1990
- label={t("factoryRoute.automationFailureAlertEmail")}
1991
- description={t("factoryRoute.automationFailureAlertEmailPlaceholder")}
1992
- control={
1993
- <Input
1994
- aria-label={t("factoryRoute.automationFailureAlertEmail")}
1995
- type="email"
1996
- value={automationFailureAlertEmail}
1997
- onChange={(event) =>
1998
- setAutomationFailureAlertEmail(event.target.value)
1999
- }
2000
- placeholder={t(
2001
- "factoryRoute.automationFailureAlertEmailPlaceholder",
2002
- )}
2003
- className="h-9 w-full sm:w-64"
2004
- />
2005
- }
2006
- />
2007
- <SettingsRow
2008
- label={t("factoryRoute.automationFailureEmailReadiness")}
2009
- description={t("factoryRoute.automationEmailReadinessHint")}
2010
- control={
2011
- <span className="rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
2012
- {(query.data as TriageConfig | undefined)?.emailReadiness
2013
- ?.status ?? "unknown"}
2014
- </span>
2015
- }
2016
- />
2017
- {query.isError && (
2018
- <div className="px-5 py-3 text-xs text-destructive sm:px-6">
2019
- {t("factoryRoute.automationDiagnosticsLoadError")}{" "}
2020
- {query.error instanceof Error
2021
- ? query.error.message
2022
- : String(query.error)}
2023
- </div>
2024
- )}
2025
- </SettingsGroup>
2026
-
2027
- <div className="flex flex-wrap items-center justify-end gap-3">
2028
- {mutation.isError && (
2029
- <p className="text-sm text-destructive" role="alert">
2030
- {t("triage.settingsError")}
2031
- </p>
2032
- )}
2033
- {mutation.isSuccess && (
2034
- <p className="text-sm text-muted-foreground" role="status">
2035
- {t("triage.settingsSaved")}
2036
- </p>
2037
- )}
2038
- <Button onClick={saveSettings} disabled={mutation.isPending}>
2039
- {mutation.isPending && <IconLoader2 className="animate-spin" />}
2040
- {t("triage.saveSettings")}
2041
- </Button>
2042
- </div>
2043
-
2044
- <SchedulerHealthStatus
2045
- health={schedulerHealthQuery.data}
2046
- isError={schedulerHealthQuery.isError}
2047
- error={schedulerHealthQuery.error}
2048
- t={t}
2049
- />
2050
- </div>
2051
- );
2052
- }
2053
-
2054
- function FactorySettingsSkeleton({ t }: { t: ReturnType<typeof useT> }) {
2055
- return (
2056
- <div className="space-y-6" aria-label={t("triage.loading")}>
2057
- {Array.from({ length: 2 }).map((_, index) => (
2058
- <div key={index} className="grid gap-2">
2059
- <div className="grid gap-2">
2060
- {Array.from({ length: index === 0 ? 5 : 3 }).map((_, rowIndex) => (
2061
- <div
2062
- key={rowIndex}
2063
- className="flex min-h-20 items-center justify-between gap-4 rounded-xl bg-card px-5 py-4 shadow-sm sm:px-6"
2064
- >
2065
- <div className="h-3.5 w-36 animate-pulse rounded bg-muted" />
2066
- <div className="h-9 w-24 animate-pulse rounded bg-muted" />
2067
- </div>
2068
- ))}
2069
- </div>
2070
- </div>
2071
- ))}
2072
- </div>
2073
- );
2074
- }
2075
- function SchedulerHealthStatus({
2076
- health,
2077
- isError,
2078
- error,
2079
- t,
2080
- }: {
2081
- health?: FactoryAutomationHealth;
2082
- isError: boolean;
2083
- error: unknown;
2084
- t: ReturnType<typeof useT>;
2085
- }) {
2086
- const healthLabel = isError
2087
- ? t("factoryRoute.automationHealthError")
2088
- : health
2089
- ? {
2090
- healthy: t("factoryRoute.automationHealthHealthy"),
2091
- stale: t("factoryRoute.automationHealthStale"),
2092
- error: t("factoryRoute.automationHealthError"),
2093
- "no-data": t("factoryRoute.automationHealthNoData"),
2094
- }[health.status]
2095
- : t("factoryRoute.automationHealthNoData");
2096
- const hasNoHeartbeat =
2097
- !isError &&
2098
- (!health || health.status === "no-data") &&
2099
- !health?.lastCheckedAt;
2100
- const healthDescription = isError
2101
- ? `${t("factoryRoute.automationDiagnosticsLoadError")} ${error instanceof Error ? error.message : String(error)}`
2102
- : health?.lastError
2103
- ? `${t("factoryRoute.automationHealthErrorDetail")}: ${health.lastError}`
2104
- : health?.status === "stale"
2105
- ? t("factoryRoute.automationHealthStaleHint")
2106
- : hasNoHeartbeat
2107
- ? t("factoryRoute.automationHealthNoDataHint")
2108
- : undefined;
2109
-
2110
- return (
2111
- <SettingsGroup variant="soft">
2112
- <SettingsRow
2113
- label={t("factoryRoute.automationHealthTitle")}
2114
- description={
2115
- healthDescription ?? t("factoryRoute.automationHealthDescription")
2116
- }
2117
- control={
2118
- <div className="flex flex-wrap items-center justify-end gap-x-4 gap-y-1">
2119
- <span className="rounded-full bg-muted px-2.5 py-1 text-xs font-medium">
2120
- {healthLabel}
2121
- </span>
2122
- {health?.lastCheckedAt && (
2123
- <span className="text-xs text-muted-foreground">
2124
- {t("factoryRoute.automationLastCheck")}:{" "}
2125
- {formatAutomationDate(health.lastCheckedAt)}
2126
- </span>
2127
- )}
2128
- {health?.lastDispatchedAt && (
2129
- <span className="text-xs text-muted-foreground">
2130
- {t("factoryRoute.automationLastDispatch")}:{" "}
2131
- {formatAutomationDate(health.lastDispatchedAt)}
2132
- </span>
2133
- )}
2134
- </div>
2135
- }
2136
- />
2137
- </SettingsGroup>
2138
- );
2139
- }
2140
-
2141
1851
  function ErrorState({
2142
1852
  message,
2143
1853
  onRetry,
@@ -14,6 +14,14 @@ export interface SlackMessage {
14
14
  thread_ts?: string;
15
15
  reply_count?: number;
16
16
  permalink?: string;
17
+ reactions?: Array<{ name: string; count?: number; users?: string[] }>;
18
+ }
19
+
20
+ export interface SlackAuthTestResult {
21
+ userId: string;
22
+ userName: string;
23
+ teamId: string;
24
+ teamName: string;
17
25
  }
18
26
 
19
27
  export interface SlackTeamInfo {
@@ -45,6 +53,10 @@ export interface SlackReactionResult {
45
53
  already_present: boolean;
46
54
  }
47
55
 
56
+ export interface SlackReactionState {
57
+ eyesPresent: boolean;
58
+ }
59
+
48
60
  export interface SlackPostMessageResult {
49
61
  ok?: boolean;
50
62
  error?: string;
@@ -219,6 +231,56 @@ export async function getThread(
219
231
  };
220
232
  }
221
233
 
234
+ export async function authTest(
235
+ workspace: Workspace,
236
+ tokenResolver?: SlackTokenResolver,
237
+ ): Promise<SlackAuthTestResult> {
238
+ const data = await slackApi<{
239
+ user_id?: string;
240
+ user?: string;
241
+ team_id?: string;
242
+ team?: string;
243
+ }>(workspace, "auth.test", undefined, tokenResolver);
244
+ if (
245
+ typeof data.user_id !== "string" ||
246
+ typeof data.user !== "string" ||
247
+ typeof data.team_id !== "string" ||
248
+ typeof data.team !== "string"
249
+ ) {
250
+ throw new Error("Slack auth.test response is missing bot identity.");
251
+ }
252
+ return {
253
+ userId: data.user_id,
254
+ userName: data.user,
255
+ teamId: data.team_id,
256
+ teamName: data.team,
257
+ };
258
+ }
259
+
260
+ export async function getEyesReaction(
261
+ workspace: Workspace,
262
+ channelId: string,
263
+ timestamp: string,
264
+ tokenResolver?: SlackTokenResolver,
265
+ ): Promise<SlackReactionState> {
266
+ const data = await slackApi<{
267
+ message?: { reactions?: Array<{ name?: string; count?: number }> };
268
+ }>(
269
+ workspace,
270
+ "reactions.get",
271
+ { channel: channelId, timestamp },
272
+ tokenResolver,
273
+ );
274
+ if (!data.message) {
275
+ throw new Error("Slack reaction response is missing the message.");
276
+ }
277
+ return {
278
+ eyesPresent: (data.message.reactions ?? []).some(
279
+ (reaction) => reaction.name === "eyes" && (reaction.count ?? 0) > 0,
280
+ ),
281
+ };
282
+ }
283
+
222
284
  export async function addEyesReaction(
223
285
  workspace: Workspace,
224
286
  channelId: string,
@@ -110,6 +110,7 @@ export const triageConfig = table("factory_config", {
110
110
  slackWorkspace: text("slack_workspace").notNull().default("primary"),
111
111
  slackChannelId: text("slack_channel_id"),
112
112
  slackChannelName: text("slack_channel_name"),
113
+ builderSlackUserId: text("builder_slack_user_id"),
113
114
  pollingEnabled: integer("polling_enabled").notNull().default(0),
114
115
  lastSlackTs: text("last_slack_ts"),
115
116
  slackHistoryCursor: text("slack_history_cursor"),
@@ -0,0 +1,30 @@
1
+ export const SLACK_MENTION_GUARD =
2
+ "Never post Slack messages, reactions, or plaintext @handles yourself. Call start-builder-for-item; that action pings Builder with a Slack user id. Plaintext @builder.io does not notify anyone.";
3
+
4
+ export const SLACK_HANDOFF_INSTRUCTION = `Do not post to Slack, add reactions, or type @handles yourself. Call
5
+ start-builder-for-item; that action adds 👀 and pings Builder with a Slack
6
+ user id so it runs /address-feedback. The posted reply points Builder at the
7
+ relevant repository skills, the representative source, every related source,
8
+ and the need to fix the underlying boundary across the whole cluster. Never
9
+ call that action for owner-managed Clips, Design, or Content work, or for a
10
+ non-bug report.`;
11
+
12
+ const OBSOLETE_SLACK_TAG_PARAGRAPH =
13
+ /The Builder reply must tag @builder\.io[\s\S]*?non-bug\s+report\.\s*/;
14
+
15
+ export function repairSlackFeedbackPrompt(content: string): string {
16
+ let next = content.replace(
17
+ OBSOLETE_SLACK_TAG_PARAGRAPH,
18
+ `${SLACK_HANDOFF_INSTRUCTION}\n\n`,
19
+ );
20
+ if (/tag @builder\.io/i.test(next)) {
21
+ next = next
22
+ .split("\n")
23
+ .filter((line) => !/tag @builder\.io/i.test(line))
24
+ .join("\n");
25
+ }
26
+ if (!next.includes(SLACK_MENTION_GUARD)) {
27
+ next = `${next.trimEnd()}\n\n${SLACK_MENTION_GUARD}\n`;
28
+ }
29
+ return next;
30
+ }
@@ -92,14 +92,15 @@ thread-preserving start-builder-for-item flow; GitHub and Sentry clear bugs use
92
92
  the Builder agent-run flow. Slack repeat reports must be clustered by underlying
93
93
  symptom, with one Builder thread for the cluster and 👀 on every grouped report.
94
94
  After classifying an item, call start-builder-for-item with clearBug true or
95
- false and a short reason so a skip is recorded. Use /address-feedback for the
96
- repository feedback workflow. For pull requests, follow review-prs: read the
97
- complete diff and review evidence, verify current BuilderIO membership, preserve
98
- the ultra-scary safety gate, and distinguish unknown or unresolved checks from
99
- clean ones. Auto-merge also requires a verified Factory Builder run. When a user
100
- says to do a review-gated item now, use the explicit approval action, which
101
- records the approver and applies the rule's configured executor policy. Keep
102
- Slack replies concise and link to the
95
+ false and a short reason so a skip is recorded. Do not post Slack messages,
96
+ reactions, or @mentions yourself; start-builder-for-item owns the Builder ping.
97
+ Use /address-feedback for the repository feedback workflow. For pull requests,
98
+ follow review-prs: read the complete diff and review evidence, verify current
99
+ BuilderIO membership, preserve the ultra-scary safety gate, and distinguish
100
+ unknown or unresolved checks from clean ones. Auto-merge also requires a
101
+ verified Factory Builder run. When a user says to do a review-gated item now,
102
+ use the explicit approval action, which records the approver and applies the
103
+ rule's configured executor policy. Keep Slack replies concise and link to the
103
104
  Factory item when a review is needed. The scheduled builder-io-bot PR babysitter
104
105
  posts its exact feedback-fix request through GitHub, persists a 20-minute quiet
105
106
  window, and never approves or merges.`,
@@ -287,6 +287,13 @@ const migrations = [
287
287
  ON factory_audit_events (org_id, item_id, created_at);
288
288
  `,
289
289
  },
290
+ {
291
+ version: 19,
292
+ name: "factory-config-builder-slack-user-id",
293
+ sql: `
294
+ ALTER TABLE factory_config ADD COLUMN builder_slack_user_id TEXT;
295
+ `,
296
+ },
290
297
  ];
291
298
 
292
299
  export const runFactoryMigrations = runMigrations(migrations, {