@agent-native/core 0.164.3 → 0.164.11

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 (234) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
  3. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
  4. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
  5. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
  6. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
  7. package/corpus/templates/calendar/app/i18n-data.ts +120 -0
  8. package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
  9. package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
  10. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  11. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  12. package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  13. package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  14. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +1 -1
  15. package/corpus/templates/clips/app/components/player/video-player.tsx +12 -0
  16. package/corpus/templates/clips/app/routes/_app.dictate.tsx +18 -4
  17. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +114 -20
  18. package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -78
  19. package/corpus/templates/clips/changelog/2026-08-19-clips-nightly-builds-are-available-for-trying-the-latest-upd.md +6 -0
  20. package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +6 -0
  21. package/corpus/templates/clips/desktop/README.md +19 -4
  22. package/corpus/templates/clips/desktop/src/app.tsx +78 -16
  23. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -2
  24. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -0
  25. package/corpus/templates/clips/server/lib/slack-unfurls.ts +7 -13
  26. package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  27. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
  28. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  29. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
  30. package/corpus/templates/clips/server/routes/api/clips-updater.json.get.ts +41 -22
  31. package/corpus/templates/clips/shared/share-meta.ts +67 -3
  32. package/corpus/templates/content/actions/list-content-databases.ts +95 -1
  33. package/corpus/templates/content/changelog/2026-08-11-content-mutations-delegated-from-verified-slack-dms-now-use-.md +6 -0
  34. package/corpus/templates/content/shared/api.ts +11 -0
  35. package/corpus/templates/factory/AGENTS.md +1 -1
  36. package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
  37. package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
  38. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
  39. package/corpus/templates/factory/app/i18n/en-US.ts +4 -0
  40. package/corpus/templates/factory/app/routes/factory.tsx +209 -46
  41. package/corpus/templates/factory/server/connectors/slack.ts +44 -2
  42. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -7
  43. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
  44. package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
  45. package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
  46. package/corpus/templates/mail/actions/view-screen.ts +1 -1
  47. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
  48. package/corpus/templates/mail/server/handlers/google-auth.ts +125 -24
  49. package/corpus/templates/mail/server/lib/gmail-query.ts +5 -2
  50. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  51. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  52. package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  53. package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  54. package/corpus/templates/plan/app/pages/PlansPage.tsx +0 -12
  55. package/corpus/templates/plan/changelog/2026-08-19-plan-prompts-now-guide-you-to-connect-ai-before-sending-and-.md +6 -0
  56. package/corpus/templates/slides/actions/approve-deck-access-request.ts +234 -0
  57. package/corpus/templates/slides/actions/get-deck-access-status.ts +35 -2
  58. package/corpus/templates/slides/actions/request-deck-access.ts +263 -64
  59. package/corpus/templates/slides/app/components/editor/MissingDeckAccessPane.tsx +196 -0
  60. package/corpus/templates/slides/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
  61. package/corpus/templates/slides/app/hooks/use-deck-access.ts +7 -4
  62. package/corpus/templates/slides/app/i18n/en-US.ts +29 -6
  63. package/corpus/templates/slides/app/pages/DeckEditor.tsx +147 -169
  64. package/corpus/templates/slides/app/routes/access-request.approve.tsx +250 -0
  65. package/corpus/templates/slides/server/db/schema.ts +8 -0
  66. package/corpus/templates/slides/server/lib/access-request-email.ts +36 -0
  67. package/corpus/templates/slides/server/lib/emails.ts +26 -0
  68. package/corpus/templates/slides/server/plugins/agent-chat.ts +7 -0
  69. package/corpus/templates/slides/server/plugins/db.ts +50 -0
  70. package/corpus/templates/slides/shared/deck-access.ts +31 -0
  71. package/dist/a2a/artifact-response.d.ts +50 -0
  72. package/dist/a2a/artifact-response.js +266 -39
  73. package/dist/a2a/index.d.ts +2 -0
  74. package/dist/a2a/index.js +1 -0
  75. package/dist/a2a/task-store.d.ts +1 -0
  76. package/dist/a2a/task-store.js +1 -1
  77. package/dist/agent/observational-memory/store.d.ts +1 -0
  78. package/dist/agent/observational-memory/store.js +1 -1
  79. package/dist/agent/run-store.d.ts +1 -0
  80. package/dist/agent/run-store.js +1 -1
  81. package/dist/agent/tool-approval-store.d.ts +1 -0
  82. package/dist/agent/tool-approval-store.js +1 -1
  83. package/dist/application-state/store.d.ts +1 -0
  84. package/dist/application-state/store.js +1 -1
  85. package/dist/browser-sessions/store.d.ts +1 -0
  86. package/dist/browser-sessions/store.js +1 -1
  87. package/dist/checkpoints/store.d.ts +1 -0
  88. package/dist/checkpoints/store.js +1 -1
  89. package/dist/cli/code-agent-schedules.d.ts +11 -0
  90. package/dist/cli/code-agent-schedules.js +44 -5
  91. package/dist/client/AgentPanel.d.ts +5 -1
  92. package/dist/client/AgentPanel.js +2 -2
  93. package/dist/client/AssistantChat.d.ts +5 -1
  94. package/dist/client/AssistantChat.js +40 -6
  95. package/dist/client/EnvironmentBadge.d.ts +7 -13
  96. package/dist/client/EnvironmentBadge.js +43 -92
  97. package/dist/client/agent-chat/index.d.ts +1 -0
  98. package/dist/client/agent-chat/index.js +1 -0
  99. package/dist/client/agent-chat-adapter.js +76 -50
  100. package/dist/client/app-providers.js +4 -4
  101. package/dist/client/chat/message-components.d.ts +4 -0
  102. package/dist/client/chat/message-components.js +28 -1
  103. package/dist/client/chat/run-recovery.js +26 -0
  104. package/dist/client/chat-first/apps-rail.d.ts +1 -0
  105. package/dist/client/chat-first/apps-rail.js +28 -2
  106. package/dist/client/sse-event-processor.js +13 -8
  107. package/dist/client/use-active-agent-chat-run.d.ts +2 -0
  108. package/dist/client/use-active-agent-chat-run.js +22 -0
  109. package/dist/coding-tools/sandbox/executions-store.d.ts +1 -0
  110. package/dist/coding-tools/sandbox/executions-store.js +1 -1
  111. package/dist/collab/awareness-store.d.ts +1 -0
  112. package/dist/collab/awareness-store.js +1 -1
  113. package/dist/collab/routes.d.ts +1 -1
  114. package/dist/collab/storage.d.ts +1 -0
  115. package/dist/collab/storage.js +1 -1
  116. package/dist/db/client.js +3 -3
  117. package/dist/db/ddl-guard.js +8 -0
  118. package/dist/db/migration-runtime.d.ts +23 -0
  119. package/dist/db/migration-runtime.js +41 -0
  120. package/dist/db/migrations.d.ts +1 -2
  121. package/dist/db/migrations.js +2 -24
  122. package/dist/email-catalog/log.d.ts +1 -0
  123. package/dist/email-catalog/log.js +1 -1
  124. package/dist/guards/index.d.ts +1 -0
  125. package/dist/guards/index.js +1 -0
  126. package/dist/guards/release-schema-complete.d.ts +22 -0
  127. package/dist/guards/release-schema-complete.js +144 -0
  128. package/dist/integrations/a2a-continuations-store.d.ts +1 -0
  129. package/dist/integrations/a2a-continuations-store.js +1 -1
  130. package/dist/integrations/adapters/slack.js +58 -7
  131. package/dist/integrations/awaiting-input-store.d.ts +1 -0
  132. package/dist/integrations/awaiting-input-store.js +1 -1
  133. package/dist/integrations/config-store.d.ts +1 -0
  134. package/dist/integrations/config-store.js +1 -1
  135. package/dist/integrations/controls-store.d.ts +1 -0
  136. package/dist/integrations/controls-store.js +1 -1
  137. package/dist/integrations/identity-links-store.d.ts +1 -0
  138. package/dist/integrations/identity-links-store.js +1 -1
  139. package/dist/integrations/installations-store.d.ts +1 -0
  140. package/dist/integrations/installations-store.js +1 -1
  141. package/dist/integrations/remote-commands-store.d.ts +1 -0
  142. package/dist/integrations/remote-commands-store.js +1 -1
  143. package/dist/integrations/remote-devices-store.d.ts +1 -0
  144. package/dist/integrations/remote-devices-store.js +1 -1
  145. package/dist/integrations/remote-push-store.d.ts +1 -0
  146. package/dist/integrations/remote-push-store.js +1 -1
  147. package/dist/integrations/remote-run-events-store.d.ts +1 -0
  148. package/dist/integrations/remote-run-events-store.js +1 -1
  149. package/dist/integrations/scope-store.d.ts +1 -0
  150. package/dist/integrations/scope-store.js +1 -1
  151. package/dist/integrations/thread-mapping-store.d.ts +1 -0
  152. package/dist/integrations/thread-mapping-store.js +1 -1
  153. package/dist/integrations/usage-budget-store.d.ts +1 -0
  154. package/dist/integrations/usage-budget-store.js +1 -1
  155. package/dist/jobs/background-automation-runner.js +111 -9
  156. package/dist/jobs/run-history.d.ts +1 -0
  157. package/dist/jobs/run-history.js +1 -1
  158. package/dist/jobs/scheduler-health.d.ts +1 -0
  159. package/dist/jobs/scheduler-health.js +1 -1
  160. package/dist/mcp/approval-store.d.ts +1 -0
  161. package/dist/mcp/approval-store.js +1 -1
  162. package/dist/mcp/connect-store.d.ts +1 -0
  163. package/dist/mcp/connect-store.js +1 -1
  164. package/dist/mcp/oauth-store.d.ts +1 -0
  165. package/dist/mcp/oauth-store.js +1 -1
  166. package/dist/notifications/routes.d.ts +3 -3
  167. package/dist/notifications/store.d.ts +1 -0
  168. package/dist/notifications/store.js +1 -1
  169. package/dist/oauth-tokens/store.d.ts +1 -0
  170. package/dist/oauth-tokens/store.js +1 -1
  171. package/dist/observability/routes.d.ts +3 -3
  172. package/dist/progress/routes.d.ts +1 -1
  173. package/dist/progress/store.d.ts +1 -0
  174. package/dist/progress/store.js +1 -1
  175. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  176. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  177. package/dist/provider-api/corpus-jobs-store.d.ts +1 -0
  178. package/dist/provider-api/corpus-jobs-store.js +1 -1
  179. package/dist/provider-api/custom-registry.d.ts +1 -0
  180. package/dist/provider-api/custom-registry.js +1 -1
  181. package/dist/provider-api/quota-governor.d.ts +1 -0
  182. package/dist/provider-api/quota-governor.js +1 -1
  183. package/dist/provider-api/staged-datasets-store.d.ts +1 -0
  184. package/dist/provider-api/staged-datasets-store.js +1 -1
  185. package/dist/resources/handlers.d.ts +3 -3
  186. package/dist/resources/store.d.ts +1 -0
  187. package/dist/resources/store.js +1 -1
  188. package/dist/scripts/dev/index.js +15 -1
  189. package/dist/search/index.js +3 -0
  190. package/dist/secrets/storage.d.ts +1 -0
  191. package/dist/secrets/storage.js +1 -1
  192. package/dist/server/agent-chat/action-filters-a2a.d.ts +2 -1
  193. package/dist/server/agent-chat/action-filters-a2a.js +10 -2
  194. package/dist/server/agent-chat-plugin.js +71 -20
  195. package/dist/server/agent-teams-run-queue.d.ts +1 -0
  196. package/dist/server/agent-teams-run-queue.js +1 -1
  197. package/dist/server/auth-marketing.js +3 -1
  198. package/dist/server/auth.d.ts +18 -1
  199. package/dist/server/auth.js +370 -154
  200. package/dist/server/embed-session.d.ts +1 -0
  201. package/dist/server/embed-session.js +1 -1
  202. package/dist/server/google-auth-plugin.js +3 -1
  203. package/dist/server/google-oauth.d.ts +10 -0
  204. package/dist/server/google-oauth.js +25 -1
  205. package/dist/server/identity-sso-store.d.ts +1 -0
  206. package/dist/server/identity-sso-store.js +1 -1
  207. package/dist/server/index.d.ts +1 -1
  208. package/dist/server/index.js +1 -1
  209. package/dist/server/onboarding-html.js +200 -31
  210. package/dist/server/poll.d.ts +2 -0
  211. package/dist/server/poll.js +19 -1
  212. package/dist/server/realtime-token.d.ts +1 -1
  213. package/dist/server/release-migrations.js +5 -0
  214. package/dist/server/release-schema.d.ts +46 -0
  215. package/dist/server/release-schema.js +307 -0
  216. package/dist/settings/store.d.ts +1 -0
  217. package/dist/settings/store.js +1 -1
  218. package/dist/shared/environment-lanes.d.ts +28 -0
  219. package/dist/shared/environment-lanes.js +61 -0
  220. package/dist/sharing/actions/share-resource.js +22 -2
  221. package/dist/styles/agent-native.css +17 -15
  222. package/dist/triggers/routes.d.ts +11 -0
  223. package/dist/triggers/routes.js +10 -0
  224. package/dist/usage/alerts-store.d.ts +1 -0
  225. package/dist/usage/alerts-store.js +1 -1
  226. package/dist/usage/store.d.ts +1 -0
  227. package/dist/usage/store.js +1 -1
  228. package/docs/content/drop-in-agent.mdx +4 -4
  229. package/docs/content/faq.mdx +15 -15
  230. package/docs/content/getting-started.mdx +173 -41
  231. package/docs/content/key-concepts.mdx +10 -10
  232. package/docs/content/using-your-agent.mdx +2 -2
  233. package/docs/content/what-is-agent-native.mdx +79 -228
  234. package/package.json +2 -2
@@ -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";
@@ -78,6 +80,7 @@ type TriageDecision = {
78
80
  type TriageItem = {
79
81
  itemId?: string;
80
82
  id?: string;
83
+ title?: string | null;
81
84
  source?: string | null;
82
85
  sourceName?: string | null;
83
86
  sourceUrl?: string | null;
@@ -87,6 +90,8 @@ type TriageItem = {
87
90
  reason?: string | null;
88
91
  decisionSummary?: string | null;
89
92
  decisions?: TriageDecision[] | null;
93
+ createdAt?: string | null;
94
+ updatedAt?: string | null;
90
95
  };
91
96
 
92
97
  type TriageRule = {
@@ -159,6 +164,7 @@ type FactoryAutomation = {
159
164
  timezone?: string | null;
160
165
  condition?: string | null;
161
166
  canUpdate?: boolean;
167
+ updatedAt?: string | number | null;
162
168
  runs?: FactoryAutomationRun[] | null;
163
169
  pastRuns?: FactoryAutomationRun[] | null;
164
170
  };
@@ -976,7 +982,8 @@ function AutomationsView({
976
982
  current.body === nextDraft.body &&
977
983
  current.model === nextDraft.model &&
978
984
  current.schedule === nextDraft.schedule &&
979
- current.enabled === nextDraft.enabled
985
+ current.enabled === nextDraft.enabled &&
986
+ current.updatedAt === nextDraft.updatedAt
980
987
  ) {
981
988
  return current;
982
989
  }
@@ -986,16 +993,25 @@ function AutomationsView({
986
993
 
987
994
  async function saveAutomation() {
988
995
  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();
996
+ try {
997
+ await saveMutation.mutateAsync({
998
+ factoryId,
999
+ automationId: draft.id,
1000
+ name: draft.name,
1001
+ prompt: draft.prompt ?? draft.body ?? "",
1002
+ model: draft.model ?? "",
1003
+ schedule: draft.schedule ?? "",
1004
+ enabled: draft.enabled,
1005
+ });
1006
+ await automationsQuery.refetch();
1007
+ toast.success(t("factoryRoute.automationSaved"));
1008
+ } catch (error) {
1009
+ toast.error(
1010
+ error instanceof Error
1011
+ ? error.message
1012
+ : t("factoryRoute.automationSaveFailed"),
1013
+ );
1014
+ }
999
1015
  }
1000
1016
 
1001
1017
  async function runAutomation() {
@@ -1031,27 +1047,34 @@ function AutomationsView({
1031
1047
  role="tablist"
1032
1048
  aria-label={t("factoryRoute.automationsTitle")}
1033
1049
  >
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
- ))}
1050
+ {automations.map((automation) => {
1051
+ const selected = activeAutomationId === automation.id;
1052
+ return (
1053
+ <button
1054
+ key={automation.id}
1055
+ type="button"
1056
+ id={`factory-automation-tab-${automation.id}`}
1057
+ role="tab"
1058
+ aria-selected={selected}
1059
+ aria-controls="factory-automation-panel"
1060
+ 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 ${
1061
+ selected
1062
+ ? "bg-primary/10 ring-1 ring-inset ring-primary/40"
1063
+ : "bg-muted/20 hover:bg-muted/50"
1064
+ }`}
1065
+ onClick={() => selectAutomation(automation.id)}
1066
+ >
1067
+ <span className="block truncate text-sm font-medium">
1068
+ {automation.name}
1069
+ </span>
1070
+ <span className="mt-1 block truncate text-xs text-muted-foreground">
1071
+ {automation.enabled
1072
+ ? t("factoryRoute.automationEnabled")
1073
+ : t("factoryRoute.automationDisabled")}
1074
+ </span>
1075
+ </button>
1076
+ );
1077
+ })}
1055
1078
  </div>
1056
1079
  )}
1057
1080
  </CardContent>
@@ -1189,8 +1212,24 @@ function AutomationsView({
1189
1212
  </div>
1190
1213
  </div>
1191
1214
  <div className="grid gap-1.5">
1192
- <Label>{t("factoryRoute.automationPrompt")}</Label>
1215
+ <div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
1216
+ <Label htmlFor="factory-automation-prompt">
1217
+ {t("factoryRoute.automationPrompt")}
1218
+ </Label>
1219
+ {draft.updatedAt ? (
1220
+ <span className="capitalize text-xs text-muted-foreground">
1221
+ {t("factoryRoute.automationLastUpdated")}{" "}
1222
+ <time
1223
+ dateTime={new Date(draft.updatedAt).toISOString()}
1224
+ title={formatAutomationDate(draft.updatedAt)}
1225
+ >
1226
+ {formatInboxDateTime(draft.updatedAt)}
1227
+ </time>
1228
+ </span>
1229
+ ) : null}
1230
+ </div>
1193
1231
  <Textarea
1232
+ id="factory-automation-prompt"
1194
1233
  value={draft.prompt ?? draft.body ?? ""}
1195
1234
  onChange={(event) =>
1196
1235
  setDraft({ ...draft, prompt: event.target.value })
@@ -1226,11 +1265,25 @@ function AutomationsView({
1226
1265
  </span>
1227
1266
  {run.threadId && (
1228
1267
  <a
1229
- className="inline-flex items-center gap-1 text-xs text-muted-foreground underline-offset-4 hover:underline"
1268
+ className="inline-flex items-center text-xs text-muted-foreground underline-offset-4 hover:underline"
1230
1269
  href={`/chat/${encodeURIComponent(run.threadId)}`}
1270
+ onClick={(event) => {
1271
+ if (
1272
+ event.metaKey ||
1273
+ event.ctrlKey ||
1274
+ event.shiftKey ||
1275
+ event.altKey ||
1276
+ event.button !== 0
1277
+ ) {
1278
+ return;
1279
+ }
1280
+ event.preventDefault();
1281
+ requestAgentChatThreadOpen({
1282
+ threadId: run.threadId!,
1283
+ });
1284
+ }}
1231
1285
  >
1232
1286
  {t("factoryRoute.automationOpenThread")}
1233
- <IconExternalLink className="size-3" />
1234
1287
  </a>
1235
1288
  )}
1236
1289
  {run.error && (
@@ -1259,6 +1312,52 @@ function formatAutomationDate(value: string | number | null | undefined) {
1259
1312
  return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
1260
1313
  }
1261
1314
 
1315
+ function formatInboxDateTime(value: string | number | null | undefined) {
1316
+ if (!value) return null;
1317
+ const date = new Date(value);
1318
+ if (Number.isNaN(date.getTime())) return String(value);
1319
+ return date.toLocaleString(undefined, {
1320
+ month: "short",
1321
+ day: "numeric",
1322
+ year: "numeric",
1323
+ hour: "numeric",
1324
+ minute: "2-digit",
1325
+ });
1326
+ }
1327
+
1328
+ function formatInboxAge(value: string | number | null | undefined) {
1329
+ if (!value) return null;
1330
+ const date = new Date(value);
1331
+ if (Number.isNaN(date.getTime())) return String(value);
1332
+
1333
+ const elapsedSeconds = Math.max(
1334
+ 0,
1335
+ Math.floor((Date.now() - date.getTime()) / 1_000),
1336
+ );
1337
+ if (elapsedSeconds < 60) return "now";
1338
+ const elapsedMinutes = Math.floor(elapsedSeconds / 60);
1339
+ if (elapsedMinutes < 60) return `${elapsedMinutes}m`;
1340
+ const elapsedHours = Math.floor(elapsedMinutes / 60);
1341
+ if (elapsedHours < 24) return `${elapsedHours}h`;
1342
+ const elapsedDays = Math.floor(elapsedHours / 24);
1343
+ if (elapsedDays < 30) return `${elapsedDays}d`;
1344
+ const elapsedMonths = Math.floor(elapsedDays / 30);
1345
+ if (elapsedMonths < 12) return `${elapsedMonths}mo`;
1346
+ return `${Math.floor(elapsedMonths / 12)}y`;
1347
+ }
1348
+
1349
+ const reviewListColumns =
1350
+ "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";
1351
+
1352
+ function formatInboxSource(source: string | null | undefined) {
1353
+ const normalized = source?.toLowerCase() ?? "";
1354
+ if (normalized.includes("slack")) return "Slack";
1355
+ if (normalized.includes("github")) return "GitHub";
1356
+ if (normalized.includes("sentry")) return "Sentry";
1357
+ const trimmed = source?.trim();
1358
+ return trimmed ? trimmed : "Source";
1359
+ }
1360
+
1262
1361
  function formatModelName(model: string | null | undefined) {
1263
1362
  const value = model?.trim();
1264
1363
  if (!value) return "the app default";
@@ -1279,6 +1378,7 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
1279
1378
  );
1280
1379
  const [feedbackNote, setFeedbackNote] = useState("");
1281
1380
  const [verdict, setVerdict] = useState<Verdict | null>(null);
1381
+ const selectedRowRef = useRef<HTMLButtonElement | null>(null);
1282
1382
  const listQuery = useActionQuery("list-triage-items", {
1283
1383
  limit: 50,
1284
1384
  ...(status.trim()
@@ -1312,6 +1412,14 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
1312
1412
  setSelectedId(searchParams.get("itemId"));
1313
1413
  }, [searchParams]);
1314
1414
 
1415
+ useEffect(() => {
1416
+ if (!selectedId) return;
1417
+ selectedRowRef.current?.scrollIntoView({
1418
+ block: "nearest",
1419
+ inline: "nearest",
1420
+ });
1421
+ }, [selectedId, normalizedItems.length]);
1422
+
1315
1423
  function selectItem(itemId: string) {
1316
1424
  setSelectedId(itemId);
1317
1425
  setVerdict(null);
@@ -1371,19 +1479,51 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
1371
1479
  </p>
1372
1480
  ) : (
1373
1481
  <div className="grid gap-1.5 p-2">
1482
+ <div
1483
+ className={`hidden px-4 py-2 text-[11px] uppercase tracking-wide text-muted-foreground sm:grid ${reviewListColumns}`}
1484
+ >
1485
+ <span />
1486
+ <span>{t("triage.risk")}</span>
1487
+ <span>{t("triage.status")}</span>
1488
+ <span>{t("triage.updatedAt")}</span>
1489
+ <span>{t("triage.reason")}</span>
1490
+ </div>
1374
1491
  {normalizedItems.map((item) => {
1375
1492
  const id = item.itemId ?? item.id ?? "";
1493
+ const selected = selectedId === id;
1494
+ const createdAtLabel = formatInboxDateTime(item.createdAt);
1495
+ const updatedAge = formatInboxAge(item.updatedAt);
1376
1496
  return (
1377
1497
  <button
1378
1498
  key={id}
1499
+ ref={selected ? selectedRowRef : undefined}
1379
1500
  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" : ""}`}
1501
+ aria-current={selected ? "true" : undefined}
1502
+ className={`grid rounded-lg px-4 py-3 text-left transition-colors ${reviewListColumns} ${
1503
+ selected
1504
+ ? "bg-primary/10 ring-1 ring-inset ring-primary/40"
1505
+ : "bg-muted/20 hover:bg-muted/50"
1506
+ }`}
1381
1507
  onClick={() => selectItem(id)}
1382
1508
  >
1383
1509
  <span className="min-w-0">
1510
+ <span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
1511
+ {formatInboxSource(item.source ?? item.sourceName)}
1512
+ </span>
1384
1513
  <span className="block truncate text-sm font-medium">
1385
- {item.sourceName ?? item.source ?? "Unknown source"}
1514
+ {item.title?.trim() ||
1515
+ item.sourceName ||
1516
+ item.source ||
1517
+ "Untitled"}
1386
1518
  </span>
1519
+ {createdAtLabel && item.createdAt && (
1520
+ <time
1521
+ className="mt-0.5 block text-xs text-muted-foreground"
1522
+ dateTime={item.createdAt}
1523
+ >
1524
+ {createdAtLabel}
1525
+ </time>
1526
+ )}
1387
1527
  {item.sourceUrl && (
1388
1528
  <span className="mt-1 flex max-w-full items-center gap-1 truncate text-xs text-primary">
1389
1529
  {item.sourceUrl}
@@ -1392,20 +1532,36 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
1392
1532
  )}
1393
1533
  </span>
1394
1534
  <span>
1395
- <span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
1396
- Risk
1535
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
1536
+ {t("triage.risk")}
1397
1537
  </span>
1398
1538
  <TriageStatusPill status={item.risk} />
1399
1539
  </span>
1400
1540
  <span>
1401
- <span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
1402
- Status
1541
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
1542
+ {t("triage.status")}
1403
1543
  </span>
1404
1544
  <TriageStatusPill status={item.status} />
1405
1545
  </span>
1406
- <span className="truncate">
1407
- <span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
1408
- Reason
1546
+ <span>
1547
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
1548
+ {t("triage.updatedAt")}
1549
+ </span>
1550
+ {updatedAge && item.updatedAt ? (
1551
+ <time
1552
+ className="text-sm tabular-nums text-muted-foreground"
1553
+ dateTime={item.updatedAt}
1554
+ title={formatAutomationDate(item.updatedAt)}
1555
+ >
1556
+ {updatedAge}
1557
+ </time>
1558
+ ) : (
1559
+ <span className="text-sm text-muted-foreground">-</span>
1560
+ )}
1561
+ </span>
1562
+ <span className="min-w-0 truncate">
1563
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
1564
+ {t("triage.reason")}
1409
1565
  </span>
1410
1566
  <span className="text-sm">
1411
1567
  {item.reason ?? item.decisionSummary ?? "-"}
@@ -1431,8 +1587,15 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
1431
1587
  <>
1432
1588
  <div className="flex items-start justify-between gap-3">
1433
1589
  <div>
1590
+ <p className="text-[11px] uppercase tracking-wide text-muted-foreground">
1591
+ {formatInboxSource(
1592
+ selectedItem.source ?? selectedItem.sourceName,
1593
+ )}
1594
+ </p>
1434
1595
  <p className="text-sm font-medium">
1435
- {selectedItem.sourceName ?? selectedItem.source}
1596
+ {selectedItem.title?.trim() ||
1597
+ selectedItem.sourceName ||
1598
+ selectedItem.source}
1436
1599
  </p>
1437
1600
  {selectedItem.sourceUrl && (
1438
1601
  <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
+ }
@@ -89,13 +89,15 @@ Builder, auto-approve, or auto-merge those items. Slack clear bugs use the
89
89
  thread-preserving start-builder-for-item flow; GitHub and Sentry clear bugs use
90
90
  the Builder agent-run flow. Slack repeat reports must be clustered by underlying
91
91
  symptom, with one Builder thread for the cluster and 👀 on every grouped report.
92
- Use /address-feedback for the repository feedback workflow. For pull requests,
93
- follow review-prs: read the complete diff and review evidence, verify current
94
- BuilderIO membership, preserve the ultra-scary safety gate, and distinguish
95
- unknown or unresolved checks from clean ones. Auto-merge also requires a
96
- verified Factory Builder run. When a user says to do a review-gated item now,
97
- use the explicit approval action, which records the approver and applies the
98
- rule's configured executor policy. Keep Slack replies concise and link to the
92
+ After classifying an item, call start-builder-for-item with clearBug true or
93
+ false and a short reason so a skip is recorded. Use /address-feedback for the
94
+ repository feedback workflow. For pull requests, follow review-prs: read the
95
+ complete diff and review evidence, verify current BuilderIO membership, preserve
96
+ the ultra-scary safety gate, and distinguish unknown or unresolved checks from
97
+ clean ones. Auto-merge also requires a verified Factory Builder run. When a user
98
+ says to do a review-gated item now, use the explicit approval action, which
99
+ records the approver and applies the rule's configured executor policy. Keep
100
+ Slack replies concise and link to the
99
101
  Factory item when a review is needed. The scheduled builder-io-bot PR babysitter
100
102
  posts its exact feedback-fix request through GitHub, persists a 20-minute quiet
101
103
  window, and never approves or merges.`,
@@ -169,6 +169,8 @@ const FACTORY_DEFAULT_MODEL =
169
169
  process.env.FACTORY_AUTOMATION_MODEL?.trim() || "gpt-5.6-luna";
170
170
  const FACTORY_DEFAULT_MAX_ITERATIONS = 32;
171
171
  const FACTORY_DEFAULT_MAX_RUN_INPUT_TOKENS = 1_000_000;
172
+ const SKIP_RECORD_GUARD =
173
+ "After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.";
172
174
 
173
175
  const AUTOMATION_SEEDS: AutomationSeed[] = [
174
176
  {
@@ -224,6 +226,7 @@ the representative source, every related source, and the need to fix the
224
226
  underlying boundary across the whole cluster. Never add the reaction or tag
225
227
  Builder for owner-managed Clips, Design, or Content work, or for a non-bug
226
228
  report.
229
+ After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.
227
230
 
228
231
  Keep each run bounded. Preserve action errors and do not claim a Builder reply,
229
232
  PR, merge, or fix unless an action returned that state.
@@ -254,6 +257,7 @@ For each eligible clear bug, call start-builder-for-item with clearBug true,
254
257
  an evidence-grounded reason, and clearErrorReport containing only the bounded
255
258
  Sentry evidence. Builder should open a PR; do not claim it did so until the
256
259
  run callback or PR observation confirms it.
260
+ After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.
257
261
  `,
258
262
  },
259
263
  {
@@ -281,6 +285,7 @@ For each eligible item call start-builder-for-item with clearBug true,
281
285
  evidence-grounded reason, and the bounded issue body as clearErrorReport.
282
286
  Preserve failures and never report a successful Builder run without its action
283
287
  confirmation.
288
+ After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.
284
289
  `,
285
290
  },
286
291
  {
@@ -522,6 +527,14 @@ async function ensureOrganizationAutomations(
522
527
  if (promptGuard && !repaired.includes(promptGuard)) {
523
528
  repaired = `${repaired.trimEnd()}\n\n${promptGuard}\n`;
524
529
  }
530
+ if (
531
+ (seed.name === "factory-slack-feedback" ||
532
+ seed.name === "factory-sentry-errors" ||
533
+ seed.name === "factory-github-issues") &&
534
+ !repaired.includes(SKIP_RECORD_GUARD)
535
+ ) {
536
+ repaired = `${repaired.trimEnd()}\n\n${SKIP_RECORD_GUARD}\n`;
537
+ }
525
538
  if (repaired === existing.content) return;
526
539
 
527
540
  const updated = await resourcePutIfCurrent({
@@ -4,11 +4,13 @@ import {
4
4
  addEyesReaction as writeEyesReaction,
5
5
  getThread as readThread,
6
6
  getTeamInfo as readTeamInfo,
7
+ getUserInfo as readUserInfo,
7
8
  postThreadReply as writeThreadReply,
8
9
  type ChannelHistoryResult,
9
10
  type SlackPostMessageResult,
10
11
  type SlackReactionResult,
11
12
  type SlackTeamInfo,
13
+ type SlackUserInfo,
12
14
  type SlackTokenResolver,
13
15
  type ThreadRepliesResult,
14
16
  type Workspace,
@@ -57,6 +59,9 @@ export function createSlackReader(identity: SlackReaderIdentity) {
57
59
  getTeamInfo(workspace: Workspace): Promise<SlackTeamInfo> {
58
60
  return readTeamInfo(workspace, tokenResolver);
59
61
  },
62
+ getUserInfo(workspace: Workspace, userId: string): Promise<SlackUserInfo> {
63
+ return readUserInfo(workspace, userId, tokenResolver);
64
+ },
60
65
  getThread(
61
66
  workspace: Workspace,
62
67
  channelId: string,
@@ -5,6 +5,7 @@ import { createSlackReader } from "./slack-client";
5
5
  const SLACK_HISTORY_LIMIT = 100;
6
6
  const MAX_HISTORY_PAGES = 5;
7
7
  const MAX_SUMMARY_LENGTH = 500;
8
+ export const SLACK_USER_INFO_CONCURRENCY = 4;
8
9
 
9
10
  export interface SlackPollInput {
10
11
  workspace: Workspace;
@@ -37,17 +38,75 @@ function compactText(text: string): string {
37
38
  : compact;
38
39
  }
39
40
 
40
- function messageLabel(message: SlackMessage): string {
41
+ function messageLabel(message: SlackMessage, resolvedLabel?: string): string {
41
42
  if (message.bot_id || message.username) {
42
43
  return `Slack bot ${message.username ?? message.bot_id ?? "unknown"}`;
43
44
  }
44
- return `Slack user ${message.user ?? "unknown"}`;
45
+ return `Slack user ${resolvedLabel ?? message.user ?? "unknown"}`;
46
+ }
47
+
48
+ function slackUserLabel(
49
+ user: { name: string | null; displayName: string | null } | null,
50
+ userId: string,
51
+ ): string {
52
+ const displayName = user?.displayName?.trim();
53
+ if (displayName) return displayName;
54
+ const name = user?.name?.trim();
55
+ if (name) return name.startsWith("@") ? name : `@${name}`;
56
+ return userId;
57
+ }
58
+
59
+ async function resolveUserLabels(
60
+ slack: ReturnType<typeof createSlackReader>,
61
+ workspace: Workspace,
62
+ messages: SlackMessage[],
63
+ ): Promise<Map<string, string>> {
64
+ const userIds = [
65
+ ...new Set(
66
+ messages
67
+ .filter((message) => !message.bot_id && !message.username)
68
+ .map((message) => message.user?.trim())
69
+ .filter((userId): userId is string => Boolean(userId)),
70
+ ),
71
+ ];
72
+ const labels = new Map<string, string>();
73
+ await mapWithConcurrency(
74
+ userIds,
75
+ SLACK_USER_INFO_CONCURRENCY,
76
+ async (userId) => {
77
+ try {
78
+ const user = await slack.getUserInfo(workspace, userId);
79
+ labels.set(userId, slackUserLabel(user, userId));
80
+ } catch {
81
+ labels.set(userId, userId);
82
+ }
83
+ },
84
+ );
85
+ return labels;
86
+ }
87
+
88
+ async function mapWithConcurrency<T>(
89
+ values: T[],
90
+ concurrency: number,
91
+ mapper: (value: T) => Promise<void>,
92
+ ): Promise<void> {
93
+ let nextIndex = 0;
94
+ const worker = async () => {
95
+ while (nextIndex < values.length) {
96
+ const index = nextIndex++;
97
+ await mapper(values[index]!);
98
+ }
99
+ };
100
+ await Promise.all(
101
+ Array.from({ length: Math.min(concurrency, values.length) }, worker),
102
+ );
45
103
  }
46
104
 
47
105
  function toEnvelope(
48
106
  channelId: string,
49
107
  message: SlackMessage,
50
108
  teamDomain?: string,
109
+ userLabels?: Map<string, string>,
51
110
  ): IngestionEnvelope {
52
111
  const threadTs = message.thread_ts ?? message.ts;
53
112
  const suppliedPermalink = (message as SlackMessage & { permalink?: string })
@@ -63,7 +122,10 @@ function toEnvelope(
63
122
  externalId: `${channelId}:${threadTs}`,
64
123
  receivedAt: new Date().toISOString(),
65
124
  ...(sourceUrl ? { sourceUrl } : {}),
66
- title: messageLabel(message),
125
+ title: messageLabel(
126
+ message,
127
+ message.user ? userLabels?.get(message.user) : undefined,
128
+ ),
67
129
  summary: compactText(message.text),
68
130
  channelId,
69
131
  threadTs,
@@ -149,9 +211,15 @@ export async function pollSlackChannel({
149
211
  { value: priorTs, raw: priorLastSlackTs },
150
212
  );
151
213
 
214
+ const userLabels = await resolveUserLabels(
215
+ slack,
216
+ workspace,
217
+ uniqueNewMessages.map(({ message }) => message),
218
+ );
219
+
152
220
  return {
153
221
  envelopes: uniqueNewMessages.map(({ message }) =>
154
- toEnvelope(channelId, message, teamDomain),
222
+ toEnvelope(channelId, message, teamDomain, userLabels),
155
223
  ),
156
224
  nextLastSlackTs: maxSeen.raw,
157
225
  nextHistoryCursor,