@agent-native/core 0.164.4 → 0.164.12

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 (268) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +84 -0
  3. package/corpus/templates/calendar/AGENTS.md +5 -0
  4. package/corpus/templates/calendar/actions/delete-event.ts +1 -1
  5. package/corpus/templates/calendar/actions/delete-events.ts +508 -0
  6. package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
  7. package/corpus/templates/calendar/actions/list-events.ts +53 -0
  8. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
  9. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
  10. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
  11. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
  12. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
  13. package/corpus/templates/calendar/app/i18n-data.ts +120 -0
  14. package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
  15. package/corpus/templates/calendar/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
  16. package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
  17. package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
  18. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  19. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  20. package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  21. package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  22. package/corpus/templates/clips/app/components/player/video-player.tsx +12 -0
  23. package/corpus/templates/clips/app/routes/_app.dictate.tsx +18 -4
  24. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +114 -20
  25. package/corpus/templates/clips/app/routes/share.$shareId.tsx +83 -78
  26. package/corpus/templates/clips/changelog/2026-08-19-clips-nightly-builds-are-available-for-trying-the-latest-upd.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +1 -0
  28. package/corpus/templates/clips/desktop/README.md +19 -4
  29. package/corpus/templates/clips/desktop/src/app.tsx +78 -16
  30. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -2
  31. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -0
  32. package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  33. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
  34. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  35. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
  36. package/corpus/templates/clips/server/routes/api/clips-updater.json.get.ts +41 -22
  37. package/corpus/templates/factory/AGENTS.md +3 -2
  38. package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
  39. package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
  40. package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
  41. package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
  42. package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
  43. package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
  44. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
  45. package/corpus/templates/factory/app/components/factory/FactoryHistoryView.tsx +709 -0
  46. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
  47. package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
  48. package/corpus/templates/factory/app/i18n/en-US.ts +47 -0
  49. package/corpus/templates/factory/app/routes/factory.tsx +324 -61
  50. package/corpus/templates/factory/server/connectors/slack.ts +44 -2
  51. package/corpus/templates/factory/server/plugins/agent-chat.ts +12 -8
  52. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
  53. package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
  54. package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
  55. package/corpus/templates/mail/actions/view-screen.ts +1 -1
  56. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
  57. package/corpus/templates/mail/server/handlers/google-auth.ts +125 -24
  58. package/corpus/templates/mail/server/lib/gmail-query.ts +5 -2
  59. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  60. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  61. package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  62. package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  63. package/corpus/templates/plan/app/pages/PlansPage.tsx +0 -12
  64. package/corpus/templates/plan/changelog/2026-08-19-plan-prompts-now-guide-you-to-connect-ai-before-sending-and-.md +6 -0
  65. package/corpus/templates/slides/actions/approve-deck-access-request.ts +234 -0
  66. package/corpus/templates/slides/actions/get-deck-access-status.ts +35 -2
  67. package/corpus/templates/slides/actions/request-deck-access.ts +263 -64
  68. package/corpus/templates/slides/app/components/editor/MissingDeckAccessPane.tsx +196 -0
  69. package/corpus/templates/slides/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
  70. package/corpus/templates/slides/app/hooks/use-deck-access.ts +7 -4
  71. package/corpus/templates/slides/app/i18n/en-US.ts +29 -6
  72. package/corpus/templates/slides/app/pages/DeckEditor.tsx +147 -169
  73. package/corpus/templates/slides/app/routes/access-request.approve.tsx +250 -0
  74. package/corpus/templates/slides/server/db/schema.ts +8 -0
  75. package/corpus/templates/slides/server/lib/access-request-email.ts +36 -0
  76. package/corpus/templates/slides/server/lib/emails.ts +26 -0
  77. package/corpus/templates/slides/server/plugins/agent-chat.ts +7 -0
  78. package/corpus/templates/slides/server/plugins/db.ts +50 -0
  79. package/corpus/templates/slides/shared/deck-access.ts +31 -0
  80. package/dist/a2a/task-store.d.ts +1 -0
  81. package/dist/a2a/task-store.js +1 -1
  82. package/dist/agent/observational-memory/store.d.ts +1 -0
  83. package/dist/agent/observational-memory/store.js +1 -1
  84. package/dist/agent/run-store.d.ts +1 -0
  85. package/dist/agent/run-store.js +1 -1
  86. package/dist/agent/tool-approval-store.d.ts +1 -0
  87. package/dist/agent/tool-approval-store.js +1 -1
  88. package/dist/application-state/store.d.ts +1 -0
  89. package/dist/application-state/store.js +1 -1
  90. package/dist/browser-sessions/store.d.ts +1 -0
  91. package/dist/browser-sessions/store.js +1 -1
  92. package/dist/checkpoints/store.d.ts +1 -0
  93. package/dist/checkpoints/store.js +1 -1
  94. package/dist/cli/code-agent-schedules.d.ts +11 -0
  95. package/dist/cli/code-agent-schedules.js +44 -5
  96. package/dist/client/AgentPanel.d.ts +5 -1
  97. package/dist/client/AgentPanel.js +2 -2
  98. package/dist/client/AssistantChat.d.ts +5 -1
  99. package/dist/client/AssistantChat.js +40 -6
  100. package/dist/client/EnvironmentBadge.d.ts +7 -13
  101. package/dist/client/EnvironmentBadge.js +43 -92
  102. package/dist/client/agent-chat/index.d.ts +1 -0
  103. package/dist/client/agent-chat/index.js +1 -0
  104. package/dist/client/agent-chat-adapter.js +76 -50
  105. package/dist/client/app-providers.js +4 -4
  106. package/dist/client/chat/message-components.d.ts +4 -0
  107. package/dist/client/chat/message-components.js +28 -1
  108. package/dist/client/chat/run-recovery.js +26 -0
  109. package/dist/client/chat-first/apps-rail.d.ts +1 -0
  110. package/dist/client/chat-first/apps-rail.js +28 -2
  111. package/dist/client/sse-event-processor.js +13 -8
  112. package/dist/client/use-active-agent-chat-run.d.ts +2 -0
  113. package/dist/client/use-active-agent-chat-run.js +22 -0
  114. package/dist/coding-tools/sandbox/executions-store.d.ts +1 -0
  115. package/dist/coding-tools/sandbox/executions-store.js +1 -1
  116. package/dist/collab/awareness-store.d.ts +1 -0
  117. package/dist/collab/awareness-store.js +1 -1
  118. package/dist/collab/routes.d.ts +1 -1
  119. package/dist/collab/storage.d.ts +1 -0
  120. package/dist/collab/storage.js +1 -1
  121. package/dist/collab/struct-routes.d.ts +1 -1
  122. package/dist/db/client.js +3 -3
  123. package/dist/db/ddl-guard.js +8 -0
  124. package/dist/db/migration-runtime.d.ts +23 -0
  125. package/dist/db/migration-runtime.js +41 -0
  126. package/dist/db/migrations.d.ts +1 -2
  127. package/dist/db/migrations.js +2 -24
  128. package/dist/email-catalog/log.d.ts +1 -0
  129. package/dist/email-catalog/log.js +1 -1
  130. package/dist/guards/index.d.ts +1 -0
  131. package/dist/guards/index.js +1 -0
  132. package/dist/guards/release-schema-complete.d.ts +22 -0
  133. package/dist/guards/release-schema-complete.js +144 -0
  134. package/dist/integrations/a2a-continuations-store.d.ts +1 -0
  135. package/dist/integrations/a2a-continuations-store.js +1 -1
  136. package/dist/integrations/awaiting-input-store.d.ts +1 -0
  137. package/dist/integrations/awaiting-input-store.js +1 -1
  138. package/dist/integrations/config-store.d.ts +1 -0
  139. package/dist/integrations/config-store.js +1 -1
  140. package/dist/integrations/controls-store.d.ts +1 -0
  141. package/dist/integrations/controls-store.js +1 -1
  142. package/dist/integrations/identity-links-store.d.ts +1 -0
  143. package/dist/integrations/identity-links-store.js +1 -1
  144. package/dist/integrations/installations-store.d.ts +1 -0
  145. package/dist/integrations/installations-store.js +1 -1
  146. package/dist/integrations/remote-commands-store.d.ts +1 -0
  147. package/dist/integrations/remote-commands-store.js +1 -1
  148. package/dist/integrations/remote-devices-store.d.ts +1 -0
  149. package/dist/integrations/remote-devices-store.js +1 -1
  150. package/dist/integrations/remote-push-store.d.ts +1 -0
  151. package/dist/integrations/remote-push-store.js +1 -1
  152. package/dist/integrations/remote-run-events-store.d.ts +1 -0
  153. package/dist/integrations/remote-run-events-store.js +1 -1
  154. package/dist/integrations/scope-store.d.ts +1 -0
  155. package/dist/integrations/scope-store.js +1 -1
  156. package/dist/integrations/thread-mapping-store.d.ts +1 -0
  157. package/dist/integrations/thread-mapping-store.js +1 -1
  158. package/dist/integrations/usage-budget-store.d.ts +1 -0
  159. package/dist/integrations/usage-budget-store.js +1 -1
  160. package/dist/jobs/background-automation-runner.js +111 -9
  161. package/dist/jobs/run-history.d.ts +1 -0
  162. package/dist/jobs/run-history.js +1 -1
  163. package/dist/jobs/scheduler-health.d.ts +1 -0
  164. package/dist/jobs/scheduler-health.js +1 -1
  165. package/dist/mcp/approval-store.d.ts +1 -0
  166. package/dist/mcp/approval-store.js +1 -1
  167. package/dist/mcp/connect-store.d.ts +1 -0
  168. package/dist/mcp/connect-store.js +1 -1
  169. package/dist/mcp/oauth-store.d.ts +1 -0
  170. package/dist/mcp/oauth-store.js +1 -1
  171. package/dist/notifications/store.d.ts +1 -0
  172. package/dist/notifications/store.js +1 -1
  173. package/dist/oauth-tokens/store.d.ts +1 -0
  174. package/dist/oauth-tokens/store.js +1 -1
  175. package/dist/observability/routes.d.ts +5 -5
  176. package/dist/progress/routes.d.ts +1 -1
  177. package/dist/progress/store.d.ts +1 -0
  178. package/dist/progress/store.js +1 -1
  179. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  180. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  181. package/dist/provider-api/corpus-jobs-store.d.ts +1 -0
  182. package/dist/provider-api/corpus-jobs-store.js +1 -1
  183. package/dist/provider-api/custom-registry.d.ts +1 -0
  184. package/dist/provider-api/custom-registry.js +1 -1
  185. package/dist/provider-api/quota-governor.d.ts +1 -0
  186. package/dist/provider-api/quota-governor.js +1 -1
  187. package/dist/provider-api/staged-datasets-store.d.ts +1 -0
  188. package/dist/provider-api/staged-datasets-store.js +1 -1
  189. package/dist/resources/handlers.d.ts +3 -3
  190. package/dist/resources/store.d.ts +1 -0
  191. package/dist/resources/store.js +1 -1
  192. package/dist/scripts/dev/index.js +15 -1
  193. package/dist/search/index.js +3 -0
  194. package/dist/secrets/routes.d.ts +9 -9
  195. package/dist/secrets/storage.d.ts +1 -0
  196. package/dist/secrets/storage.js +1 -1
  197. package/dist/server/agent-chat-plugin.js +38 -9
  198. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  199. package/dist/server/agent-teams-run-queue.d.ts +1 -0
  200. package/dist/server/agent-teams-run-queue.js +1 -1
  201. package/dist/server/auth-marketing.js +3 -1
  202. package/dist/server/auth.d.ts +18 -1
  203. package/dist/server/auth.js +370 -154
  204. package/dist/server/embed-session.d.ts +1 -0
  205. package/dist/server/embed-session.js +1 -1
  206. package/dist/server/google-auth-plugin.js +3 -1
  207. package/dist/server/google-oauth.d.ts +10 -0
  208. package/dist/server/google-oauth.js +25 -1
  209. package/dist/server/identity-sso-store.d.ts +1 -0
  210. package/dist/server/identity-sso-store.js +1 -1
  211. package/dist/server/index.d.ts +1 -1
  212. package/dist/server/index.js +1 -1
  213. package/dist/server/onboarding-html.js +200 -31
  214. package/dist/server/poll.d.ts +2 -0
  215. package/dist/server/poll.js +19 -1
  216. package/dist/server/realtime-token.d.ts +1 -1
  217. package/dist/server/release-migrations.js +5 -0
  218. package/dist/server/release-schema.d.ts +46 -0
  219. package/dist/server/release-schema.js +307 -0
  220. package/dist/settings/store.d.ts +1 -0
  221. package/dist/settings/store.js +1 -1
  222. package/dist/shared/environment-lanes.d.ts +28 -0
  223. package/dist/shared/environment-lanes.js +61 -0
  224. package/dist/sharing/actions/share-resource.js +22 -2
  225. package/dist/styles/agent-native.css +17 -15
  226. package/dist/triggers/routes.d.ts +11 -0
  227. package/dist/triggers/routes.js +10 -0
  228. package/dist/usage/alerts-store.d.ts +1 -0
  229. package/dist/usage/alerts-store.js +1 -1
  230. package/dist/usage/store.d.ts +1 -0
  231. package/dist/usage/store.js +1 -1
  232. package/docs/content/actions-access-control.mdx +136 -2
  233. package/docs/content/actions-agent-tools.mdx +101 -0
  234. package/docs/content/actions-defining.mdx +2 -2
  235. package/docs/content/audit-log.mdx +1 -1
  236. package/docs/content/authentication.mdx +1 -1
  237. package/docs/content/cloudflare.mdx +43 -0
  238. package/docs/content/creating-templates.mdx +1 -1
  239. package/docs/content/deploy-an-app.mdx +109 -0
  240. package/docs/content/deployment-environment-variables.mdx +101 -0
  241. package/docs/content/deployment.mdx +125 -792
  242. package/docs/content/docs-components.mdx +2 -2
  243. package/docs/content/environment-variables.mdx +41 -41
  244. package/docs/content/file-uploads.mdx +74 -40
  245. package/docs/content/harness-agents.mdx +2 -2
  246. package/docs/content/internationalization.mdx +61 -65
  247. package/docs/content/key-concepts.mdx +3 -3
  248. package/docs/content/multi-app-workspace.mdx +2 -2
  249. package/docs/content/netlify.mdx +47 -0
  250. package/docs/content/node-docker.mdx +138 -0
  251. package/docs/content/organizations-teams-permissions.mdx +5 -5
  252. package/docs/content/other-platforms.mdx +27 -0
  253. package/docs/content/ssr-caching.mdx +45 -0
  254. package/docs/content/template-assets-developers.mdx +2 -2
  255. package/docs/content/template-chat-developers.mdx +2 -2
  256. package/docs/content/template-content-developers.mdx +2 -2
  257. package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
  258. package/docs/content/template-content-sync.mdx +2 -2
  259. package/docs/content/toolkit-agent-ux.mdx +1 -1
  260. package/docs/content/toolkit-resources.mdx +2 -2
  261. package/docs/content/updating-ui-in-production.mdx +35 -0
  262. package/docs/content/vercel.mdx +38 -0
  263. package/docs/content/what-is-agent-native.mdx +1 -1
  264. package/docs/content/workspace-deployment.mdx +350 -0
  265. package/package.json +2 -2
  266. package/docs/content/agents.mdx +0 -9
  267. package/docs/content/database.mdx +0 -317
  268. package/docs/content/human-approval.mdx +0 -156
@@ -0,0 +1,709 @@
1
+ import {
2
+ useActionMutation,
3
+ useActionQuery,
4
+ } from "@agent-native/core/client/hooks";
5
+ import { useT } from "@agent-native/core/client/i18n";
6
+ import {
7
+ IconAlertCircle,
8
+ IconCheck,
9
+ IconHistory,
10
+ IconLoader2,
11
+ IconRestore,
12
+ } from "@tabler/icons-react";
13
+ import { useEffect, useState } from "react";
14
+
15
+ import {
16
+ AlertDialog,
17
+ AlertDialogAction,
18
+ AlertDialogCancel,
19
+ AlertDialogContent,
20
+ AlertDialogDescription,
21
+ AlertDialogFooter,
22
+ AlertDialogHeader,
23
+ AlertDialogTitle,
24
+ } from "@/components/ui/alert-dialog";
25
+ import { Button } from "@/components/ui/button";
26
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
27
+
28
+ import { FactoryCanvas, type FactoryCanvasGraph } from "./FactoryCanvas";
29
+
30
+ type FactoryGraphVersion = {
31
+ id: string;
32
+ factoryId: string;
33
+ version: number;
34
+ source: string;
35
+ changeSummary: string;
36
+ createdAt: string;
37
+ createdBy: string;
38
+ isCurrent: boolean;
39
+ };
40
+
41
+ type FactoryGraphVersionSnapshot = FactoryGraphVersion & {
42
+ graph: FactoryCanvasGraph;
43
+ };
44
+
45
+ type FactoryGraphHistoryResponse = {
46
+ factoryId: string;
47
+ currentVersion: number | null;
48
+ hasMore: boolean;
49
+ nextBeforeVersion: number | null;
50
+ versions: FactoryGraphVersion[];
51
+ };
52
+
53
+ type RestoreResult = {
54
+ versionId: string;
55
+ graphVersion: number;
56
+ name: string;
57
+ graph: FactoryCanvasGraph;
58
+ };
59
+
60
+ interface FactoryHistoryViewProps {
61
+ factoryId: string;
62
+ hasUnsavedChanges: boolean;
63
+ onRestored: (result?: RestoreResult) => Promise<void>;
64
+ }
65
+
66
+ const HISTORY_PAGE_SIZE = 25;
67
+
68
+ export function FactoryHistoryView({
69
+ factoryId,
70
+ hasUnsavedChanges,
71
+ onRestored,
72
+ }: FactoryHistoryViewProps) {
73
+ const t = useT();
74
+ const [beforeVersion, setBeforeVersion] = useState<number | undefined>();
75
+ const firstPageHistoryQuery = useActionQuery<FactoryGraphHistoryResponse>(
76
+ "list-factory-graph-versions",
77
+ {
78
+ factoryId,
79
+ limit: HISTORY_PAGE_SIZE,
80
+ },
81
+ );
82
+ const olderHistoryQuery = useActionQuery<FactoryGraphHistoryResponse>(
83
+ "list-factory-graph-versions",
84
+ {
85
+ factoryId,
86
+ limit: HISTORY_PAGE_SIZE,
87
+ beforeVersion,
88
+ },
89
+ { enabled: beforeVersion !== undefined },
90
+ );
91
+ const historyQuery =
92
+ beforeVersion === undefined ? firstPageHistoryQuery : olderHistoryQuery;
93
+ const [versions, setVersions] = useState<FactoryGraphVersion[]>([]);
94
+ const [hasMore, setHasMore] = useState(false);
95
+ const [nextBeforeVersion, setNextBeforeVersion] = useState<number | null>(
96
+ null,
97
+ );
98
+ const restoreMutation = useActionMutation("restore-factory-graph-version");
99
+ const [selectedVersionId, setSelectedVersionId] = useState<string | null>(
100
+ null,
101
+ );
102
+ const [pendingRestore, setPendingRestore] =
103
+ useState<FactoryGraphVersion | null>(null);
104
+ const [restoringVersionId, setRestoringVersionId] = useState<string | null>(
105
+ null,
106
+ );
107
+ const [restoreError, setRestoreError] = useState<string | null>(null);
108
+ const [restoreRefreshError, setRestoreRefreshError] = useState<string | null>(
109
+ null,
110
+ );
111
+ const [restoreStatus, setRestoreStatus] = useState<string | null>(null);
112
+
113
+ useEffect(() => {
114
+ const page = historyQuery.data;
115
+ if (!page) return;
116
+ setVersions((current) => {
117
+ if (beforeVersion === undefined) return page.versions;
118
+ const existingIds = new Set(current.map((version) => version.id));
119
+ return [
120
+ ...current,
121
+ ...page.versions.filter((version) => !existingIds.has(version.id)),
122
+ ];
123
+ });
124
+ setHasMore(page.hasMore);
125
+ setNextBeforeVersion(page.nextBeforeVersion);
126
+ }, [beforeVersion, historyQuery.data]);
127
+
128
+ const selectedVersion =
129
+ versions.find((version) => version.id === selectedVersionId) ??
130
+ versions[0] ??
131
+ null;
132
+ const selectedSnapshotQuery = useActionQuery<FactoryGraphVersionSnapshot>(
133
+ "get-factory-graph-version",
134
+ selectedVersion
135
+ ? { factoryId, versionId: selectedVersion.id }
136
+ : { factoryId, versionId: "" },
137
+ { enabled: Boolean(selectedVersion) },
138
+ );
139
+
140
+ async function refreshHistory(preferredVersionId?: string) {
141
+ const refreshed = await firstPageHistoryQuery.refetch();
142
+ if (refreshed.error || !refreshed.data) {
143
+ throw refreshed.error ?? new Error("Factory history refresh failed.");
144
+ }
145
+ const page = refreshed.data;
146
+ setBeforeVersion(undefined);
147
+ setVersions(page.versions);
148
+ setHasMore(page.hasMore);
149
+ setNextBeforeVersion(page.nextBeforeVersion);
150
+ setSelectedVersionId(
151
+ preferredVersionId &&
152
+ page.versions.some((version) => version.id === preferredVersionId)
153
+ ? preferredVersionId
154
+ : (page.versions[0]?.id ?? null),
155
+ );
156
+ return page;
157
+ }
158
+
159
+ function isCurrent(version: FactoryGraphVersion) {
160
+ return version.isCurrent;
161
+ }
162
+
163
+ function requestRestore(version: FactoryGraphVersion) {
164
+ setRestoreError(null);
165
+ setRestoreRefreshError(null);
166
+ setRestoreStatus(null);
167
+ setPendingRestore(version);
168
+ }
169
+
170
+ function loadOlderVersions() {
171
+ if (historyQuery.isFetching || nextBeforeVersion === null) return;
172
+ setBeforeVersion(nextBeforeVersion);
173
+ }
174
+
175
+ async function confirmRestore(event: React.MouseEvent<HTMLButtonElement>) {
176
+ event.preventDefault();
177
+ const version = pendingRestore;
178
+ if (!version || restoringVersionId) return;
179
+
180
+ setRestoreError(null);
181
+ setRestoreRefreshError(null);
182
+ setRestoringVersionId(version.id);
183
+ const expectedCurrentVersion = historyQuery.data?.currentVersion ?? null;
184
+ let rawResult: unknown;
185
+ try {
186
+ rawResult = await restoreMutation.mutateAsync({
187
+ factoryId,
188
+ versionId: version.id,
189
+ });
190
+ } catch (error) {
191
+ let reconciliation: RestoreReconciliation = "unverified";
192
+ try {
193
+ const page = await refreshHistory();
194
+ reconciliation = reconcileRestoreFailure(
195
+ page,
196
+ version,
197
+ expectedCurrentVersion,
198
+ );
199
+ } catch {
200
+ reconciliation = "unverified";
201
+ }
202
+ if (reconciliation === "committed") {
203
+ setPendingRestore(null);
204
+ setRestoreError(null);
205
+ setRestoreStatus(t("factoryRoute.historyRestored"));
206
+ try {
207
+ await onRestored();
208
+ } catch {
209
+ setRestoreRefreshError(t("factoryRoute.historyRefreshFailed"));
210
+ }
211
+ } else if (reconciliation === "conflict") {
212
+ setPendingRestore(null);
213
+ setRestoreError(t("factoryRoute.historyRestoreConflict"));
214
+ } else if (reconciliation === "unverified") {
215
+ setPendingRestore(null);
216
+ setRestoreError(t("factoryRoute.historyRestoreUnverified"));
217
+ } else {
218
+ setRestoreError(
219
+ error instanceof Error
220
+ ? error.message
221
+ : t("factoryRoute.historyRestoreFailed"),
222
+ );
223
+ }
224
+ setRestoringVersionId(null);
225
+ return;
226
+ }
227
+
228
+ let result: RestoreResult;
229
+ try {
230
+ result = readRestoreResult(rawResult);
231
+ } catch {
232
+ setPendingRestore(null);
233
+ setRestoreStatus(null);
234
+ setRestoreError(t("factoryRoute.historyRestoreUnverified"));
235
+ try {
236
+ await onRestored();
237
+ } catch {
238
+ setRestoreRefreshError(t("factoryRoute.historyRefreshFailed"));
239
+ }
240
+ try {
241
+ await refreshHistory();
242
+ } catch {
243
+ setRestoreRefreshError(t("factoryRoute.historyRefreshFailed"));
244
+ }
245
+ setRestoringVersionId(null);
246
+ return;
247
+ }
248
+
249
+ setPendingRestore(null);
250
+ setRestoreStatus(t("factoryRoute.historyRestored"));
251
+ let refreshFailed = false;
252
+ try {
253
+ await onRestored(result);
254
+ } catch {
255
+ refreshFailed = true;
256
+ }
257
+ try {
258
+ await refreshHistory(result.versionId);
259
+ } catch {
260
+ refreshFailed = true;
261
+ }
262
+ if (refreshFailed) {
263
+ setRestoreRefreshError(t("factoryRoute.historyRefreshFailed"));
264
+ }
265
+ setRestoringVersionId(null);
266
+ }
267
+
268
+ if (
269
+ historyQuery.isLoading ||
270
+ (versions.length === 0 && historyQuery.isFetching)
271
+ ) {
272
+ return (
273
+ <div
274
+ className="grid gap-4 p-4 lg:grid-cols-[minmax(250px,.34fr)_minmax(0,1fr)] lg:p-6"
275
+ aria-label={t("factoryRoute.historyLoading")}
276
+ >
277
+ <div className="h-[520px] animate-pulse rounded-xl bg-card shadow-sm" />
278
+ <div className="h-[520px] animate-pulse rounded-xl bg-card shadow-sm" />
279
+ </div>
280
+ );
281
+ }
282
+
283
+ if (historyQuery.isError && versions.length === 0) {
284
+ return (
285
+ <div className="p-4 lg:p-6">
286
+ <Card>
287
+ <CardContent className="flex items-center gap-2 p-4 text-sm text-destructive">
288
+ <IconAlertCircle className="size-4 shrink-0" />
289
+ <span>{t("factoryRoute.historyLoadError")}</span>
290
+ <Button
291
+ type="button"
292
+ variant="link"
293
+ size="sm"
294
+ className="h-auto p-0"
295
+ onClick={() => void historyQuery.refetch()}
296
+ >
297
+ {t("triage.refresh")}
298
+ </Button>
299
+ </CardContent>
300
+ </Card>
301
+ </div>
302
+ );
303
+ }
304
+
305
+ if (versions.length === 0) {
306
+ return (
307
+ <div className="p-4 lg:p-6">
308
+ <Card>
309
+ <CardContent className="flex min-h-72 flex-col items-center justify-center gap-3 p-6 text-center">
310
+ <span className="flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground">
311
+ <IconHistory className="size-5" />
312
+ </span>
313
+ <div className="space-y-1">
314
+ <p className="text-sm font-medium">
315
+ {t("factoryRoute.historyEmpty")}
316
+ </p>
317
+ <p className="max-w-sm text-sm text-muted-foreground">
318
+ {t("factoryRoute.historyEmptyHint")}
319
+ </p>
320
+ </div>
321
+ </CardContent>
322
+ </Card>
323
+ </div>
324
+ );
325
+ }
326
+
327
+ return (
328
+ <>
329
+ <div className="grid gap-4 p-4 lg:grid-cols-[minmax(250px,.34fr)_minmax(0,1fr)] lg:p-6">
330
+ <Card className="h-fit lg:sticky lg:top-4">
331
+ <CardHeader className="flex-row items-center justify-between gap-3 px-4 pb-3 pt-4">
332
+ <CardTitle className="flex items-center gap-2 text-base">
333
+ <IconHistory className="size-4 text-muted-foreground" />
334
+ {t("factoryRoute.historyTitle")}
335
+ </CardTitle>
336
+ <span className="text-xs tabular-nums text-muted-foreground">
337
+ {versions.length}
338
+ </span>
339
+ </CardHeader>
340
+ <CardContent className="space-y-1 p-2">
341
+ {versions.map((version) => {
342
+ const current = isCurrent(version);
343
+ const selected = selectedVersion?.id === version.id;
344
+ return (
345
+ <button
346
+ key={version.id}
347
+ type="button"
348
+ data-testid={`factory-history-version-${version.version}`}
349
+ aria-pressed={selected}
350
+ className={`flex w-full items-start gap-3 rounded-lg border-s-2 px-3 py-3 text-start transition-colors ${selected ? "border-s-primary bg-muted/70" : "border-transparent hover:bg-muted/50"}`}
351
+ onClick={() => {
352
+ setSelectedVersionId(version.id);
353
+ setRestoreError(null);
354
+ setRestoreRefreshError(null);
355
+ setRestoreStatus(null);
356
+ }}
357
+ >
358
+ <span
359
+ className={`mt-1.5 size-2 shrink-0 rounded-full ${current ? "bg-primary" : "bg-muted-foreground/40"}`}
360
+ aria-hidden="true"
361
+ />
362
+ <span className="min-w-0 flex-1">
363
+ <span className="flex flex-wrap items-center gap-x-2 gap-y-1">
364
+ <span className="text-sm font-medium">
365
+ {t("factoryRoute.historyVersion", {
366
+ version: version.version,
367
+ })}
368
+ </span>
369
+ {current ? (
370
+ <span className="rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary">
371
+ {t("factoryRoute.historyCurrent")}
372
+ </span>
373
+ ) : null}
374
+ </span>
375
+ <span className="mt-1 block text-xs text-muted-foreground">
376
+ {sourceLabel(version.source, t)} ·{" "}
377
+ {formatDate(version.createdAt)}
378
+ </span>
379
+ <span className="mt-1 block line-clamp-2 text-xs leading-5 text-muted-foreground">
380
+ {version.changeSummary ||
381
+ t("factoryRoute.historyNoSummary")}
382
+ </span>
383
+ </span>
384
+ </button>
385
+ );
386
+ })}
387
+ {historyQuery.isError ? (
388
+ <div
389
+ className="flex items-center gap-2 px-3 py-2 text-xs text-destructive"
390
+ role="alert"
391
+ >
392
+ <IconAlertCircle className="size-3.5 shrink-0" />
393
+ <span>{t("factoryRoute.historyLoadError")}</span>
394
+ <Button
395
+ type="button"
396
+ variant="link"
397
+ size="sm"
398
+ className="h-auto p-0 text-destructive"
399
+ onClick={() => void historyQuery.refetch()}
400
+ >
401
+ {t("triage.refresh")}
402
+ </Button>
403
+ </div>
404
+ ) : null}
405
+ {hasMore ? (
406
+ <Button
407
+ type="button"
408
+ variant="ghost"
409
+ size="sm"
410
+ className="mt-1 w-full"
411
+ disabled={historyQuery.isFetching || nextBeforeVersion === null}
412
+ onClick={loadOlderVersions}
413
+ >
414
+ {historyQuery.isFetching ? (
415
+ <IconLoader2 className="size-3.5 animate-spin" />
416
+ ) : null}
417
+ {t("factoryRoute.historyLoadOlder")}
418
+ </Button>
419
+ ) : null}
420
+ </CardContent>
421
+ </Card>
422
+
423
+ {selectedVersion ? (
424
+ <Card className="min-w-0">
425
+ <CardHeader className="gap-2 px-4 pb-3 pt-4 sm:flex-row sm:items-start sm:justify-between">
426
+ <div className="min-w-0">
427
+ <CardTitle className="text-base">
428
+ {t("factoryRoute.historyVersion", {
429
+ version: selectedVersion.version,
430
+ })}
431
+ </CardTitle>
432
+ </div>
433
+ {isCurrent(selectedVersion) ? (
434
+ <span className="inline-flex w-fit shrink-0 items-center gap-1.5 rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary">
435
+ <IconCheck className="size-3.5" />
436
+ {t("factoryRoute.historyCurrent")}
437
+ </span>
438
+ ) : null}
439
+ </CardHeader>
440
+ <CardContent className="space-y-4 px-3 pb-4 pt-0 sm:px-4">
441
+ {selectedSnapshotQuery.isLoading ? (
442
+ <div
443
+ className="h-[360px] animate-pulse rounded-lg bg-muted/40"
444
+ aria-label={t("factoryRoute.historyLoading")}
445
+ />
446
+ ) : selectedSnapshotQuery.isError ? (
447
+ <div
448
+ className="flex items-center gap-2 rounded-lg border border-destructive/20 bg-destructive/5 p-4 text-sm text-destructive"
449
+ role="alert"
450
+ >
451
+ <IconAlertCircle className="size-4 shrink-0" />
452
+ <span>{t("factoryRoute.historySnapshotLoadError")}</span>
453
+ <Button
454
+ type="button"
455
+ variant="link"
456
+ size="sm"
457
+ className="h-auto p-0 text-destructive"
458
+ onClick={() => void selectedSnapshotQuery.refetch()}
459
+ >
460
+ {t("triage.refresh")}
461
+ </Button>
462
+ </div>
463
+ ) : selectedSnapshotQuery.data ? (
464
+ <>
465
+ <div className="overflow-hidden rounded-lg border bg-muted/10 p-2">
466
+ <FactoryCanvas
467
+ graph={selectedSnapshotQuery.data.graph}
468
+ preview
469
+ />
470
+ </div>
471
+
472
+ <dl className="grid gap-3 rounded-lg bg-muted/25 p-3 text-sm sm:grid-cols-3">
473
+ <div>
474
+ <dt className="text-xs text-muted-foreground">
475
+ {t("factoryRoute.historySource")}
476
+ </dt>
477
+ <dd className="mt-1 font-medium">
478
+ {sourceLabel(selectedVersion.source, t)}
479
+ </dd>
480
+ </div>
481
+ <div>
482
+ <dt className="text-xs text-muted-foreground">
483
+ {t("factoryRoute.historyNodes")}
484
+ </dt>
485
+ <dd className="mt-1 font-medium tabular-nums">
486
+ {selectedSnapshotQuery.data.graph.nodes.length}
487
+ </dd>
488
+ </div>
489
+ <div>
490
+ <dt className="text-xs text-muted-foreground">
491
+ {t("factoryRoute.historyConnections")}
492
+ </dt>
493
+ <dd className="mt-1 font-medium tabular-nums">
494
+ {selectedSnapshotQuery.data.graph.edges.length}
495
+ </dd>
496
+ </div>
497
+ </dl>
498
+
499
+ <div className="space-y-1">
500
+ <p className="text-xs font-medium text-muted-foreground">
501
+ {t("factoryRoute.historyChangeSummary")}
502
+ </p>
503
+ <p className="text-sm leading-6">
504
+ {selectedVersion.changeSummary ||
505
+ t("factoryRoute.historyNoSummary")}
506
+ </p>
507
+ </div>
508
+
509
+ {isCurrent(selectedVersion) ? (
510
+ <div className="flex items-center gap-2 rounded-lg border border-primary/20 bg-primary/5 px-3 py-2.5 text-sm text-primary">
511
+ <IconCheck className="size-4 shrink-0" />
512
+ <span>{t("factoryRoute.historyCurrentHint")}</span>
513
+ </div>
514
+ ) : (
515
+ <Button
516
+ type="button"
517
+ variant="outline"
518
+ className="w-full sm:w-auto"
519
+ data-testid="factory-history-restore"
520
+ onClick={() => requestRestore(selectedVersion)}
521
+ disabled={restoreMutation.isPending}
522
+ >
523
+ {restoreMutation.isPending ? (
524
+ <IconLoader2 className="size-4 animate-spin" />
525
+ ) : (
526
+ <IconRestore className="size-4" />
527
+ )}
528
+ {t("factoryRoute.historyRestore")}
529
+ </Button>
530
+ )}
531
+ </>
532
+ ) : null}
533
+
534
+ {restoreStatus ? (
535
+ <p
536
+ className="flex items-center gap-2 text-sm text-primary"
537
+ role="status"
538
+ >
539
+ <IconCheck className="size-4 shrink-0" />
540
+ {restoreStatus}
541
+ </p>
542
+ ) : null}
543
+ {restoreRefreshError ? (
544
+ <p
545
+ className="flex items-center gap-2 text-sm text-amber-700 dark:text-amber-300"
546
+ role="status"
547
+ >
548
+ <IconAlertCircle className="size-4 shrink-0" />
549
+ {restoreRefreshError}
550
+ </p>
551
+ ) : null}
552
+ {restoreError && !pendingRestore ? (
553
+ <p
554
+ className="flex items-center gap-2 text-sm text-destructive"
555
+ role="alert"
556
+ >
557
+ <IconAlertCircle className="size-4 shrink-0" />
558
+ {restoreError}
559
+ </p>
560
+ ) : null}
561
+ </CardContent>
562
+ </Card>
563
+ ) : null}
564
+ </div>
565
+
566
+ <AlertDialog
567
+ open={pendingRestore !== null}
568
+ onOpenChange={(open) => {
569
+ if (!open && !restoringVersionId) {
570
+ setPendingRestore(null);
571
+ setRestoreError(null);
572
+ setRestoreRefreshError(null);
573
+ }
574
+ }}
575
+ >
576
+ <AlertDialogContent>
577
+ <AlertDialogHeader>
578
+ <AlertDialogTitle>
579
+ {t("factoryRoute.historyRestoreTitle", {
580
+ version: pendingRestore?.version ?? "",
581
+ })}
582
+ </AlertDialogTitle>
583
+ <AlertDialogDescription>
584
+ {t("factoryRoute.historyRestoreDescription")}
585
+ {hasUnsavedChanges ? (
586
+ <span className="mt-2 block font-medium text-amber-700 dark:text-amber-300">
587
+ {t("factoryRoute.historyRestoreUnsaved")}
588
+ </span>
589
+ ) : null}
590
+ {restoreError ? (
591
+ <span className="mt-2 block text-destructive" role="alert">
592
+ {restoreError}
593
+ </span>
594
+ ) : null}
595
+ </AlertDialogDescription>
596
+ </AlertDialogHeader>
597
+ <AlertDialogFooter>
598
+ <AlertDialogCancel disabled={Boolean(restoringVersionId)}>
599
+ {t("factoryRoute.historyCancel")}
600
+ </AlertDialogCancel>
601
+ <AlertDialogAction
602
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
603
+ disabled={Boolean(restoringVersionId)}
604
+ onClick={confirmRestore}
605
+ >
606
+ {restoringVersionId ? (
607
+ <IconLoader2 className="size-4 animate-spin" />
608
+ ) : (
609
+ <IconRestore className="size-4" />
610
+ )}
611
+ {t("factoryRoute.historyRestoreConfirm")}
612
+ </AlertDialogAction>
613
+ </AlertDialogFooter>
614
+ </AlertDialogContent>
615
+ </AlertDialog>
616
+ </>
617
+ );
618
+ }
619
+
620
+ function sourceLabel(source: string, t: ReturnType<typeof useT>) {
621
+ switch (source) {
622
+ case "ai":
623
+ return t("factoryRoute.historySourceAi");
624
+ case "restore":
625
+ return t("factoryRoute.historySourceRestore");
626
+ case "seed":
627
+ return t("factoryRoute.historySourceSeed");
628
+ case "manual":
629
+ return t("factoryRoute.historySourceManual");
630
+ default:
631
+ return t("factoryRoute.historySourceSaved");
632
+ }
633
+ }
634
+
635
+ function formatDate(value: string): string {
636
+ const date = new Date(value);
637
+ if (Number.isNaN(date.getTime())) return value;
638
+ return new Intl.DateTimeFormat(undefined, {
639
+ dateStyle: "medium",
640
+ timeStyle: "short",
641
+ }).format(date);
642
+ }
643
+
644
+ function readRestoreResult(value: unknown): RestoreResult {
645
+ if (!value || typeof value !== "object") {
646
+ throw new Error("Restore did not return a usable version.");
647
+ }
648
+ const result = value as Record<string, unknown>;
649
+ if (
650
+ typeof result.versionId !== "string" ||
651
+ typeof result.graphVersion !== "number" ||
652
+ typeof result.name !== "string" ||
653
+ !isFactoryCanvasGraph(result.graph)
654
+ ) {
655
+ throw new Error("Restore did not return a usable version.");
656
+ }
657
+ return {
658
+ versionId: result.versionId,
659
+ graphVersion: result.graphVersion,
660
+ name: result.name,
661
+ graph: result.graph,
662
+ };
663
+ }
664
+
665
+ function isFactoryCanvasGraph(value: unknown): value is FactoryCanvasGraph {
666
+ if (!value || typeof value !== "object") return false;
667
+ const graph = value as Record<string, unknown>;
668
+ return (
669
+ typeof graph.version === "number" &&
670
+ typeof graph.name === "string" &&
671
+ Array.isArray(graph.nodes) &&
672
+ Array.isArray(graph.edges)
673
+ );
674
+ }
675
+
676
+ type RestoreReconciliation =
677
+ | "committed"
678
+ | "conflict"
679
+ | "retryable"
680
+ | "unverified";
681
+
682
+ function reconcileRestoreFailure(
683
+ page: FactoryGraphHistoryResponse,
684
+ target: FactoryGraphVersion,
685
+ expectedCurrentVersion: number | null,
686
+ ): RestoreReconciliation {
687
+ const latest = page.versions[0];
688
+ const targetSummary = `Restored version ${target.version}.`;
689
+ if (
690
+ page.currentVersion === target.version ||
691
+ (page.currentVersion !== null &&
692
+ page.currentVersion > target.version &&
693
+ latest?.isCurrent === true &&
694
+ latest.source === "restore" &&
695
+ latest.changeSummary === targetSummary)
696
+ ) {
697
+ return "committed";
698
+ }
699
+ if (
700
+ expectedCurrentVersion !== null &&
701
+ page.currentVersion !== expectedCurrentVersion
702
+ ) {
703
+ return "conflict";
704
+ }
705
+ if (expectedCurrentVersion === null && page.currentVersion !== null) {
706
+ return "conflict";
707
+ }
708
+ return "retryable";
709
+ }