@agent-native/core 0.168.7 → 0.168.10

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 (91) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/mutate-dashboard.ts +8 -1
  3. package/corpus/templates/calendar/actions/delete-event.ts +34 -18
  4. package/corpus/templates/calendar/actions/delete-events.ts +13 -1
  5. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +22 -3
  6. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +19 -0
  7. package/corpus/templates/calendar/server/lib/google-api.ts +18 -1
  8. package/corpus/templates/clips/actions/finalize-meeting.ts +16 -0
  9. package/corpus/templates/clips/actions/list-meetings.ts +1 -0
  10. package/corpus/templates/clips/actions/sync-calendars.ts +10 -29
  11. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +28 -12
  12. package/corpus/templates/clips/desktop/src/lib/meeting-stop.ts +16 -0
  13. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +19 -9
  14. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +1 -0
  15. package/corpus/templates/content/app/global.css +5 -0
  16. package/corpus/templates/content/docs/solutions/2026-08-20-code-block-slash-production-regression-shape.md +492 -0
  17. package/corpus/templates/content/docs/solutions/2026-08-20-code-block-surface-background-shape.md +273 -0
  18. package/corpus/templates/factory/.agents/skills/factory-graphs/SKILL.md +5 -0
  19. package/corpus/templates/factory/AGENTS.md +12 -22
  20. package/corpus/templates/factory/README.md +11 -8
  21. package/corpus/templates/factory/actions/approve-factory-item.ts +35 -8
  22. package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +24 -9
  23. package/corpus/templates/factory/actions/create-factory.ts +211 -0
  24. package/corpus/templates/factory/actions/evaluate-triage-item.ts +29 -5
  25. package/corpus/templates/factory/actions/get-factory-graph.ts +1 -1
  26. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -11
  27. package/corpus/templates/factory/actions/get-triage-config.ts +13 -12
  28. package/corpus/templates/factory/actions/get-triage-item.ts +28 -6
  29. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +33 -17
  30. package/corpus/templates/factory/actions/ingest-github-observation.ts +10 -4
  31. package/corpus/templates/factory/actions/list-factory-audit.ts +11 -4
  32. package/corpus/templates/factory/actions/list-factory-automations.ts +34 -12
  33. package/corpus/templates/factory/actions/list-triage-items.ts +11 -3
  34. package/corpus/templates/factory/actions/list-triage-rules.ts +11 -4
  35. package/corpus/templates/factory/actions/poll-github-sources.ts +24 -10
  36. package/corpus/templates/factory/actions/poll-sentry-errors.ts +26 -11
  37. package/corpus/templates/factory/actions/poll-slack-channel.ts +31 -13
  38. package/corpus/templates/factory/actions/reconcile-triage-run.ts +59 -9
  39. package/corpus/templates/factory/actions/record-triage-feedback.ts +14 -3
  40. package/corpus/templates/factory/actions/run-factory-automation.ts +12 -2
  41. package/corpus/templates/factory/actions/save-factory-automation.ts +82 -21
  42. package/corpus/templates/factory/actions/save-factory-graph.ts +10 -1
  43. package/corpus/templates/factory/actions/save-triage-config.ts +90 -36
  44. package/corpus/templates/factory/actions/save-triage-rule.ts +16 -3
  45. package/corpus/templates/factory/actions/start-builder-for-item.ts +28 -20
  46. package/corpus/templates/factory/actions/suggest-factory-rules.ts +11 -4
  47. package/corpus/templates/factory/actions/view-screen.ts +22 -30
  48. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +7 -2
  49. package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +7 -22
  50. package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +260 -0
  51. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  52. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +2 -1
  53. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -3
  54. package/corpus/templates/factory/app/i18n/en-US.ts +18 -1
  55. package/corpus/templates/factory/app/routes/factory.tsx +78 -62
  56. package/corpus/templates/factory/app/routes/{factory-settings.tsx → new-factory.tsx} +12 -9
  57. package/corpus/templates/factory/server/db/schema.ts +18 -3
  58. package/corpus/templates/factory/server/factory-graph/contracts.ts +24 -0
  59. package/corpus/templates/factory/server/factory-graph/store.ts +16 -9
  60. package/corpus/templates/factory/server/lib/factory-automation-plan.ts +136 -0
  61. package/corpus/templates/factory/server/lib/factory-automation-repair.ts +31 -0
  62. package/corpus/templates/factory/server/lib/factory-scope.ts +416 -0
  63. package/corpus/templates/factory/server/lib/require-factory-automation.ts +11 -0
  64. package/corpus/templates/factory/server/plugins/agent-chat.ts +4 -2
  65. package/corpus/templates/factory/server/plugins/factory-migrations.ts +80 -0
  66. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +142 -43
  67. package/corpus/templates/factory/server/triage/audit.ts +4 -0
  68. package/corpus/templates/factory/server/triage/ids.ts +4 -0
  69. package/dist/cli/native-dependencies.js +4 -1
  70. package/dist/client/AgentPanel.d.ts +5 -1
  71. package/dist/client/AgentPanel.js +9 -4
  72. package/dist/client/agent-chat/index.d.ts +1 -1
  73. package/dist/client/agent-chat/index.js +1 -1
  74. package/dist/client/chat-first/apps-rail.js +6 -6
  75. package/dist/client/chat-first/primary-nav.js +14 -6
  76. package/dist/client/integrations/IntegrationsPanel.d.ts +15 -0
  77. package/dist/client/integrations/IntegrationsPanel.js +29 -21
  78. package/dist/client/integrations/index.d.ts +1 -1
  79. package/dist/client/integrations/index.js +1 -1
  80. package/dist/client/resources/McpIntegrationDialog.js +1 -1
  81. package/dist/collab/struct-routes.d.ts +1 -1
  82. package/dist/deploy/build.js +1 -1
  83. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  84. package/dist/observability/routes.d.ts +3 -3
  85. package/dist/progress/routes.d.ts +1 -1
  86. package/dist/secrets/routes.d.ts +6 -6
  87. package/dist/server/realtime-token.d.ts +1 -1
  88. package/dist/server/transcribe-voice.d.ts +1 -1
  89. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  90. package/dist/shared/mcp-embed-headers.js +10 -3
  91. package/package.json +1 -1
@@ -4,11 +4,16 @@ import { z } from "zod";
4
4
 
5
5
  import { getDb } from "../server/db/index.js";
6
6
  import {
7
- triageConfig,
8
7
  triageDecisions,
9
8
  triageItems,
10
9
  triageRuns,
11
10
  } from "../server/db/schema.js";
11
+ import {
12
+ DEFAULT_FACTORY_ID,
13
+ orgFactoryItemFilter,
14
+ orgFactoryRunFilter,
15
+ } from "../server/lib/factory-scope.js";
16
+ import { readTriageConfigRow } from "../server/lib/factory-scope.js";
12
17
  import { requireFactoryAutomation } from "../server/lib/require-factory-automation.js";
13
18
  import {
14
19
  requireWorkspaceMember,
@@ -188,6 +193,7 @@ export async function recordAutomaticBuilderDecision(input: {
188
193
  itemId: string;
189
194
  userEmail: string;
190
195
  orgId: string;
196
+ factoryId: string;
191
197
  outcome: "propose_fix" | "needs_manual";
192
198
  reason: string;
193
199
  guardResults: Array<{ code: string; passed: boolean; reason: string }>;
@@ -214,6 +220,7 @@ export async function recordAutomaticBuilderDecision(input: {
214
220
  createdAt: now,
215
221
  ownerEmail: input.userEmail,
216
222
  orgId: input.orgId,
223
+ factoryId: input.factoryId,
217
224
  })
218
225
  .onConflictDoUpdate({
219
226
  target: triageDecisions.id,
@@ -260,11 +267,6 @@ export default defineAction({
260
267
  const { userEmail, orgId } = await requireWorkspaceMember(
261
268
  workspaceMemberIdentityFromContext(context),
262
269
  );
263
- await requireFactoryAutomation(
264
- context,
265
- { userEmail, orgId },
266
- "builderDispatch",
267
- );
268
270
  const db = getDb();
269
271
  const item = (
270
272
  await db
@@ -274,6 +276,13 @@ export default defineAction({
274
276
  .limit(1)
275
277
  )[0];
276
278
  if (!item) throw new Error("Factory item not found.");
279
+ const factoryId = item.factoryId ?? DEFAULT_FACTORY_ID;
280
+ await requireFactoryAutomation(
281
+ context,
282
+ { userEmail, orgId },
283
+ "builderDispatch",
284
+ factoryId,
285
+ );
277
286
  const metadata = parseTriageMetadata(item.metadataJson);
278
287
  const relatedIds = [...new Set(relatedItemIds)].filter(
279
288
  (relatedId) => relatedId !== itemId,
@@ -285,7 +294,7 @@ export default defineAction({
285
294
  .from(triageItems)
286
295
  .where(
287
296
  and(
288
- eq(triageItems.orgId, orgId),
297
+ orgFactoryItemFilter(orgId, factoryId),
289
298
  inArray(triageItems.id, relatedIds),
290
299
  ),
291
300
  )
@@ -319,7 +328,7 @@ export default defineAction({
319
328
  .from(triageRuns)
320
329
  .where(
321
330
  and(
322
- eq(triageRuns.orgId, orgId),
331
+ orgFactoryRunFilter(orgId, factoryId),
323
332
  inArray(triageRuns.itemId, relatedIds),
324
333
  ),
325
334
  )
@@ -385,6 +394,7 @@ export default defineAction({
385
394
  itemId,
386
395
  userEmail,
387
396
  orgId,
397
+ factoryId,
388
398
  outcome: blocked ? "needs_manual" : "propose_fix",
389
399
  reason,
390
400
  guardResults,
@@ -395,6 +405,7 @@ export default defineAction({
395
405
  {
396
406
  action: "start-builder-for-item",
397
407
  kind: "decision",
408
+ factoryId,
398
409
  itemId,
399
410
  source: item.source,
400
411
  sourceUrl: item.sourceUrl,
@@ -483,6 +494,7 @@ export default defineAction({
483
494
  error: null,
484
495
  ownerEmail: item.ownerEmail,
485
496
  orgId,
497
+ factoryId: item.factoryId ?? DEFAULT_FACTORY_ID,
486
498
  });
487
499
  }
488
500
 
@@ -493,18 +505,11 @@ export default defineAction({
493
505
  "Slack feedback is missing its channel or thread identity.",
494
506
  );
495
507
  }
496
- const config = (
497
- await db
498
- .select({
499
- slackWorkspace: triageConfig.slackWorkspace,
500
- builderSlackUserId: triageConfig.builderSlackUserId,
501
- })
502
- .from(triageConfig)
503
- .where(
504
- and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)),
505
- )
506
- .limit(1)
507
- )[0];
508
+ const config = await readTriageConfigRow(
509
+ db,
510
+ orgId,
511
+ item.factoryId ?? DEFAULT_FACTORY_ID,
512
+ );
508
513
  const workspace =
509
514
  config?.slackWorkspace === "secondary" ? "secondary" : "primary";
510
515
  const builderSlackUserId = requireBuilderSlackUserId(
@@ -638,6 +643,7 @@ export default defineAction({
638
643
  itemId: related.id,
639
644
  userEmail,
640
645
  orgId,
646
+ factoryId: related.factoryId ?? factoryId,
641
647
  outcome: "propose_fix",
642
648
  reason: `Grouped with Factory item ${itemId}: ${reason}`,
643
649
  guardResults,
@@ -656,6 +662,7 @@ export default defineAction({
656
662
  {
657
663
  action: "start-builder-for-item",
658
664
  kind: "external_action",
665
+ factoryId,
659
666
  itemId,
660
667
  source: item.source,
661
668
  sourceUrl: item.sourceUrl,
@@ -728,6 +735,7 @@ export default defineAction({
728
735
  {
729
736
  action: "start-builder-for-item",
730
737
  kind: "external_action",
738
+ factoryId,
731
739
  itemId,
732
740
  source: item.source,
733
741
  sourceUrl: item.sourceUrl,
@@ -1,9 +1,15 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
- import { desc, eq } from "drizzle-orm";
2
+ import { desc } from "drizzle-orm";
3
3
  import { z } from "zod";
4
4
 
5
5
  import { getDb } from "../server/db/index.js";
6
6
  import { triageDecisions, triageFeedback } from "../server/db/schema.js";
7
+ import { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
8
+ import {
9
+ factoryIdSchema,
10
+ orgFactoryDecisionFilter,
11
+ orgFactoryFeedbackFilter,
12
+ } from "../server/lib/factory-scope.js";
7
13
  import {
8
14
  requireWorkspaceMember,
9
15
  workspaceMemberIdentityFromContext,
@@ -13,11 +19,12 @@ export default defineAction({
13
19
  description:
14
20
  "Mine human feedback on Factory decisions into inspectable rule proposals. This never edits a rule or enables execution.",
15
21
  schema: z.object({
22
+ factoryId: factoryIdSchema.default(DEFAULT_FACTORY_ID),
16
23
  limit: z.coerce.number().int().min(1).max(50).default(10),
17
24
  }),
18
25
  http: { method: "GET" },
19
26
  readOnly: true,
20
- run: async ({ limit }, context) => {
27
+ run: async ({ factoryId, limit }, context) => {
21
28
  const { orgId } = await requireWorkspaceMember(
22
29
  workspaceMemberIdentityFromContext(context),
23
30
  );
@@ -25,13 +32,13 @@ export default defineAction({
25
32
  const decisions = await db
26
33
  .select()
27
34
  .from(triageDecisions)
28
- .where(eq(triageDecisions.orgId, orgId))
35
+ .where(orgFactoryDecisionFilter(orgId, factoryId))
29
36
  .orderBy(desc(triageDecisions.createdAt))
30
37
  .limit(500);
31
38
  const feedback = await db
32
39
  .select()
33
40
  .from(triageFeedback)
34
- .where(eq(triageFeedback.orgId, orgId))
41
+ .where(orgFactoryFeedbackFilter(orgId, factoryId))
35
42
  .orderBy(desc(triageFeedback.createdAt))
36
43
  .limit(500);
37
44
  const decisionsById = new Map(
@@ -41,18 +41,6 @@ export default defineAction({
41
41
  const screen: Record<string, unknown> = {};
42
42
  if (navigation) screen.navigation = navigation;
43
43
 
44
- if (
45
- navigation &&
46
- typeof navigation === "object" &&
47
- "view" in navigation &&
48
- navigation.view === "factory-settings"
49
- ) {
50
- screen.factorySettings = {
51
- scope: "workspace",
52
- path: "/factory-settings",
53
- };
54
- }
55
-
56
44
  if (
57
45
  navigation &&
58
46
  typeof navigation === "object" &&
@@ -63,25 +51,29 @@ export default defineAction({
63
51
  workspaceMemberIdentityFromContext(context),
64
52
  );
65
53
  const state = navigation as Record<string, unknown>;
66
- const factoryId =
67
- typeof state.factoryId === "string" && state.factoryId.trim()
68
- ? state.factoryId
69
- : DEFAULT_FACTORY_ID;
70
- const row = await readFactoryDefinition(orgId, factoryId);
71
- const fallback = defaultFactoryDefinition();
72
- const graph = row ? parseFactoryGraph(row.graphJson) : fallback.graph;
73
- const selectedNodeId =
74
- typeof state.factoryNodeId === "string" ? state.factoryNodeId : null;
75
- const selectedEdgeId =
76
- typeof state.factoryEdgeId === "string" ? state.factoryEdgeId : null;
54
+ if (state.creatingFactory === true) {
55
+ screen.factory = { creating: true };
56
+ } else {
57
+ const factoryId =
58
+ typeof state.factoryId === "string" && state.factoryId.trim()
59
+ ? state.factoryId
60
+ : DEFAULT_FACTORY_ID;
61
+ const row = await readFactoryDefinition(orgId, factoryId);
62
+ const fallback = defaultFactoryDefinition();
63
+ const graph = row ? parseFactoryGraph(row.graphJson) : fallback.graph;
64
+ const selectedNodeId =
65
+ typeof state.factoryNodeId === "string" ? state.factoryNodeId : null;
66
+ const selectedEdgeId =
67
+ typeof state.factoryEdgeId === "string" ? state.factoryEdgeId : null;
77
68
 
78
- screen.factory = {
79
- id: factoryId,
80
- name: row?.name ?? fallback.name,
81
- graphVersion: row?.graphVersion ?? graph.version,
82
- selectedNode: graph.nodes.find((node) => node.id === selectedNodeId),
83
- selectedEdge: graph.edges.find((edge) => edge.id === selectedEdgeId),
84
- };
69
+ screen.factory = {
70
+ id: factoryId,
71
+ name: row?.name ?? fallback.name,
72
+ graphVersion: row?.graphVersion ?? graph.version,
73
+ selectedNode: graph.nodes.find((node) => node.id === selectedNodeId),
74
+ selectedEdge: graph.edges.find((edge) => edge.id === selectedEdgeId),
75
+ };
76
+ }
85
77
 
86
78
  if (state.factoryTab === "agents") {
87
79
  const [apps, agents] = await Promise.all([
@@ -49,7 +49,11 @@ function formatAutomationDate(value: string | number | null | undefined) {
49
49
  return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
50
50
  }
51
51
 
52
- export function FactorySettingsView() {
52
+ export function FactorySettingsView({
53
+ factoryId = "product-feedback",
54
+ }: {
55
+ factoryId?: string;
56
+ }) {
53
57
  const t = useT();
54
58
  const [workspace, setWorkspace] = useState<"primary" | "secondary">(
55
59
  "primary",
@@ -68,7 +72,7 @@ export function FactorySettingsView() {
68
72
  useState(true);
69
73
  const [automationFailureAlertEmail, setAutomationFailureAlertEmail] =
70
74
  useState("");
71
- const query = useActionQuery("get-triage-config", {});
75
+ const query = useActionQuery("get-triage-config", { factoryId });
72
76
  const schedulerHealthQuery = useActionQuery<FactoryAutomationHealth>(
73
77
  "get-factory-automation-health",
74
78
  {},
@@ -105,6 +109,7 @@ export function FactorySettingsView() {
105
109
  }
106
110
  try {
107
111
  await mutation.mutateAsync({
112
+ factoryId,
108
113
  slackWorkspace: workspace,
109
114
  slackChannelId: channelId,
110
115
  slackChannelName: channelName,
@@ -6,14 +6,9 @@ import { Link, useLocation, useSearchParams } from "react-router";
6
6
  import { Button } from "@/components/ui/button";
7
7
 
8
8
  export const FACTORY_AGENTS_HREF = "/factory?tab=agents";
9
- export const FACTORY_SETTINGS_HREF = "/factory-settings";
10
- export const FACTORY_NEW_HREF = "/factory?new=1";
9
+ export const NEW_FACTORY_HREF = "/new-factory";
11
10
 
12
- export function FactoryWorkspaceActions({
13
- onNewFactory,
14
- }: {
15
- onNewFactory?: () => void;
16
- }) {
11
+ export function FactoryWorkspaceActions() {
17
12
  const t = useT();
18
13
  const location = useLocation();
19
14
  const [searchParams] = useSearchParams();
@@ -22,31 +17,21 @@ export function FactoryWorkspaceActions({
22
17
  (location.pathname === "/factory" &&
23
18
  searchParams.get("tab") === "agents" &&
24
19
  !searchParams.get("factoryId"));
25
- const onSettings = location.pathname === "/factory-settings";
20
+ const onNewFactoryPage = location.pathname === "/new-factory";
26
21
 
27
22
  return (
28
23
  <div className="flex items-center gap-2">
29
24
  <Button asChild variant={onAgents ? "default" : "outline"} size="sm">
30
25
  <Link to={FACTORY_AGENTS_HREF}>{t("factoryRoute.agentsTab")}</Link>
31
26
  </Button>
32
- <Button asChild variant={onSettings ? "default" : "outline"} size="sm">
33
- <Link to={FACTORY_SETTINGS_HREF}>
34
- {t("factoryRoute.factorySettings")}
35
- </Link>
36
- </Button>
37
- {onNewFactory ? (
38
- <Button type="button" size="sm" onClick={onNewFactory}>
39
- <IconPlus className="size-4" />
40
- {t("factoryRoute.newFactory")}
41
- </Button>
42
- ) : (
43
- <Button asChild size="sm">
44
- <Link to={FACTORY_NEW_HREF}>
27
+ {!onNewFactoryPage ? (
28
+ <Button asChild variant="outline" size="sm">
29
+ <Link to={NEW_FACTORY_HREF}>
45
30
  <IconPlus className="size-4" />
46
31
  {t("factoryRoute.newFactory")}
47
32
  </Link>
48
33
  </Button>
49
- )}
34
+ ) : null}
50
35
  <AgentToggleButton />
51
36
  </div>
52
37
  );
@@ -0,0 +1,260 @@
1
+ import { useActionMutation } from "@agent-native/core/client/hooks";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import {
4
+ IconAlertCircle,
5
+ IconBrandGithub,
6
+ IconBrandSlack,
7
+ IconLoader2,
8
+ } from "@tabler/icons-react";
9
+ import { useState } from "react";
10
+ import { Link } from "react-router";
11
+ import { toast } from "sonner";
12
+
13
+ import { Button } from "@/components/ui/button";
14
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
15
+ import { Input } from "@/components/ui/input";
16
+ import { Label } from "@/components/ui/label";
17
+ import { Switch } from "@/components/ui/switch";
18
+ import { Textarea } from "@/components/ui/textarea";
19
+
20
+ type CreateFactoryResult = {
21
+ ok: boolean;
22
+ factoryId: string;
23
+ name: string;
24
+ };
25
+
26
+ export function NewFactoryForm({
27
+ onCreated,
28
+ }: {
29
+ onCreated: (factoryId: string) => void;
30
+ }) {
31
+ const t = useT();
32
+ const createMutation = useActionMutation("create-factory");
33
+ const [name, setName] = useState("");
34
+ const [description, setDescription] = useState("");
35
+ const [slackChannelId, setSlackChannelId] = useState("");
36
+ const [slackChannelName, setSlackChannelName] = useState("");
37
+ const [observeSlack, setObserveSlack] = useState(false);
38
+ const [repository, setRepository] = useState("");
39
+ const [observeGithub, setObserveGithub] = useState(false);
40
+ const [sentryOrgSlug, setSentryOrgSlug] = useState("");
41
+ const [sentryProjectSlug, setSentryProjectSlug] = useState("");
42
+ const [sentryEnvironment, setSentryEnvironment] = useState("");
43
+ const [observeSentry, setObserveSentry] = useState(false);
44
+
45
+ async function handleSubmit(event: React.FormEvent) {
46
+ event.preventDefault();
47
+ const trimmedName = name.trim();
48
+ if (!trimmedName) {
49
+ toast.error(t("factoryRoute.createFactoryNameRequired"));
50
+ return;
51
+ }
52
+ try {
53
+ const result = (await createMutation.mutateAsync({
54
+ name: trimmedName,
55
+ ...(description.trim() ? { description: description.trim() } : {}),
56
+ ...(slackChannelId.trim()
57
+ ? { slackChannelId: slackChannelId.trim() }
58
+ : {}),
59
+ ...(slackChannelName.trim()
60
+ ? { slackChannelName: slackChannelName.trim() }
61
+ : {}),
62
+ ...(observeSlack ? { observeSlack: true } : {}),
63
+ ...(repository.trim() ? { repository: repository.trim() } : {}),
64
+ ...(observeGithub ? { observeGithub: true } : {}),
65
+ ...(sentryOrgSlug.trim()
66
+ ? { sentryOrgSlug: sentryOrgSlug.trim() }
67
+ : {}),
68
+ ...(sentryProjectSlug.trim()
69
+ ? { sentryProjectSlug: sentryProjectSlug.trim() }
70
+ : {}),
71
+ ...(sentryEnvironment.trim()
72
+ ? { sentryEnvironment: sentryEnvironment.trim() }
73
+ : {}),
74
+ ...(observeSentry ? { observeSentry: true } : {}),
75
+ })) as CreateFactoryResult;
76
+ toast.success(t("factoryRoute.createFactorySuccess"));
77
+ onCreated(result.factoryId);
78
+ } catch (error) {
79
+ toast.error(
80
+ error instanceof Error
81
+ ? error.message
82
+ : t("factoryRoute.createFactoryFailed"),
83
+ );
84
+ }
85
+ }
86
+
87
+ return (
88
+ <form
89
+ className="mx-auto flex w-full max-w-2xl flex-col gap-6"
90
+ onSubmit={(event) => void handleSubmit(event)}
91
+ >
92
+ <div className="space-y-2">
93
+ <Label htmlFor="new-factory-name">
94
+ {t("factoryRoute.createFactoryNameLabel")}
95
+ </Label>
96
+ <Input
97
+ id="new-factory-name"
98
+ value={name}
99
+ onChange={(event) => setName(event.target.value)}
100
+ placeholder={t("factoryRoute.newFactory")}
101
+ autoFocus
102
+ />
103
+ </div>
104
+ <div className="space-y-2">
105
+ <Label htmlFor="new-factory-description">
106
+ {t("factoryRoute.createFactoryDescriptionLabel")}
107
+ </Label>
108
+ <Textarea
109
+ id="new-factory-description"
110
+ value={description}
111
+ onChange={(event) => setDescription(event.target.value)}
112
+ rows={2}
113
+ />
114
+ </div>
115
+ <div className="space-y-4">
116
+ <Card>
117
+ <CardHeader className="px-4 pb-0 pt-4">
118
+ <CardTitle className="flex items-center gap-2 text-base">
119
+ <IconBrandSlack className="size-4" />
120
+ {t("factoryRoute.createFactorySlackSourceOptional")}
121
+ </CardTitle>
122
+ </CardHeader>
123
+ <CardContent className="space-y-3 px-4 pb-4">
124
+ <div className="space-y-2">
125
+ <Label htmlFor="new-factory-slack-channel">
126
+ {t("triage.slackChannelId")}
127
+ </Label>
128
+ <Input
129
+ id="new-factory-slack-channel"
130
+ value={slackChannelId}
131
+ onChange={(event) => setSlackChannelId(event.target.value)}
132
+ placeholder={t("triage.slackChannelPlaceholder")}
133
+ />
134
+ </div>
135
+ <div className="space-y-2">
136
+ <Label htmlFor="new-factory-slack-channel-name">
137
+ {t("triage.slackChannelName")}
138
+ </Label>
139
+ <Input
140
+ id="new-factory-slack-channel-name"
141
+ value={slackChannelName}
142
+ onChange={(event) => setSlackChannelName(event.target.value)}
143
+ placeholder={t("triage.slackChannelNamePlaceholder")}
144
+ />
145
+ </div>
146
+ <div className="flex items-center justify-between gap-3">
147
+ <Label htmlFor="new-factory-observe-slack">
148
+ {t("factoryRoute.createFactoryObserveSlack")}
149
+ </Label>
150
+ <Switch
151
+ id="new-factory-observe-slack"
152
+ checked={observeSlack}
153
+ onCheckedChange={(checked) => setObserveSlack(checked === true)}
154
+ />
155
+ </div>
156
+ </CardContent>
157
+ </Card>
158
+ <Card>
159
+ <CardHeader className="px-4 pb-0 pt-4">
160
+ <CardTitle className="flex items-center gap-2 text-base">
161
+ <IconBrandGithub className="size-4" />
162
+ {t("factoryRoute.createFactoryGithubSourceOptional")}
163
+ </CardTitle>
164
+ </CardHeader>
165
+ <CardContent className="space-y-3 px-4 pb-4">
166
+ <div className="space-y-2">
167
+ <Label htmlFor="new-factory-repository">
168
+ {t("triage.repository")}
169
+ </Label>
170
+ <Input
171
+ id="new-factory-repository"
172
+ value={repository}
173
+ onChange={(event) => setRepository(event.target.value)}
174
+ placeholder={t("triage.repositoryPlaceholder")}
175
+ />
176
+ </div>
177
+ <div className="flex items-center justify-between gap-3">
178
+ <Label htmlFor="new-factory-observe-github">
179
+ {t("factoryRoute.createFactoryObserveGithub")}
180
+ </Label>
181
+ <Switch
182
+ id="new-factory-observe-github"
183
+ checked={observeGithub}
184
+ onCheckedChange={(checked) =>
185
+ setObserveGithub(checked === true)
186
+ }
187
+ />
188
+ </div>
189
+ </CardContent>
190
+ </Card>
191
+ <Card>
192
+ <CardHeader className="px-4 pb-0 pt-4">
193
+ <CardTitle className="flex items-center gap-2 text-base">
194
+ <IconAlertCircle className="size-4" />
195
+ {t("factoryRoute.createFactorySentrySourceOptional")}
196
+ </CardTitle>
197
+ </CardHeader>
198
+ <CardContent className="space-y-3 px-4 pb-4">
199
+ <div className="space-y-2">
200
+ <Label htmlFor="new-factory-sentry-org">
201
+ {t("triage.sentryOrgSlug")}
202
+ </Label>
203
+ <Input
204
+ id="new-factory-sentry-org"
205
+ value={sentryOrgSlug}
206
+ onChange={(event) => setSentryOrgSlug(event.target.value)}
207
+ placeholder={t("triage.sentryOrgPlaceholder")}
208
+ />
209
+ </div>
210
+ <div className="space-y-2">
211
+ <Label htmlFor="new-factory-sentry-project">
212
+ {t("triage.sentryProjectSlug")}
213
+ </Label>
214
+ <Input
215
+ id="new-factory-sentry-project"
216
+ value={sentryProjectSlug}
217
+ onChange={(event) => setSentryProjectSlug(event.target.value)}
218
+ placeholder={t("triage.sentryProjectPlaceholder")}
219
+ />
220
+ </div>
221
+ <div className="space-y-2">
222
+ <Label htmlFor="new-factory-sentry-environment">
223
+ {t("triage.sentryEnvironment")}
224
+ </Label>
225
+ <Input
226
+ id="new-factory-sentry-environment"
227
+ value={sentryEnvironment}
228
+ onChange={(event) => setSentryEnvironment(event.target.value)}
229
+ placeholder={t("triage.sentryEnvironmentPlaceholder")}
230
+ />
231
+ </div>
232
+ <div className="flex items-center justify-between gap-3">
233
+ <Label htmlFor="new-factory-observe-sentry">
234
+ {t("factoryRoute.createFactoryObserveSentry")}
235
+ </Label>
236
+ <Switch
237
+ id="new-factory-observe-sentry"
238
+ checked={observeSentry}
239
+ onCheckedChange={(checked) =>
240
+ setObserveSentry(checked === true)
241
+ }
242
+ />
243
+ </div>
244
+ </CardContent>
245
+ </Card>
246
+ </div>
247
+ <div className="flex justify-end gap-2">
248
+ <Button asChild type="button" variant="outline">
249
+ <Link to="/factory">{t("factoryRoute.createFactoryCancel")}</Link>
250
+ </Button>
251
+ <Button type="submit" disabled={createMutation.isPending}>
252
+ {createMutation.isPending ? (
253
+ <IconLoader2 className="size-4 animate-spin" />
254
+ ) : null}
255
+ {t("factoryRoute.createFactorySubmit")}
256
+ </Button>
257
+ </div>
258
+ </form>
259
+ );
260
+ }
@@ -42,7 +42,7 @@ function routeOwnsToolbar(pathname: string): boolean {
42
42
  pathname === "/chat" ||
43
43
  pathname.startsWith("/chat/") ||
44
44
  pathname === "/factory" ||
45
- pathname === "/factory-settings" ||
45
+ pathname === "/new-factory" ||
46
46
  pathname === "/agents" ||
47
47
  pathname === "/database" ||
48
48
  pathname.startsWith("/extensions")
@@ -445,7 +445,8 @@ export function Sidebar({
445
445
  item.href === "/"
446
446
  ? isChatRoute
447
447
  : item.href === "/factory"
448
- ? location.pathname === "/factory"
448
+ ? location.pathname === "/factory" ||
449
+ location.pathname === "/new-factory"
449
450
  : location.pathname.startsWith(item.href);
450
451
  const link = (
451
452
  <Link
@@ -13,6 +13,7 @@ export interface NavigationState {
13
13
  factoryAuditRunId?: string;
14
14
  factoryNodeId?: string;
15
15
  factoryEdgeId?: string;
16
+ creatingFactory?: boolean;
16
17
  }
17
18
 
18
19
  export function useNavigationState() {
@@ -48,6 +49,7 @@ export function useNavigationState() {
48
49
  ...(pathname === "/factory" && searchParams.get("edge")
49
50
  ? { factoryEdgeId: searchParams.get("edge") ?? undefined }
50
51
  : {}),
52
+ ...(pathname === "/new-factory" ? { creatingFactory: true } : {}),
51
53
  };
52
54
  },
53
55
  getCommandPath: (command) =>
@@ -73,7 +75,7 @@ function viewForPath(pathname: string): string {
73
75
  if (pathname.startsWith("/extensions")) return "extensions";
74
76
  if (pathname.startsWith("/observability")) return "observability";
75
77
  if (pathname.startsWith("/agents")) return "agents";
76
- if (pathname === "/factory-settings") return "factory-settings";
78
+ if (pathname === "/new-factory") return "factory";
77
79
  if (pathname === "/factory") return "factory";
78
80
  if (pathname.startsWith("/settings/agent") || pathname.startsWith("/agent")) {
79
81
  return "agent";
@@ -97,8 +99,6 @@ function pathForView(view?: string): string {
97
99
  return "/observability";
98
100
  case "factory":
99
101
  return "/factory";
100
- case "factory-settings":
101
- return "/factory-settings";
102
102
  case "agents":
103
103
  return "/agents";
104
104
  case "agent":