@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
@@ -0,0 +1,457 @@
1
+ import {
2
+ useActionMutation,
3
+ useActionQuery,
4
+ } from "@agent-native/core/client/hooks";
5
+ import { useT } from "@agent-native/core/client/i18n";
6
+ import { buildSettingsRoute } from "@agent-native/core/client/navigation";
7
+ import { SettingsGroup, SettingsRow } from "@agent-native/core/client/settings";
8
+ import { ActionQueryError } from "@agent-native/dispatch/components";
9
+ import { IconLoader2 } from "@tabler/icons-react";
10
+ import { useEffect, useState } from "react";
11
+ import { Link } from "react-router";
12
+ import { toast } from "sonner";
13
+
14
+ import { Button } from "@/components/ui/button";
15
+ import { Input } from "@/components/ui/input";
16
+ import { Switch } from "@/components/ui/switch";
17
+
18
+ type TriageConfig = {
19
+ slackWorkspace?: "primary" | "secondary";
20
+ slackChannelId?: string | null;
21
+ slackChannelName?: string | null;
22
+ builderSlackUserId?: string | null;
23
+ pollingEnabled?: boolean;
24
+ githubPollingEnabled?: boolean;
25
+ sentryPollingEnabled?: boolean;
26
+ sentryOrgSlug?: string | null;
27
+ sentryProjectSlug?: string | null;
28
+ sentryEnvironment?: string | null;
29
+ repository?: string | null;
30
+ automationFailureAlertsEnabled?: boolean;
31
+ automationFailureAlertEmail?: string | null;
32
+ emailReadiness?: {
33
+ status: "ready" | "not-configured" | "misconfigured" | "unavailable";
34
+ provider: string;
35
+ };
36
+ };
37
+
38
+ type FactoryAutomationHealth = {
39
+ status: "healthy" | "stale" | "error" | "no-data";
40
+ lastCheckedAt?: number | null;
41
+ lastDispatchedAt?: number | null;
42
+ lastError?: string | null;
43
+ runtime?: string | null;
44
+ };
45
+
46
+ function formatAutomationDate(value: string | number | null | undefined) {
47
+ if (!value) return "-";
48
+ const date = new Date(value);
49
+ return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
50
+ }
51
+
52
+ export function FactorySettingsView() {
53
+ const t = useT();
54
+ const [workspace, setWorkspace] = useState<"primary" | "secondary">(
55
+ "primary",
56
+ );
57
+ const [channelId, setChannelId] = useState("");
58
+ const [channelName, setChannelName] = useState("");
59
+ const [builderSlackUserId, setBuilderSlackUserId] = useState("");
60
+ const [repository, setRepository] = useState("");
61
+ const [polling, setPolling] = useState(false);
62
+ const [githubPolling, setGithubPolling] = useState(false);
63
+ const [sentryPolling, setSentryPolling] = useState(false);
64
+ const [sentryOrgSlug, setSentryOrgSlug] = useState("");
65
+ const [sentryProjectSlug, setSentryProjectSlug] = useState("");
66
+ const [sentryEnvironment, setSentryEnvironment] = useState("");
67
+ const [automationFailureAlertsEnabled, setAutomationFailureAlertsEnabled] =
68
+ useState(true);
69
+ const [automationFailureAlertEmail, setAutomationFailureAlertEmail] =
70
+ useState("");
71
+ const query = useActionQuery("get-triage-config", {});
72
+ const schedulerHealthQuery = useActionQuery<FactoryAutomationHealth>(
73
+ "get-factory-automation-health",
74
+ {},
75
+ { refetchInterval: 60_000 },
76
+ );
77
+ const mutation = useActionMutation("save-triage-config");
78
+
79
+ useEffect(() => {
80
+ const data = query.data as TriageConfig | undefined;
81
+ if (!data) return;
82
+ setWorkspace(data.slackWorkspace ?? "primary");
83
+ setChannelId(data.slackChannelId ?? "");
84
+ setChannelName(data.slackChannelName ?? "");
85
+ setBuilderSlackUserId(data.builderSlackUserId ?? "");
86
+ setRepository(data.repository ?? "");
87
+ setPolling(data.pollingEnabled ?? false);
88
+ setGithubPolling(data.githubPollingEnabled ?? false);
89
+ setSentryPolling(data.sentryPollingEnabled ?? false);
90
+ setSentryOrgSlug(data.sentryOrgSlug ?? "");
91
+ setSentryProjectSlug(data.sentryProjectSlug ?? "");
92
+ setSentryEnvironment(data.sentryEnvironment ?? "");
93
+ setAutomationFailureAlertsEnabled(
94
+ data.automationFailureAlertsEnabled ?? true,
95
+ );
96
+ setAutomationFailureAlertEmail(data.automationFailureAlertEmail ?? "");
97
+ }, [query.data]);
98
+
99
+ const configLoaded = Boolean(query.data) && !query.isError;
100
+
101
+ const saveSettings = async () => {
102
+ if (!configLoaded) {
103
+ toast.error(t("triage.settingsError"));
104
+ return;
105
+ }
106
+ try {
107
+ await mutation.mutateAsync({
108
+ slackWorkspace: workspace,
109
+ slackChannelId: channelId,
110
+ slackChannelName: channelName,
111
+ builderSlackUserId,
112
+ repository,
113
+ pollingEnabled: polling,
114
+ githubPollingEnabled: githubPolling,
115
+ sentryPollingEnabled: sentryPolling,
116
+ sentryOrgSlug,
117
+ sentryProjectSlug,
118
+ sentryEnvironment,
119
+ automationFailureAlertsEnabled,
120
+ automationFailureAlertEmail: automationFailureAlertEmail.trim(),
121
+ });
122
+ toast.success(t("triage.settingsSaved"));
123
+ } catch (error) {
124
+ toast.error(
125
+ error instanceof Error ? error.message : t("triage.settingsError"),
126
+ );
127
+ }
128
+ };
129
+
130
+ if (query.isLoading) {
131
+ return (
132
+ <div className="mx-auto w-full max-w-3xl space-y-6 p-4 lg:p-6">
133
+ <FactorySettingsSkeleton t={t} />
134
+ </div>
135
+ );
136
+ }
137
+
138
+ if (!configLoaded) {
139
+ return (
140
+ <div className="mx-auto w-full max-w-3xl p-4 lg:p-6">
141
+ <ActionQueryError
142
+ error={query.error}
143
+ onRetry={() => void query.refetch()}
144
+ />
145
+ </div>
146
+ );
147
+ }
148
+
149
+ const fieldControlClass = "h-9 w-full sm:w-64";
150
+
151
+ return (
152
+ <div className="mx-auto w-full max-w-3xl space-y-6 p-4 lg:p-6">
153
+ <SettingsGroup variant="soft">
154
+ <SettingsRow
155
+ label={t("factoryRoute.workspaceIntegrations")}
156
+ description={t("settings.workspaceDescription")}
157
+ control={
158
+ <Button asChild type="button" variant="outline">
159
+ <Link to={buildSettingsRoute("integrations")}>Manage</Link>
160
+ </Button>
161
+ }
162
+ />
163
+ <SettingsRow
164
+ label={t("factoryRoute.agentAccess")}
165
+ description={t("settings.agentDescription")}
166
+ control={
167
+ <Button asChild type="button" variant="outline">
168
+ <Link to={buildSettingsRoute("agent")}>Manage</Link>
169
+ </Button>
170
+ }
171
+ />
172
+ </SettingsGroup>
173
+
174
+ <SettingsGroup variant="soft">
175
+ <SettingsRow
176
+ label={t("triage.slackWorkspace")}
177
+ control={
178
+ <select
179
+ aria-label={t("triage.slackWorkspace")}
180
+ value={workspace}
181
+ onChange={(event) =>
182
+ setWorkspace(event.target.value as "primary" | "secondary")
183
+ }
184
+ className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm sm:w-64"
185
+ >
186
+ <option value="primary">primary</option>
187
+ <option value="secondary">secondary</option>
188
+ </select>
189
+ }
190
+ />
191
+ <SettingsRow
192
+ label={t("triage.slackChannelId")}
193
+ control={
194
+ <Input
195
+ aria-label={t("triage.slackChannelId")}
196
+ value={channelId}
197
+ onChange={(event) => setChannelId(event.target.value)}
198
+ placeholder={t("triage.slackChannelPlaceholder")}
199
+ className={fieldControlClass}
200
+ />
201
+ }
202
+ />
203
+ <SettingsRow
204
+ label={t("triage.slackChannelName")}
205
+ control={
206
+ <Input
207
+ aria-label={t("triage.slackChannelName")}
208
+ value={channelName}
209
+ onChange={(event) => setChannelName(event.target.value)}
210
+ placeholder={t("triage.slackChannelNamePlaceholder")}
211
+ className={fieldControlClass}
212
+ />
213
+ }
214
+ />
215
+ <SettingsRow
216
+ label={t("triage.builderSlackUserId")}
217
+ control={
218
+ <Input
219
+ aria-label={t("triage.builderSlackUserId")}
220
+ value={builderSlackUserId}
221
+ onChange={(event) => setBuilderSlackUserId(event.target.value)}
222
+ placeholder={t("triage.builderSlackUserIdPlaceholder")}
223
+ className={fieldControlClass}
224
+ />
225
+ }
226
+ />
227
+ <SettingsRow
228
+ label={t("triage.repository")}
229
+ control={
230
+ <Input
231
+ aria-label={t("triage.repository")}
232
+ value={repository}
233
+ onChange={(event) => setRepository(event.target.value)}
234
+ placeholder={t("triage.repositoryPlaceholder")}
235
+ className={fieldControlClass}
236
+ />
237
+ }
238
+ />
239
+ <SettingsRow
240
+ label={t("triage.enablePolling")}
241
+ control={
242
+ <Switch
243
+ aria-label={t("triage.enablePolling")}
244
+ checked={polling}
245
+ onCheckedChange={(checked) => setPolling(checked === true)}
246
+ />
247
+ }
248
+ />
249
+ <SettingsRow
250
+ label={t("triage.enableGithubPolling")}
251
+ control={
252
+ <Switch
253
+ aria-label={t("triage.enableGithubPolling")}
254
+ checked={githubPolling}
255
+ onCheckedChange={(checked) => setGithubPolling(checked === true)}
256
+ />
257
+ }
258
+ />
259
+ <SettingsRow
260
+ label={t("triage.sentryOrgSlug")}
261
+ control={
262
+ <Input
263
+ aria-label={t("triage.sentryOrgSlug")}
264
+ value={sentryOrgSlug}
265
+ onChange={(event) => setSentryOrgSlug(event.target.value)}
266
+ placeholder={t("triage.sentryOrgPlaceholder")}
267
+ className={fieldControlClass}
268
+ />
269
+ }
270
+ />
271
+ <SettingsRow
272
+ label={t("triage.sentryProjectSlug")}
273
+ control={
274
+ <Input
275
+ aria-label={t("triage.sentryProjectSlug")}
276
+ value={sentryProjectSlug}
277
+ onChange={(event) => setSentryProjectSlug(event.target.value)}
278
+ placeholder={t("triage.sentryProjectPlaceholder")}
279
+ className={fieldControlClass}
280
+ />
281
+ }
282
+ />
283
+ <SettingsRow
284
+ label={t("triage.sentryEnvironment")}
285
+ control={
286
+ <Input
287
+ aria-label={t("triage.sentryEnvironment")}
288
+ value={sentryEnvironment}
289
+ onChange={(event) => setSentryEnvironment(event.target.value)}
290
+ placeholder={t("triage.sentryEnvironmentPlaceholder")}
291
+ className={fieldControlClass}
292
+ />
293
+ }
294
+ />
295
+ <SettingsRow
296
+ label={t("triage.enableSentryPolling")}
297
+ control={
298
+ <Switch
299
+ aria-label={t("triage.enableSentryPolling")}
300
+ checked={sentryPolling}
301
+ onCheckedChange={(checked) => setSentryPolling(checked === true)}
302
+ />
303
+ }
304
+ />
305
+ </SettingsGroup>
306
+
307
+ <SettingsGroup variant="soft">
308
+ <SettingsRow
309
+ label={t("factoryRoute.automationFailureAlertsTitle")}
310
+ description={t("factoryRoute.automationFailureAlertsDescription")}
311
+ control={
312
+ <Switch
313
+ aria-label={t("factoryRoute.automationFailureAlertsEnabled")}
314
+ checked={automationFailureAlertsEnabled}
315
+ onCheckedChange={(checked) =>
316
+ setAutomationFailureAlertsEnabled(checked === true)
317
+ }
318
+ />
319
+ }
320
+ />
321
+ <SettingsRow
322
+ label={t("factoryRoute.automationFailureAlertEmail")}
323
+ description={t("factoryRoute.automationFailureAlertEmailPlaceholder")}
324
+ control={
325
+ <Input
326
+ aria-label={t("factoryRoute.automationFailureAlertEmail")}
327
+ type="email"
328
+ value={automationFailureAlertEmail}
329
+ onChange={(event) =>
330
+ setAutomationFailureAlertEmail(event.target.value)
331
+ }
332
+ placeholder={t(
333
+ "factoryRoute.automationFailureAlertEmailPlaceholder",
334
+ )}
335
+ className={fieldControlClass}
336
+ />
337
+ }
338
+ />
339
+ <SettingsRow
340
+ label={t("factoryRoute.automationFailureEmailReadiness")}
341
+ description={t("factoryRoute.automationEmailReadinessHint")}
342
+ control={
343
+ <span className="rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
344
+ {(query.data as TriageConfig | undefined)?.emailReadiness
345
+ ?.status ?? "unknown"}
346
+ </span>
347
+ }
348
+ />
349
+ </SettingsGroup>
350
+
351
+ <div className="flex flex-wrap items-center justify-end gap-3">
352
+ <Button
353
+ onClick={() => void saveSettings()}
354
+ disabled={mutation.isPending || !configLoaded}
355
+ >
356
+ {mutation.isPending && <IconLoader2 className="animate-spin" />}
357
+ {t("triage.saveSettings")}
358
+ </Button>
359
+ </div>
360
+
361
+ <SchedulerHealthStatus
362
+ health={schedulerHealthQuery.data}
363
+ isError={schedulerHealthQuery.isError}
364
+ error={schedulerHealthQuery.error}
365
+ t={t}
366
+ />
367
+ </div>
368
+ );
369
+ }
370
+
371
+ function FactorySettingsSkeleton({ t }: { t: ReturnType<typeof useT> }) {
372
+ return (
373
+ <div className="space-y-6" aria-label={t("triage.loading")}>
374
+ {[2, 11, 3].map((rowCount, index) => (
375
+ <div key={index} className="grid gap-2">
376
+ <div className="grid gap-2">
377
+ {Array.from({ length: rowCount }).map((_, rowIndex) => (
378
+ <div
379
+ key={rowIndex}
380
+ className="flex min-h-20 items-center justify-between gap-4 rounded-xl bg-card px-5 py-4 shadow-sm sm:px-6"
381
+ >
382
+ <div className="h-3.5 w-36 animate-pulse rounded bg-muted" />
383
+ <div className="h-9 w-24 animate-pulse rounded bg-muted" />
384
+ </div>
385
+ ))}
386
+ </div>
387
+ </div>
388
+ ))}
389
+ </div>
390
+ );
391
+ }
392
+
393
+ function SchedulerHealthStatus({
394
+ health,
395
+ isError,
396
+ error,
397
+ t,
398
+ }: {
399
+ health?: FactoryAutomationHealth;
400
+ isError: boolean;
401
+ error: unknown;
402
+ t: ReturnType<typeof useT>;
403
+ }) {
404
+ const healthLabel = isError
405
+ ? t("factoryRoute.automationHealthError")
406
+ : health
407
+ ? {
408
+ healthy: t("factoryRoute.automationHealthHealthy"),
409
+ stale: t("factoryRoute.automationHealthStale"),
410
+ error: t("factoryRoute.automationHealthError"),
411
+ "no-data": t("factoryRoute.automationHealthNoData"),
412
+ }[health.status]
413
+ : t("factoryRoute.automationHealthNoData");
414
+ const hasNoHeartbeat =
415
+ !isError &&
416
+ (!health || health.status === "no-data") &&
417
+ !health?.lastCheckedAt;
418
+ const healthDescription = isError
419
+ ? `${t("factoryRoute.automationDiagnosticsLoadError")} ${error instanceof Error ? error.message : String(error)}`
420
+ : health?.lastError
421
+ ? `${t("factoryRoute.automationHealthErrorDetail")}: ${health.lastError}`
422
+ : health?.status === "stale"
423
+ ? t("factoryRoute.automationHealthStaleHint")
424
+ : hasNoHeartbeat
425
+ ? t("factoryRoute.automationHealthNoDataHint")
426
+ : undefined;
427
+
428
+ return (
429
+ <SettingsGroup variant="soft">
430
+ <SettingsRow
431
+ label={t("factoryRoute.automationHealthTitle")}
432
+ description={
433
+ healthDescription ?? t("factoryRoute.automationHealthDescription")
434
+ }
435
+ control={
436
+ <div className="flex flex-wrap items-center justify-end gap-x-4 gap-y-1">
437
+ <span className="rounded-full bg-muted px-2.5 py-1 text-xs font-medium">
438
+ {healthLabel}
439
+ </span>
440
+ {health?.lastCheckedAt && (
441
+ <span className="text-xs text-muted-foreground">
442
+ {t("factoryRoute.automationLastCheck")}:{" "}
443
+ {formatAutomationDate(health.lastCheckedAt)}
444
+ </span>
445
+ )}
446
+ {health?.lastDispatchedAt && (
447
+ <span className="text-xs text-muted-foreground">
448
+ {t("factoryRoute.automationLastDispatch")}:{" "}
449
+ {formatAutomationDate(health.lastDispatchedAt)}
450
+ </span>
451
+ )}
452
+ </div>
453
+ }
454
+ />
455
+ </SettingsGroup>
456
+ );
457
+ }
@@ -0,0 +1,53 @@
1
+ import { AgentToggleButton } from "@agent-native/core/client/agent-chat";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import { IconPlus } from "@tabler/icons-react";
4
+ import { Link, useLocation, useSearchParams } from "react-router";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+
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";
11
+
12
+ export function FactoryWorkspaceActions({
13
+ onNewFactory,
14
+ }: {
15
+ onNewFactory?: () => void;
16
+ }) {
17
+ const t = useT();
18
+ const location = useLocation();
19
+ const [searchParams] = useSearchParams();
20
+ const onAgents =
21
+ location.pathname === "/agents" ||
22
+ (location.pathname === "/factory" &&
23
+ searchParams.get("tab") === "agents" &&
24
+ !searchParams.get("factoryId"));
25
+ const onSettings = location.pathname === "/factory-settings";
26
+
27
+ return (
28
+ <div className="flex items-center gap-2">
29
+ <Button asChild variant={onAgents ? "default" : "outline"} size="sm">
30
+ <Link to={FACTORY_AGENTS_HREF}>{t("factoryRoute.agentsTab")}</Link>
31
+ </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}>
45
+ <IconPlus className="size-4" />
46
+ {t("factoryRoute.newFactory")}
47
+ </Link>
48
+ </Button>
49
+ )}
50
+ <AgentToggleButton />
51
+ </div>
52
+ );
53
+ }
@@ -42,6 +42,8 @@ function routeOwnsToolbar(pathname: string): boolean {
42
42
  pathname === "/chat" ||
43
43
  pathname.startsWith("/chat/") ||
44
44
  pathname === "/factory" ||
45
+ pathname === "/factory-settings" ||
46
+ pathname === "/agents" ||
45
47
  pathname === "/database" ||
46
48
  pathname.startsWith("/extensions")
47
49
  );
@@ -444,7 +444,9 @@ export function Sidebar({
444
444
  const isActive =
445
445
  item.href === "/"
446
446
  ? isChatRoute
447
- : location.pathname.startsWith(item.href);
447
+ : item.href === "/factory"
448
+ ? location.pathname === "/factory"
449
+ : location.pathname.startsWith(item.href);
448
450
  const link = (
449
451
  <Link
450
452
  to={item.href}
@@ -25,27 +25,27 @@ export function useNavigationState() {
25
25
  view: viewForPath(pathname),
26
26
  path: appPath(pathname),
27
27
  ...(threadId ? { threadId } : {}),
28
- ...(pathname.startsWith("/factory") && searchParams.get("factoryId")
28
+ ...(pathname === "/factory" && searchParams.get("factoryId")
29
29
  ? { factoryId: searchParams.get("factoryId") ?? undefined }
30
30
  : {}),
31
- ...(pathname.startsWith("/factory") && searchParams.get("tab")
31
+ ...(pathname === "/factory" && searchParams.get("tab")
32
32
  ? { factoryTab: searchParams.get("tab") ?? undefined }
33
33
  : {}),
34
- ...(pathname.startsWith("/factory") && searchParams.get("automationId")
34
+ ...(pathname === "/factory" && searchParams.get("automationId")
35
35
  ? {
36
36
  factoryAutomationId:
37
37
  searchParams.get("automationId") ?? undefined,
38
38
  }
39
39
  : {}),
40
- ...(pathname.startsWith("/factory") && searchParams.get("auditRunId")
40
+ ...(pathname === "/factory" && searchParams.get("auditRunId")
41
41
  ? {
42
42
  factoryAuditRunId: searchParams.get("auditRunId") ?? undefined,
43
43
  }
44
44
  : {}),
45
- ...(pathname.startsWith("/factory") && searchParams.get("node")
45
+ ...(pathname === "/factory" && searchParams.get("node")
46
46
  ? { factoryNodeId: searchParams.get("node") ?? undefined }
47
47
  : {}),
48
- ...(pathname.startsWith("/factory") && searchParams.get("edge")
48
+ ...(pathname === "/factory" && searchParams.get("edge")
49
49
  ? { factoryEdgeId: searchParams.get("edge") ?? undefined }
50
50
  : {}),
51
51
  };
@@ -73,7 +73,8 @@ function viewForPath(pathname: string): string {
73
73
  if (pathname.startsWith("/extensions")) return "extensions";
74
74
  if (pathname.startsWith("/observability")) return "observability";
75
75
  if (pathname.startsWith("/agents")) return "agents";
76
- if (pathname.startsWith("/factory")) return "factory";
76
+ if (pathname === "/factory-settings") return "factory-settings";
77
+ if (pathname === "/factory") return "factory";
77
78
  if (pathname.startsWith("/settings/agent") || pathname.startsWith("/agent")) {
78
79
  return "agent";
79
80
  }
@@ -96,6 +97,8 @@ function pathForView(view?: string): string {
96
97
  return "/observability";
97
98
  case "factory":
98
99
  return "/factory";
100
+ case "factory-settings":
101
+ return "/factory-settings";
99
102
  case "agents":
100
103
  return "/agents";
101
104
  case "agent":
@@ -108,6 +108,8 @@ const messages = {
108
108
  slackChannelPlaceholder: "For example, C0123456789",
109
109
  slackChannelName: "Channel name",
110
110
  slackChannelNamePlaceholder: "For example, product-agent-native-feedback",
111
+ builderSlackUserId: "@Builder.io Slack Member ID",
112
+ builderSlackUserIdPlaceholder: "For example, U096KN3EL2Y",
111
113
  repository: "Repository",
112
114
  repositoryPlaceholder: "For example, BuilderIO/agent-native",
113
115
  enablePolling: "Enable one-minute observation polling",
@@ -165,6 +167,7 @@ const messages = {
165
167
  rulesTab: "Rules",
166
168
  automationsTab: "Automations",
167
169
  agentsTab: "Agents",
170
+ factorySettings: "Factory Settings",
168
171
  agenticAppsTitle: "Agentic apps",
169
172
  agenticAppsEmpty: "No agentic apps in this workspace yet.",
170
173
  createApp: "Create app",
@@ -1,8 +1,29 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { IconArrowLeft } from "@tabler/icons-react";
3
+ import { Link } from "react-router";
4
+
1
5
  import { FactoryAgentsView } from "@/components/factory/FactoryAgentsView";
6
+ import { FactoryWorkspaceActions } from "@/components/factory/FactoryWorkspaceActions";
7
+ import { Button } from "@/components/ui/button";
2
8
 
3
9
  export default function AgentsRoute() {
10
+ const t = useT();
11
+
4
12
  return (
5
- <div className="h-full min-h-0 overflow-y-auto bg-background">
13
+ <div className="flex h-full min-h-0 flex-col overflow-y-auto bg-background">
14
+ <div className="flex items-center gap-3 px-4 py-4 lg:px-6">
15
+ <Button asChild type="button" variant="ghost" size="icon">
16
+ <Link to="/factory" aria-label={t("factoryRoute.backToFactories")}>
17
+ <IconArrowLeft className="size-4" />
18
+ </Link>
19
+ </Button>
20
+ <h1 className="text-sm font-medium sm:text-base">
21
+ {t("factoryRoute.agentsTitle")}
22
+ </h1>
23
+ <div className="ms-auto">
24
+ <FactoryWorkspaceActions />
25
+ </div>
26
+ </div>
6
27
  <FactoryAgentsView />
7
28
  </div>
8
29
  );
@@ -0,0 +1,34 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { IconArrowLeft } from "@tabler/icons-react";
3
+ import { Link } from "react-router";
4
+
5
+ import { FactorySettingsView } from "@/components/factory/FactorySettingsView";
6
+ import { FactoryWorkspaceActions } from "@/components/factory/FactoryWorkspaceActions";
7
+ import { Button } from "@/components/ui/button";
8
+
9
+ export function meta() {
10
+ return [{ title: "Factory" }];
11
+ }
12
+
13
+ export default function FactorySettingsRoute() {
14
+ const t = useT();
15
+
16
+ return (
17
+ <div className="flex h-full min-h-0 flex-col overflow-y-auto bg-background">
18
+ <div className="flex items-center gap-3 px-4 py-4 lg:px-6">
19
+ <Button asChild type="button" variant="ghost" size="icon">
20
+ <Link to="/factory" aria-label={t("factoryRoute.backToFactories")}>
21
+ <IconArrowLeft className="size-4" />
22
+ </Link>
23
+ </Button>
24
+ <h1 className="text-sm font-medium sm:text-base">
25
+ {t("factoryRoute.factorySettings")}
26
+ </h1>
27
+ <div className="ms-auto">
28
+ <FactoryWorkspaceActions />
29
+ </div>
30
+ </div>
31
+ <FactorySettingsView />
32
+ </div>
33
+ );
34
+ }