@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
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  AgentToggleButton,
3
+ requestAgentChatThreadOpen,
3
4
  useChatModels,
4
5
  } from "@agent-native/core/client/agent-chat";
5
6
  import {
@@ -18,8 +19,9 @@ import {
18
19
  IconPlus,
19
20
  IconRefresh,
20
21
  } from "@tabler/icons-react";
21
- import { useEffect, useMemo, useState } from "react";
22
+ import { useEffect, useMemo, useRef, useState } from "react";
22
23
  import { Link, useSearchParams } from "react-router";
24
+ import { toast } from "sonner";
23
25
 
24
26
  import { FactoryAgentsView } from "@/components/factory/FactoryAgentsView";
25
27
  import { FactoryAuditView } from "@/components/factory/FactoryAuditView";
@@ -29,6 +31,7 @@ import {
29
31
  type FactoryCanvasGraph,
30
32
  type FactoryCanvasNode,
31
33
  } from "@/components/factory/FactoryCanvas";
34
+ import { FactoryHistoryView } from "@/components/factory/FactoryHistoryView";
32
35
  import { FactoryInspector } from "@/components/factory/FactoryInspector";
33
36
  import { TriageStatusPill } from "@/components/triage/triage-status-pill";
34
37
  import { Button } from "@/components/ui/button";
@@ -78,6 +81,7 @@ type TriageDecision = {
78
81
  type TriageItem = {
79
82
  itemId?: string;
80
83
  id?: string;
84
+ title?: string | null;
81
85
  source?: string | null;
82
86
  sourceName?: string | null;
83
87
  sourceUrl?: string | null;
@@ -87,6 +91,8 @@ type TriageItem = {
87
91
  reason?: string | null;
88
92
  decisionSummary?: string | null;
89
93
  decisions?: TriageDecision[] | null;
94
+ createdAt?: string | null;
95
+ updatedAt?: string | null;
90
96
  };
91
97
 
92
98
  type TriageRule = {
@@ -126,6 +132,7 @@ type WorkspaceTab =
126
132
  | "automations"
127
133
  | "agents"
128
134
  | "audit"
135
+ | "history"
129
136
  | "settings";
130
137
 
131
138
  type FactoryAutomationRun = {
@@ -159,6 +166,7 @@ type FactoryAutomation = {
159
166
  timezone?: string | null;
160
167
  condition?: string | null;
161
168
  canUpdate?: boolean;
169
+ updatedAt?: string | number | null;
162
170
  runs?: FactoryAutomationRun[] | null;
163
171
  pastRuns?: FactoryAutomationRun[] | null;
164
172
  };
@@ -178,9 +186,14 @@ export default function FactoryRoute() {
178
186
  const [creating, setCreating] = useState(false);
179
187
  const [draftGraph, setDraftGraph] = useState<FactoryCanvasGraph | null>(null);
180
188
  const [dirty, setDirty] = useState(false);
189
+ const [saveError, setSaveError] = useState<string | null>(null);
190
+ const [saveConflictRemoteGraph, setSaveConflictRemoteGraph] =
191
+ useState<FactoryCanvasGraph | null>(null);
192
+ const [refreshingFactory, setRefreshingFactory] = useState(false);
181
193
  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
182
194
  const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
183
195
  const [auditRefreshToken, setAuditRefreshToken] = useState(0);
196
+ const draftRevisionRef = useRef(0);
184
197
 
185
198
  function setActiveTab(tab: WorkspaceTab) {
186
199
  setSearchParams(
@@ -188,6 +201,10 @@ export default function FactoryRoute() {
188
201
  const next = new URLSearchParams(current);
189
202
  if (tab === "overview") next.delete("tab");
190
203
  else next.set("tab", tab);
204
+ if (tab === "history") {
205
+ next.delete("node");
206
+ next.delete("edge");
207
+ }
191
208
  return next;
192
209
  },
193
210
  { replace: true },
@@ -249,17 +266,17 @@ export default function FactoryRoute() {
249
266
  const graphData =
250
267
  rawGraphData?.factory.id === factoryId ? rawGraphData : undefined;
251
268
  const graph = draftGraph ?? graphData?.graph ?? null;
252
- const graphVersion = graphData?.factory.graphVersion ?? graph?.version ?? 1;
269
+ const graphVersion = graph?.version ?? graphData?.factory.graphVersion ?? 1;
253
270
  const saveGraphMutation = useActionMutation("save-factory-graph");
254
271
  const factoryList = (factoryListQuery.data ?? []) as FactorySummary[];
255
272
 
256
273
  useEffect(() => {
257
- if (!graphData || creating) return;
274
+ if (!graphData || creating || dirty) return;
258
275
  setDraftGraph(graphData.graph);
259
276
  setDirty(false);
260
277
  setSelectedNodeId(null);
261
278
  setSelectedEdgeId(null);
262
- }, [creating, graphData]);
279
+ }, [creating, dirty, graphData]);
263
280
 
264
281
  useEffect(() => {
265
282
  setSelectedNodeId(searchParams.get("node"));
@@ -307,6 +324,9 @@ export default function FactoryRoute() {
307
324
  }
308
325
 
309
326
  function updateGraph(next: FactoryCanvasGraph) {
327
+ draftRevisionRef.current += 1;
328
+ setSaveError(null);
329
+ setSaveConflictRemoteGraph(null);
310
330
  setDraftGraph(next);
311
331
  setDirty(
312
332
  !graphData?.graph ||
@@ -390,20 +410,87 @@ export default function FactoryRoute() {
390
410
 
391
411
  async function saveGraph() {
392
412
  if (!graph || !selectedFactoryId) return;
393
- await saveGraphMutation.mutateAsync({
394
- factoryId: selectedFactoryId,
395
- name: graph.name,
396
- description: graph.description,
397
- prompt: creating ? "" : (graphData?.factory.prompt ?? ""),
398
- source: "manual",
399
- changeSummary: creating
400
- ? "Created from the Factory visual editor."
401
- : "Updated in the Factory visual editor.",
402
- graph,
403
- });
413
+ const submittedDraftRevision = draftRevisionRef.current;
414
+ setSaveError(null);
415
+ try {
416
+ await saveGraphMutation.mutateAsync({
417
+ factoryId: selectedFactoryId,
418
+ name: graph.name,
419
+ description: graph.description,
420
+ prompt: creating ? "" : (graphData?.factory.prompt ?? ""),
421
+ source: "manual",
422
+ changeSummary: creating
423
+ ? "Created from the Factory visual editor."
424
+ : "Updated in the Factory visual editor.",
425
+ expectedGraphVersion: creating ? 0 : graph.version,
426
+ graph,
427
+ });
428
+ setCreating(false);
429
+ setDirty(draftRevisionRef.current !== submittedDraftRevision);
430
+ setSaveConflictRemoteGraph(null);
431
+ await Promise.all([graphQuery.refetch(), factoryListQuery.refetch()]);
432
+ } catch (error) {
433
+ setSaveError(
434
+ error instanceof Error
435
+ ? error.message
436
+ : t("factoryRoute.saveConflictFallback"),
437
+ );
438
+ }
439
+ }
440
+
441
+ async function refreshFactoryAfterSaveConflict() {
442
+ setRefreshingFactory(true);
443
+ try {
444
+ const results = await Promise.all([
445
+ graphQuery.refetch(),
446
+ factoryListQuery.refetch(),
447
+ ]);
448
+ if (results.some((result) => result.isError)) {
449
+ throw new Error("Factory refresh failed.");
450
+ }
451
+ const remoteGraph = (results[0].data as FactoryGraphResponse | undefined)
452
+ ?.graph;
453
+ if (dirty) {
454
+ if (!remoteGraph) throw new Error("Factory graph refresh failed.");
455
+ setSaveConflictRemoteGraph(remoteGraph);
456
+ setSaveError(t("factoryRoute.saveConflictFallback"));
457
+ return;
458
+ }
459
+ setSaveError(null);
460
+ } catch {
461
+ setSaveError(t("factoryRoute.saveConflictFallback"));
462
+ } finally {
463
+ setRefreshingFactory(false);
464
+ }
465
+ }
466
+
467
+ function discardLocalFactoryChanges() {
468
+ if (!saveConflictRemoteGraph) return;
469
+ setDraftGraph(saveConflictRemoteGraph);
470
+ setSaveConflictRemoteGraph(null);
471
+ setSaveError(null);
472
+ setDirty(false);
404
473
  setCreating(false);
474
+ setSelectedNodeId(null);
475
+ setSelectedEdgeId(null);
476
+ }
477
+
478
+ async function handleFactoryRestored(result?: { graph: FactoryCanvasGraph }) {
479
+ setCreating(false);
480
+ if (result?.graph) setDraftGraph(result.graph);
481
+ else setDraftGraph(null);
405
482
  setDirty(false);
406
- await Promise.all([graphQuery.refetch(), factoryListQuery.refetch()]);
483
+ setSaveError(null);
484
+ setSaveConflictRemoteGraph(null);
485
+ setSelectedNodeId(null);
486
+ setSelectedEdgeId(null);
487
+ const results = await Promise.all([
488
+ graphQuery.refetch(),
489
+ factoryListQuery.refetch(),
490
+ ]);
491
+ if (results.some((result) => result.isError)) {
492
+ throw new Error("Factory view refresh failed.");
493
+ }
407
494
  }
408
495
 
409
496
  if (!selectedFactoryId && activeTab === "agents") {
@@ -653,6 +740,12 @@ export default function FactoryRoute() {
653
740
  >
654
741
  Activity
655
742
  </TabButton>
743
+ <TabButton
744
+ active={activeTab === "history"}
745
+ onClick={() => setActiveTab("history")}
746
+ >
747
+ {t("factoryRoute.historyTab")}
748
+ </TabButton>
656
749
  <TabButton
657
750
  active={activeTab === "settings"}
658
751
  onClick={() => setActiveTab("settings")}
@@ -721,8 +814,13 @@ export default function FactoryRoute() {
721
814
  factoryId={factoryId}
722
815
  dirty={dirty}
723
816
  saving={saveGraphMutation.isPending}
817
+ saveError={saveError}
818
+ saveConflictNeedsResolution={Boolean(saveConflictRemoteGraph)}
819
+ refreshing={refreshingFactory}
724
820
  onGraphChange={updateGraph}
725
821
  onSave={() => void saveGraph()}
822
+ onRefresh={refreshFactoryAfterSaveConflict}
823
+ onDiscardLocalChanges={discardLocalFactoryChanges}
726
824
  onAddNode={addNode}
727
825
  onDeleteNode={deleteNode}
728
826
  onConnect={connectNodes}
@@ -741,6 +839,13 @@ export default function FactoryRoute() {
741
839
  factoryId={factoryId}
742
840
  refreshToken={auditRefreshToken}
743
841
  />
842
+ ) : activeTab === "history" ? (
843
+ <FactoryHistoryView
844
+ key={factoryId}
845
+ factoryId={factoryId}
846
+ hasUnsavedChanges={dirty}
847
+ onRestored={handleFactoryRestored}
848
+ />
744
849
  ) : (
745
850
  <SettingsView t={t} />
746
851
  )}
@@ -777,6 +882,7 @@ function parseWorkspaceTab(value: string | null): WorkspaceTab {
777
882
  value === "automations" ||
778
883
  value === "agents" ||
779
884
  value === "audit" ||
885
+ value === "history" ||
780
886
  value === "settings"
781
887
  ? value
782
888
  : "overview";
@@ -976,7 +1082,8 @@ function AutomationsView({
976
1082
  current.body === nextDraft.body &&
977
1083
  current.model === nextDraft.model &&
978
1084
  current.schedule === nextDraft.schedule &&
979
- current.enabled === nextDraft.enabled
1085
+ current.enabled === nextDraft.enabled &&
1086
+ current.updatedAt === nextDraft.updatedAt
980
1087
  ) {
981
1088
  return current;
982
1089
  }
@@ -986,16 +1093,25 @@ function AutomationsView({
986
1093
 
987
1094
  async function saveAutomation() {
988
1095
  if (!draft) return;
989
- await saveMutation.mutateAsync({
990
- factoryId,
991
- automationId: draft.id,
992
- name: draft.name,
993
- prompt: draft.prompt ?? draft.body ?? "",
994
- model: draft.model ?? "",
995
- schedule: draft.schedule ?? "",
996
- enabled: draft.enabled,
997
- });
998
- await automationsQuery.refetch();
1096
+ try {
1097
+ await saveMutation.mutateAsync({
1098
+ factoryId,
1099
+ automationId: draft.id,
1100
+ name: draft.name,
1101
+ prompt: draft.prompt ?? draft.body ?? "",
1102
+ model: draft.model ?? "",
1103
+ schedule: draft.schedule ?? "",
1104
+ enabled: draft.enabled,
1105
+ });
1106
+ await automationsQuery.refetch();
1107
+ toast.success(t("factoryRoute.automationSaved"));
1108
+ } catch (error) {
1109
+ toast.error(
1110
+ error instanceof Error
1111
+ ? error.message
1112
+ : t("factoryRoute.automationSaveFailed"),
1113
+ );
1114
+ }
999
1115
  }
1000
1116
 
1001
1117
  async function runAutomation() {
@@ -1031,27 +1147,34 @@ function AutomationsView({
1031
1147
  role="tablist"
1032
1148
  aria-label={t("factoryRoute.automationsTitle")}
1033
1149
  >
1034
- {automations.map((automation) => (
1035
- <button
1036
- key={automation.id}
1037
- type="button"
1038
- id={`factory-automation-tab-${automation.id}`}
1039
- role="tab"
1040
- aria-selected={activeAutomationId === automation.id}
1041
- aria-controls="factory-automation-panel"
1042
- className={`w-full cursor-pointer rounded-lg bg-muted/20 p-4 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${activeAutomationId === automation.id ? "bg-muted/60" : ""}`}
1043
- onClick={() => selectAutomation(automation.id)}
1044
- >
1045
- <span className="block truncate text-sm font-medium">
1046
- {automation.name}
1047
- </span>
1048
- <span className="mt-1 block truncate text-xs text-muted-foreground">
1049
- {automation.enabled
1050
- ? t("factoryRoute.automationEnabled")
1051
- : t("factoryRoute.automationDisabled")}
1052
- </span>
1053
- </button>
1054
- ))}
1150
+ {automations.map((automation) => {
1151
+ const selected = activeAutomationId === automation.id;
1152
+ return (
1153
+ <button
1154
+ key={automation.id}
1155
+ type="button"
1156
+ id={`factory-automation-tab-${automation.id}`}
1157
+ role="tab"
1158
+ aria-selected={selected}
1159
+ aria-controls="factory-automation-panel"
1160
+ className={`w-full cursor-pointer rounded-lg p-4 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${
1161
+ selected
1162
+ ? "bg-primary/10 ring-1 ring-inset ring-primary/40"
1163
+ : "bg-muted/20 hover:bg-muted/50"
1164
+ }`}
1165
+ onClick={() => selectAutomation(automation.id)}
1166
+ >
1167
+ <span className="block truncate text-sm font-medium">
1168
+ {automation.name}
1169
+ </span>
1170
+ <span className="mt-1 block truncate text-xs text-muted-foreground">
1171
+ {automation.enabled
1172
+ ? t("factoryRoute.automationEnabled")
1173
+ : t("factoryRoute.automationDisabled")}
1174
+ </span>
1175
+ </button>
1176
+ );
1177
+ })}
1055
1178
  </div>
1056
1179
  )}
1057
1180
  </CardContent>
@@ -1189,8 +1312,24 @@ function AutomationsView({
1189
1312
  </div>
1190
1313
  </div>
1191
1314
  <div className="grid gap-1.5">
1192
- <Label>{t("factoryRoute.automationPrompt")}</Label>
1315
+ <div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
1316
+ <Label htmlFor="factory-automation-prompt">
1317
+ {t("factoryRoute.automationPrompt")}
1318
+ </Label>
1319
+ {draft.updatedAt ? (
1320
+ <span className="capitalize text-xs text-muted-foreground">
1321
+ {t("factoryRoute.automationLastUpdated")}{" "}
1322
+ <time
1323
+ dateTime={new Date(draft.updatedAt).toISOString()}
1324
+ title={formatAutomationDate(draft.updatedAt)}
1325
+ >
1326
+ {formatInboxDateTime(draft.updatedAt)}
1327
+ </time>
1328
+ </span>
1329
+ ) : null}
1330
+ </div>
1193
1331
  <Textarea
1332
+ id="factory-automation-prompt"
1194
1333
  value={draft.prompt ?? draft.body ?? ""}
1195
1334
  onChange={(event) =>
1196
1335
  setDraft({ ...draft, prompt: event.target.value })
@@ -1226,11 +1365,25 @@ function AutomationsView({
1226
1365
  </span>
1227
1366
  {run.threadId && (
1228
1367
  <a
1229
- className="inline-flex items-center gap-1 text-xs text-muted-foreground underline-offset-4 hover:underline"
1368
+ className="inline-flex items-center text-xs text-muted-foreground underline-offset-4 hover:underline"
1230
1369
  href={`/chat/${encodeURIComponent(run.threadId)}`}
1370
+ onClick={(event) => {
1371
+ if (
1372
+ event.metaKey ||
1373
+ event.ctrlKey ||
1374
+ event.shiftKey ||
1375
+ event.altKey ||
1376
+ event.button !== 0
1377
+ ) {
1378
+ return;
1379
+ }
1380
+ event.preventDefault();
1381
+ requestAgentChatThreadOpen({
1382
+ threadId: run.threadId!,
1383
+ });
1384
+ }}
1231
1385
  >
1232
1386
  {t("factoryRoute.automationOpenThread")}
1233
- <IconExternalLink className="size-3" />
1234
1387
  </a>
1235
1388
  )}
1236
1389
  {run.error && (
@@ -1259,6 +1412,52 @@ function formatAutomationDate(value: string | number | null | undefined) {
1259
1412
  return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
1260
1413
  }
1261
1414
 
1415
+ function formatInboxDateTime(value: string | number | null | undefined) {
1416
+ if (!value) return null;
1417
+ const date = new Date(value);
1418
+ if (Number.isNaN(date.getTime())) return String(value);
1419
+ return date.toLocaleString(undefined, {
1420
+ month: "short",
1421
+ day: "numeric",
1422
+ year: "numeric",
1423
+ hour: "numeric",
1424
+ minute: "2-digit",
1425
+ });
1426
+ }
1427
+
1428
+ function formatInboxAge(value: string | number | null | undefined) {
1429
+ if (!value) return null;
1430
+ const date = new Date(value);
1431
+ if (Number.isNaN(date.getTime())) return String(value);
1432
+
1433
+ const elapsedSeconds = Math.max(
1434
+ 0,
1435
+ Math.floor((Date.now() - date.getTime()) / 1_000),
1436
+ );
1437
+ if (elapsedSeconds < 60) return "now";
1438
+ const elapsedMinutes = Math.floor(elapsedSeconds / 60);
1439
+ if (elapsedMinutes < 60) return `${elapsedMinutes}m`;
1440
+ const elapsedHours = Math.floor(elapsedMinutes / 60);
1441
+ if (elapsedHours < 24) return `${elapsedHours}h`;
1442
+ const elapsedDays = Math.floor(elapsedHours / 24);
1443
+ if (elapsedDays < 30) return `${elapsedDays}d`;
1444
+ const elapsedMonths = Math.floor(elapsedDays / 30);
1445
+ if (elapsedMonths < 12) return `${elapsedMonths}mo`;
1446
+ return `${Math.floor(elapsedMonths / 12)}y`;
1447
+ }
1448
+
1449
+ const reviewListColumns =
1450
+ "w-full gap-3 sm:grid-cols-[minmax(0,1.3fr)_minmax(4.75rem,auto)_minmax(5.5rem,auto)_minmax(4.5rem,auto)_minmax(0,1.3fr)] sm:items-start";
1451
+
1452
+ function formatInboxSource(source: string | null | undefined) {
1453
+ const normalized = source?.toLowerCase() ?? "";
1454
+ if (normalized.includes("slack")) return "Slack";
1455
+ if (normalized.includes("github")) return "GitHub";
1456
+ if (normalized.includes("sentry")) return "Sentry";
1457
+ const trimmed = source?.trim();
1458
+ return trimmed ? trimmed : "Source";
1459
+ }
1460
+
1262
1461
  function formatModelName(model: string | null | undefined) {
1263
1462
  const value = model?.trim();
1264
1463
  if (!value) return "the app default";
@@ -1279,6 +1478,7 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
1279
1478
  );
1280
1479
  const [feedbackNote, setFeedbackNote] = useState("");
1281
1480
  const [verdict, setVerdict] = useState<Verdict | null>(null);
1481
+ const selectedRowRef = useRef<HTMLButtonElement | null>(null);
1282
1482
  const listQuery = useActionQuery("list-triage-items", {
1283
1483
  limit: 50,
1284
1484
  ...(status.trim()
@@ -1312,6 +1512,14 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
1312
1512
  setSelectedId(searchParams.get("itemId"));
1313
1513
  }, [searchParams]);
1314
1514
 
1515
+ useEffect(() => {
1516
+ if (!selectedId) return;
1517
+ selectedRowRef.current?.scrollIntoView({
1518
+ block: "nearest",
1519
+ inline: "nearest",
1520
+ });
1521
+ }, [selectedId, normalizedItems.length]);
1522
+
1315
1523
  function selectItem(itemId: string) {
1316
1524
  setSelectedId(itemId);
1317
1525
  setVerdict(null);
@@ -1371,19 +1579,51 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
1371
1579
  </p>
1372
1580
  ) : (
1373
1581
  <div className="grid gap-1.5 p-2">
1582
+ <div
1583
+ className={`hidden px-4 py-2 text-[11px] uppercase tracking-wide text-muted-foreground sm:grid ${reviewListColumns}`}
1584
+ >
1585
+ <span />
1586
+ <span>{t("triage.risk")}</span>
1587
+ <span>{t("triage.status")}</span>
1588
+ <span>{t("triage.updatedAt")}</span>
1589
+ <span>{t("triage.reason")}</span>
1590
+ </div>
1374
1591
  {normalizedItems.map((item) => {
1375
1592
  const id = item.itemId ?? item.id ?? "";
1593
+ const selected = selectedId === id;
1594
+ const createdAtLabel = formatInboxDateTime(item.createdAt);
1595
+ const updatedAge = formatInboxAge(item.updatedAt);
1376
1596
  return (
1377
1597
  <button
1378
1598
  key={id}
1599
+ ref={selected ? selectedRowRef : undefined}
1379
1600
  type="button"
1380
- className={`grid w-full gap-3 rounded-lg bg-muted/20 p-4 text-left transition-colors hover:bg-muted/50 sm:grid-cols-[1.1fr_.7fr_.9fr_1.6fr] ${selectedId === id ? "bg-muted/60" : ""}`}
1601
+ aria-current={selected ? "true" : undefined}
1602
+ className={`grid rounded-lg px-4 py-3 text-left transition-colors ${reviewListColumns} ${
1603
+ selected
1604
+ ? "bg-primary/10 ring-1 ring-inset ring-primary/40"
1605
+ : "bg-muted/20 hover:bg-muted/50"
1606
+ }`}
1381
1607
  onClick={() => selectItem(id)}
1382
1608
  >
1383
1609
  <span className="min-w-0">
1610
+ <span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
1611
+ {formatInboxSource(item.source ?? item.sourceName)}
1612
+ </span>
1384
1613
  <span className="block truncate text-sm font-medium">
1385
- {item.sourceName ?? item.source ?? "Unknown source"}
1614
+ {item.title?.trim() ||
1615
+ item.sourceName ||
1616
+ item.source ||
1617
+ "Untitled"}
1386
1618
  </span>
1619
+ {createdAtLabel && item.createdAt && (
1620
+ <time
1621
+ className="mt-0.5 block text-xs text-muted-foreground"
1622
+ dateTime={item.createdAt}
1623
+ >
1624
+ {createdAtLabel}
1625
+ </time>
1626
+ )}
1387
1627
  {item.sourceUrl && (
1388
1628
  <span className="mt-1 flex max-w-full items-center gap-1 truncate text-xs text-primary">
1389
1629
  {item.sourceUrl}
@@ -1392,20 +1632,36 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
1392
1632
  )}
1393
1633
  </span>
1394
1634
  <span>
1395
- <span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
1396
- Risk
1635
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
1636
+ {t("triage.risk")}
1397
1637
  </span>
1398
1638
  <TriageStatusPill status={item.risk} />
1399
1639
  </span>
1400
1640
  <span>
1401
- <span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
1402
- Status
1641
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
1642
+ {t("triage.status")}
1403
1643
  </span>
1404
1644
  <TriageStatusPill status={item.status} />
1405
1645
  </span>
1406
- <span className="truncate">
1407
- <span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
1408
- Reason
1646
+ <span>
1647
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
1648
+ {t("triage.updatedAt")}
1649
+ </span>
1650
+ {updatedAge && item.updatedAt ? (
1651
+ <time
1652
+ className="text-sm tabular-nums text-muted-foreground"
1653
+ dateTime={item.updatedAt}
1654
+ title={formatAutomationDate(item.updatedAt)}
1655
+ >
1656
+ {updatedAge}
1657
+ </time>
1658
+ ) : (
1659
+ <span className="text-sm text-muted-foreground">-</span>
1660
+ )}
1661
+ </span>
1662
+ <span className="min-w-0 truncate">
1663
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
1664
+ {t("triage.reason")}
1409
1665
  </span>
1410
1666
  <span className="text-sm">
1411
1667
  {item.reason ?? item.decisionSummary ?? "-"}
@@ -1431,8 +1687,15 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
1431
1687
  <>
1432
1688
  <div className="flex items-start justify-between gap-3">
1433
1689
  <div>
1690
+ <p className="text-[11px] uppercase tracking-wide text-muted-foreground">
1691
+ {formatInboxSource(
1692
+ selectedItem.source ?? selectedItem.sourceName,
1693
+ )}
1694
+ </p>
1434
1695
  <p className="text-sm font-medium">
1435
- {selectedItem.sourceName ?? selectedItem.source}
1696
+ {selectedItem.title?.trim() ||
1697
+ selectedItem.sourceName ||
1698
+ selectedItem.source}
1436
1699
  </p>
1437
1700
  {selectedItem.sourceUrl && (
1438
1701
  <a
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+
1
3
  export type Workspace = "primary" | "secondary";
2
4
 
3
5
  export type SlackTokenResolver = (workspace: Workspace) => Promise<string>;
@@ -20,6 +22,12 @@ export interface SlackTeamInfo {
20
22
  domain: string;
21
23
  }
22
24
 
25
+ export interface SlackUserInfo {
26
+ id: string;
27
+ name: string | null;
28
+ displayName: string | null;
29
+ }
30
+
23
31
  export interface ChannelHistoryResult {
24
32
  messages: SlackMessage[];
25
33
  has_more: boolean;
@@ -65,16 +73,20 @@ async function getToken(
65
73
  );
66
74
  }
67
75
 
76
+ function slackCacheScope(token: string): string {
77
+ return createHash("sha256").update(token).digest("hex").slice(0, 16);
78
+ }
79
+
68
80
  async function slackApi<T>(
69
81
  workspace: Workspace,
70
82
  method: string,
71
83
  params: Record<string, string> | undefined,
72
84
  tokenResolver?: SlackTokenResolver,
73
85
  ): Promise<T> {
74
- const cacheKey = `${workspace}:${method}:${JSON.stringify(params ?? {})}`;
86
+ const token = await getToken(workspace, tokenResolver);
87
+ const cacheKey = `${workspace}:${slackCacheScope(token)}:${method}:${JSON.stringify(params ?? {})}`;
75
88
  const cached = cache.get(cacheKey);
76
89
  if (cached && cached.expiresAt > Date.now()) return cached.value as T;
77
- const token = await getToken(workspace, tokenResolver);
78
90
  const url = new URL(`https://slack.com/api/${method}`);
79
91
  for (const [key, value] of Object.entries(params ?? {}))
80
92
  url.searchParams.set(key, value);
@@ -255,3 +267,33 @@ export async function getTeamInfo(
255
267
  );
256
268
  return data.team ?? { id: data.team_id ?? "", name: workspace, domain: "" };
257
269
  }
270
+
271
+ export async function getUserInfo(
272
+ workspace: Workspace,
273
+ userId: string,
274
+ tokenResolver?: SlackTokenResolver,
275
+ ): Promise<SlackUserInfo> {
276
+ const id = userId.trim();
277
+ if (!id) throw new Error("A Slack user id is required.");
278
+ const data = await slackApi<{
279
+ user?: {
280
+ id?: string;
281
+ name?: string;
282
+ profile?: { display_name?: string };
283
+ };
284
+ }>(workspace, "users.info", { user: id }, tokenResolver);
285
+ const user = data.user;
286
+ if (!user || typeof user.id !== "string" || !user.id.trim()) {
287
+ throw new Error("Slack user response is missing a user id.");
288
+ }
289
+ const displayName =
290
+ typeof user.profile?.display_name === "string"
291
+ ? user.profile.display_name.trim()
292
+ : "";
293
+ const name = typeof user.name === "string" ? user.name.trim() : "";
294
+ return {
295
+ id: user.id.trim(),
296
+ name: name || null,
297
+ displayName: displayName || null,
298
+ };
299
+ }