@agent-native/core 0.164.4 → 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 (214) 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/app/components/player/video-player.tsx +12 -0
  15. package/corpus/templates/clips/app/routes/_app.dictate.tsx +18 -4
  16. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +114 -20
  17. package/corpus/templates/clips/app/routes/share.$shareId.tsx +83 -78
  18. package/corpus/templates/clips/changelog/2026-08-19-clips-nightly-builds-are-available-for-trying-the-latest-upd.md +6 -0
  19. package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +1 -0
  20. package/corpus/templates/clips/desktop/README.md +19 -4
  21. package/corpus/templates/clips/desktop/src/app.tsx +78 -16
  22. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -2
  23. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -0
  24. package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  25. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
  26. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  27. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
  28. package/corpus/templates/clips/server/routes/api/clips-updater.json.get.ts +41 -22
  29. package/corpus/templates/factory/AGENTS.md +1 -1
  30. package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
  31. package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
  32. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
  33. package/corpus/templates/factory/app/i18n/en-US.ts +4 -0
  34. package/corpus/templates/factory/app/routes/factory.tsx +209 -46
  35. package/corpus/templates/factory/server/connectors/slack.ts +44 -2
  36. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -7
  37. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
  38. package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
  39. package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
  40. package/corpus/templates/mail/actions/view-screen.ts +1 -1
  41. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
  42. package/corpus/templates/mail/server/handlers/google-auth.ts +125 -24
  43. package/corpus/templates/mail/server/lib/gmail-query.ts +5 -2
  44. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  45. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  46. package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  47. package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  48. package/corpus/templates/plan/app/pages/PlansPage.tsx +0 -12
  49. package/corpus/templates/plan/changelog/2026-08-19-plan-prompts-now-guide-you-to-connect-ai-before-sending-and-.md +6 -0
  50. package/corpus/templates/slides/actions/approve-deck-access-request.ts +234 -0
  51. package/corpus/templates/slides/actions/get-deck-access-status.ts +35 -2
  52. package/corpus/templates/slides/actions/request-deck-access.ts +263 -64
  53. package/corpus/templates/slides/app/components/editor/MissingDeckAccessPane.tsx +196 -0
  54. package/corpus/templates/slides/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
  55. package/corpus/templates/slides/app/hooks/use-deck-access.ts +7 -4
  56. package/corpus/templates/slides/app/i18n/en-US.ts +29 -6
  57. package/corpus/templates/slides/app/pages/DeckEditor.tsx +147 -169
  58. package/corpus/templates/slides/app/routes/access-request.approve.tsx +250 -0
  59. package/corpus/templates/slides/server/db/schema.ts +8 -0
  60. package/corpus/templates/slides/server/lib/access-request-email.ts +36 -0
  61. package/corpus/templates/slides/server/lib/emails.ts +26 -0
  62. package/corpus/templates/slides/server/plugins/agent-chat.ts +7 -0
  63. package/corpus/templates/slides/server/plugins/db.ts +50 -0
  64. package/corpus/templates/slides/shared/deck-access.ts +31 -0
  65. package/dist/a2a/task-store.d.ts +1 -0
  66. package/dist/a2a/task-store.js +1 -1
  67. package/dist/agent/observational-memory/store.d.ts +1 -0
  68. package/dist/agent/observational-memory/store.js +1 -1
  69. package/dist/agent/run-store.d.ts +1 -0
  70. package/dist/agent/run-store.js +1 -1
  71. package/dist/agent/tool-approval-store.d.ts +1 -0
  72. package/dist/agent/tool-approval-store.js +1 -1
  73. package/dist/application-state/store.d.ts +1 -0
  74. package/dist/application-state/store.js +1 -1
  75. package/dist/browser-sessions/store.d.ts +1 -0
  76. package/dist/browser-sessions/store.js +1 -1
  77. package/dist/checkpoints/store.d.ts +1 -0
  78. package/dist/checkpoints/store.js +1 -1
  79. package/dist/cli/code-agent-schedules.d.ts +11 -0
  80. package/dist/cli/code-agent-schedules.js +44 -5
  81. package/dist/client/AgentPanel.d.ts +5 -1
  82. package/dist/client/AgentPanel.js +2 -2
  83. package/dist/client/AssistantChat.d.ts +5 -1
  84. package/dist/client/AssistantChat.js +40 -6
  85. package/dist/client/EnvironmentBadge.d.ts +7 -13
  86. package/dist/client/EnvironmentBadge.js +43 -92
  87. package/dist/client/agent-chat/index.d.ts +1 -0
  88. package/dist/client/agent-chat/index.js +1 -0
  89. package/dist/client/agent-chat-adapter.js +76 -50
  90. package/dist/client/app-providers.js +4 -4
  91. package/dist/client/chat/message-components.d.ts +4 -0
  92. package/dist/client/chat/message-components.js +28 -1
  93. package/dist/client/chat/run-recovery.js +26 -0
  94. package/dist/client/chat-first/apps-rail.d.ts +1 -0
  95. package/dist/client/chat-first/apps-rail.js +28 -2
  96. package/dist/client/sse-event-processor.js +13 -8
  97. package/dist/client/use-active-agent-chat-run.d.ts +2 -0
  98. package/dist/client/use-active-agent-chat-run.js +22 -0
  99. package/dist/coding-tools/sandbox/executions-store.d.ts +1 -0
  100. package/dist/coding-tools/sandbox/executions-store.js +1 -1
  101. package/dist/collab/awareness-store.d.ts +1 -0
  102. package/dist/collab/awareness-store.js +1 -1
  103. package/dist/collab/awareness.d.ts +2 -2
  104. package/dist/collab/routes.d.ts +1 -1
  105. package/dist/collab/storage.d.ts +1 -0
  106. package/dist/collab/storage.js +1 -1
  107. package/dist/db/client.js +3 -3
  108. package/dist/db/ddl-guard.js +8 -0
  109. package/dist/db/migration-runtime.d.ts +23 -0
  110. package/dist/db/migration-runtime.js +41 -0
  111. package/dist/db/migrations.d.ts +1 -2
  112. package/dist/db/migrations.js +2 -24
  113. package/dist/email-catalog/log.d.ts +1 -0
  114. package/dist/email-catalog/log.js +1 -1
  115. package/dist/guards/index.d.ts +1 -0
  116. package/dist/guards/index.js +1 -0
  117. package/dist/guards/release-schema-complete.d.ts +22 -0
  118. package/dist/guards/release-schema-complete.js +144 -0
  119. package/dist/integrations/a2a-continuations-store.d.ts +1 -0
  120. package/dist/integrations/a2a-continuations-store.js +1 -1
  121. package/dist/integrations/awaiting-input-store.d.ts +1 -0
  122. package/dist/integrations/awaiting-input-store.js +1 -1
  123. package/dist/integrations/config-store.d.ts +1 -0
  124. package/dist/integrations/config-store.js +1 -1
  125. package/dist/integrations/controls-store.d.ts +1 -0
  126. package/dist/integrations/controls-store.js +1 -1
  127. package/dist/integrations/identity-links-store.d.ts +1 -0
  128. package/dist/integrations/identity-links-store.js +1 -1
  129. package/dist/integrations/installations-store.d.ts +1 -0
  130. package/dist/integrations/installations-store.js +1 -1
  131. package/dist/integrations/remote-commands-store.d.ts +1 -0
  132. package/dist/integrations/remote-commands-store.js +1 -1
  133. package/dist/integrations/remote-devices-store.d.ts +1 -0
  134. package/dist/integrations/remote-devices-store.js +1 -1
  135. package/dist/integrations/remote-push-store.d.ts +1 -0
  136. package/dist/integrations/remote-push-store.js +1 -1
  137. package/dist/integrations/remote-run-events-store.d.ts +1 -0
  138. package/dist/integrations/remote-run-events-store.js +1 -1
  139. package/dist/integrations/scope-store.d.ts +1 -0
  140. package/dist/integrations/scope-store.js +1 -1
  141. package/dist/integrations/thread-mapping-store.d.ts +1 -0
  142. package/dist/integrations/thread-mapping-store.js +1 -1
  143. package/dist/integrations/usage-budget-store.d.ts +1 -0
  144. package/dist/integrations/usage-budget-store.js +1 -1
  145. package/dist/jobs/background-automation-runner.js +111 -9
  146. package/dist/jobs/run-history.d.ts +1 -0
  147. package/dist/jobs/run-history.js +1 -1
  148. package/dist/jobs/scheduler-health.d.ts +1 -0
  149. package/dist/jobs/scheduler-health.js +1 -1
  150. package/dist/mcp/approval-store.d.ts +1 -0
  151. package/dist/mcp/approval-store.js +1 -1
  152. package/dist/mcp/connect-store.d.ts +1 -0
  153. package/dist/mcp/connect-store.js +1 -1
  154. package/dist/mcp/oauth-store.d.ts +1 -0
  155. package/dist/mcp/oauth-store.js +1 -1
  156. package/dist/notifications/routes.d.ts +3 -3
  157. package/dist/notifications/store.d.ts +1 -0
  158. package/dist/notifications/store.js +1 -1
  159. package/dist/oauth-tokens/store.d.ts +1 -0
  160. package/dist/oauth-tokens/store.js +1 -1
  161. package/dist/observability/routes.d.ts +3 -3
  162. package/dist/progress/store.d.ts +1 -0
  163. package/dist/progress/store.js +1 -1
  164. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  165. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  166. package/dist/provider-api/corpus-jobs-store.d.ts +1 -0
  167. package/dist/provider-api/corpus-jobs-store.js +1 -1
  168. package/dist/provider-api/custom-registry.d.ts +1 -0
  169. package/dist/provider-api/custom-registry.js +1 -1
  170. package/dist/provider-api/quota-governor.d.ts +1 -0
  171. package/dist/provider-api/quota-governor.js +1 -1
  172. package/dist/provider-api/staged-datasets-store.d.ts +1 -0
  173. package/dist/provider-api/staged-datasets-store.js +1 -1
  174. package/dist/resources/handlers.d.ts +3 -3
  175. package/dist/resources/store.d.ts +1 -0
  176. package/dist/resources/store.js +1 -1
  177. package/dist/scripts/dev/index.js +15 -1
  178. package/dist/search/index.js +3 -0
  179. package/dist/secrets/storage.d.ts +1 -0
  180. package/dist/secrets/storage.js +1 -1
  181. package/dist/server/agent-chat-plugin.js +38 -9
  182. package/dist/server/agent-teams-run-queue.d.ts +1 -0
  183. package/dist/server/agent-teams-run-queue.js +1 -1
  184. package/dist/server/auth-marketing.js +3 -1
  185. package/dist/server/auth.d.ts +18 -1
  186. package/dist/server/auth.js +370 -154
  187. package/dist/server/embed-session.d.ts +1 -0
  188. package/dist/server/embed-session.js +1 -1
  189. package/dist/server/google-auth-plugin.js +3 -1
  190. package/dist/server/google-oauth.d.ts +10 -0
  191. package/dist/server/google-oauth.js +25 -1
  192. package/dist/server/identity-sso-store.d.ts +1 -0
  193. package/dist/server/identity-sso-store.js +1 -1
  194. package/dist/server/index.d.ts +1 -1
  195. package/dist/server/index.js +1 -1
  196. package/dist/server/onboarding-html.js +200 -31
  197. package/dist/server/poll.d.ts +2 -0
  198. package/dist/server/poll.js +19 -1
  199. package/dist/server/release-migrations.js +5 -0
  200. package/dist/server/release-schema.d.ts +46 -0
  201. package/dist/server/release-schema.js +307 -0
  202. package/dist/settings/store.d.ts +1 -0
  203. package/dist/settings/store.js +1 -1
  204. package/dist/shared/environment-lanes.d.ts +28 -0
  205. package/dist/shared/environment-lanes.js +61 -0
  206. package/dist/sharing/actions/share-resource.js +22 -2
  207. package/dist/styles/agent-native.css +17 -15
  208. package/dist/triggers/routes.d.ts +11 -0
  209. package/dist/triggers/routes.js +10 -0
  210. package/dist/usage/alerts-store.d.ts +1 -0
  211. package/dist/usage/alerts-store.js +1 -1
  212. package/dist/usage/store.d.ts +1 -0
  213. package/dist/usage/store.js +1 -1
  214. package/package.json +2 -2
@@ -4,10 +4,14 @@ import {
4
4
  isElectron,
5
5
  resolveGoogleSignInCredentials,
6
6
  resolveOAuthRedirectUri,
7
+ registerDesktopExchange,
8
+ prepareDesktopOAuthBrowserBinding,
7
9
  safeReturnPath,
8
10
  } from "@agent-native/core/server";
9
11
  import {
10
12
  defineEventHandler,
13
+ getHeader,
14
+ getMethod,
11
15
  getQuery,
12
16
  setResponseStatus,
13
17
  type H3Event,
@@ -54,11 +58,39 @@ export default defineEventHandler(async (event: H3Event) => {
54
58
  isElectron(event) || q.desktop === "1" || q.desktop === "true";
55
59
  const flowId =
56
60
  desktop && typeof q.flow_id === "string" ? q.flow_id : undefined;
61
+ if (getMethod(event) === "POST" && (!desktop || !flowId)) {
62
+ setResponseStatus(event, 400);
63
+ return { error: "Invalid desktop exchange challenge." };
64
+ }
65
+ const calendarConnect =
66
+ q.calendar === "1" || q.calendar === "true" || q.product === "calendar";
67
+ let desktopVerifierHash: string | undefined;
68
+ let desktopBrowserBindingHash: string | undefined;
69
+ if (flowId && !calendarConnect) {
70
+ if (getMethod(event) !== "POST" || q.redirect !== undefined) {
71
+ setResponseStatus(event, 400);
72
+ return { error: "Invalid desktop exchange challenge." };
73
+ }
74
+ const verifier = getHeader(event, "x-agent-native-desktop-verifier");
75
+ if (!verifier || q.verifier !== undefined) {
76
+ setResponseStatus(event, 400);
77
+ return { error: "Invalid desktop exchange challenge." };
78
+ }
79
+ try {
80
+ desktopBrowserBindingHash = prepareDesktopOAuthBrowserBinding(event);
81
+ desktopVerifierHash = await registerDesktopExchange(
82
+ flowId,
83
+ verifier,
84
+ desktopBrowserBindingHash,
85
+ );
86
+ } catch {
87
+ setResponseStatus(event, 400);
88
+ return { error: "Invalid desktop exchange challenge." };
89
+ }
90
+ }
57
91
  const requestedReturn =
58
92
  typeof q.return === "string" ? safeReturnPath(q.return) : "/";
59
93
  const returnUrl = requestedReturn !== "/" ? requestedReturn : undefined;
60
- const calendarConnect =
61
- q.calendar === "1" || q.calendar === "true" || q.product === "calendar";
62
94
  const credentials = calendarConnect
63
95
  ? ((await resolveGoogleOAuthCredentialCandidates())[0] ?? null)
64
96
  : resolveGoogleSignInCredentials();
@@ -84,10 +116,16 @@ export default defineEventHandler(async (event: H3Event) => {
84
116
  redirectUri,
85
117
  owner,
86
118
  desktop,
119
+ desktopWebview: desktop && q.webview === "1" && !calendarConnect,
87
120
  addAccount: calendarConnect,
88
121
  app: CLIPS_GOOGLE_OAUTH_APP_ID,
89
- returnUrl,
122
+ returnUrl:
123
+ desktop && q.webview === "1" && !calendarConnect
124
+ ? "/?desktop_auth=complete"
125
+ : returnUrl,
90
126
  flowId: calendarConnect ? undefined : flowId,
127
+ desktopVerifierHash,
128
+ desktopBrowserBindingHash,
91
129
  });
92
130
 
93
131
  const params = new URLSearchParams({
@@ -0,0 +1 @@
1
+ export { default } from "./auth-url.get.js";
@@ -6,6 +6,7 @@ import {
6
6
  oauthErrorPage,
7
7
  resolveGoogleSignInCredentials,
8
8
  resolveOAuthOwner,
9
+ matchesDesktopOAuthBrowserBinding,
9
10
  setDesktopExchange,
10
11
  type OAuthStatePayload,
11
12
  } from "@agent-native/core/server";
@@ -31,6 +32,17 @@ async function handleGoogleSignInCallback(
31
32
  ) {
32
33
  const desktop = state.desktop;
33
34
  const flowId = state.flowId;
35
+ if (
36
+ flowId &&
37
+ (!state.desktopVerifierHash ||
38
+ !state.desktopBrowserBindingHash ||
39
+ !matchesDesktopOAuthBrowserBinding(
40
+ event,
41
+ state.desktopBrowserBindingHash,
42
+ ))
43
+ ) {
44
+ return oauthErrorPage("Desktop OAuth browser binding is invalid.");
45
+ }
34
46
 
35
47
  try {
36
48
  const query = getQuery(event);
@@ -107,12 +119,21 @@ async function handleGoogleSignInCallback(
107
119
  });
108
120
 
109
121
  if (flowId && sessionToken) {
110
- setDesktopExchange(flowId, sessionToken, email);
122
+ if (!state.desktopVerifierHash) {
123
+ throw new Error("Missing desktop exchange challenge.");
124
+ }
125
+ await setDesktopExchange(
126
+ flowId,
127
+ sessionToken,
128
+ email,
129
+ state.desktopVerifierHash,
130
+ );
111
131
  }
112
132
 
113
133
  return oauthCallbackResponse(event, email, {
114
134
  sessionToken,
115
135
  desktop,
136
+ desktopWebview: state.desktopWebview,
116
137
  returnUrl: state.returnUrl,
117
138
  flowId,
118
139
  appName: "Clips",
@@ -1,17 +1,23 @@
1
- import { defineEventHandler, setResponseHeaders } from "h3";
1
+ import { defineEventHandler, getQuery, setResponseHeaders } from "h3";
2
2
 
3
3
  /**
4
4
  * Tauri updater manifest endpoint for the Clips desktop app.
5
5
  *
6
6
  * The installed app only needs this endpoint to be valid JSON when no signed
7
- * updater bundle is available. If the `clips-latest` GitHub pointer release
8
- * exists, we proxy its signed manifest. If it does not, return a deliberately
9
- * old no-update manifest so the desktop UI stays quiet instead of surfacing a
7
+ * updater bundle is available. If the channel's GitHub pointer release exists,
8
+ * we proxy its signed manifest. If it does not, return a deliberately old
9
+ * no-update manifest so the desktop UI stays quiet instead of surfacing a
10
10
  * release-channel setup error to end users.
11
11
  */
12
12
 
13
- const GITHUB_MANIFEST_URL =
14
- "https://github.com/BuilderIO/agent-native/releases/download/clips-latest/clips-latest.json";
13
+ type ClipsUpdateChannel = "production" | "nightly";
14
+
15
+ const GITHUB_MANIFEST_URL: Record<ClipsUpdateChannel, string> = {
16
+ production:
17
+ "https://github.com/BuilderIO/agent-native/releases/download/clips-latest/clips-latest.json",
18
+ nightly:
19
+ "https://github.com/BuilderIO/agent-native/releases/download/clips-nightly-latest/clips-nightly-latest.json",
20
+ };
15
21
  const CACHE_TTL_MS = 5 * 60_000;
16
22
 
17
23
  // Tauri throws a red-banner error if the requesting client's target triple is
@@ -48,8 +54,8 @@ export const INERT_MANIFEST = {
48
54
  },
49
55
  };
50
56
 
51
- let cache: { data: unknown; ts: number } | null = null;
52
- let inFlight: Promise<unknown> | null = null;
57
+ const cache = new Map<ClipsUpdateChannel, { data: unknown; ts: number }>();
58
+ const inFlight = new Map<ClipsUpdateChannel, Promise<unknown>>();
53
59
 
54
60
  function isManifestLike(value: unknown): value is Record<string, unknown> {
55
61
  if (!value || typeof value !== "object") return false;
@@ -72,8 +78,10 @@ export function hasAllRequiredPlatforms(value: unknown): boolean {
72
78
  return REQUIRED_PLATFORM_KEYS.every((k) => k in platforms);
73
79
  }
74
80
 
75
- async function fetchSignedManifest(): Promise<unknown> {
76
- const res = await fetch(GITHUB_MANIFEST_URL, {
81
+ async function fetchSignedManifest(
82
+ channel: ClipsUpdateChannel,
83
+ ): Promise<unknown> {
84
+ const res = await fetch(GITHUB_MANIFEST_URL[channel], {
77
85
  headers: {
78
86
  accept: "application/json",
79
87
  "user-agent": "clips-updater-manifest",
@@ -94,38 +102,49 @@ async function fetchSignedManifest(): Promise<unknown> {
94
102
  return json;
95
103
  }
96
104
 
97
- async function getManifest(): Promise<unknown> {
105
+ function normalizeChannel(value: unknown): ClipsUpdateChannel {
106
+ return value === "nightly" ? "nightly" : "production";
107
+ }
108
+
109
+ async function getManifest(
110
+ channel: ClipsUpdateChannel = "production",
111
+ ): Promise<unknown> {
98
112
  const now = Date.now();
99
- if (cache && now - cache.ts < CACHE_TTL_MS) return cache.data;
100
- if (inFlight) return inFlight;
101
- inFlight = (async () => {
113
+ const cached = cache.get(channel);
114
+ if (cached && now - cached.ts < CACHE_TTL_MS) return cached.data;
115
+ const pending = inFlight.get(channel);
116
+ if (pending) return pending;
117
+ const request = (async () => {
102
118
  try {
103
- const data = await fetchSignedManifest();
104
- cache = { data, ts: Date.now() };
119
+ const data = await fetchSignedManifest(channel);
120
+ cache.set(channel, { data, ts: Date.now() });
105
121
  return data;
106
122
  } catch {
107
123
  // A validation or network failure may serve the last good manifest (or
108
124
  // inert fallback), but it must not make that fallback fresh again.
109
125
  // Otherwise an incomplete release manifest can indefinitely extend the
110
126
  // stale cache window on every failed refresh.
111
- return cache?.data ?? INERT_MANIFEST;
127
+ return cache.get(channel)?.data ?? INERT_MANIFEST;
112
128
  } finally {
113
- inFlight = null;
129
+ inFlight.delete(channel);
114
130
  }
115
131
  })();
116
- return inFlight;
132
+ inFlight.set(channel, request);
133
+ return request;
117
134
  }
118
135
 
119
136
  export const __clipsUpdaterTest = {
120
137
  getManifest,
121
138
  reset() {
122
- cache = null;
123
- inFlight = null;
139
+ cache.clear();
140
+ inFlight.clear();
124
141
  },
125
142
  };
126
143
 
127
144
  export default defineEventHandler(async (event) => {
128
- const manifest = await getManifest();
145
+ const query = getQuery(event);
146
+ const channel = normalizeChannel(query.channel);
147
+ const manifest = await getManifest(channel);
129
148
  setResponseHeaders(event, {
130
149
  "content-type": "application/json; charset=utf-8",
131
150
  "cache-control": "public, max-age=60",
@@ -63,7 +63,7 @@ decisions, feedback, agent runs, and provider audit records.
63
63
  | `evaluate-triage-item` | Append a decision. |
64
64
  | `record-triage-feedback` | Capture human correction for learning. |
65
65
  | `approve-factory-item` | Explicitly authorize one bounded run. |
66
- | `start-builder-for-item` | Govern clear-bug dispatch through Slack or Builder API. |
66
+ | `start-builder-for-item` | Govern clear-bug dispatch through Slack or Builder API, or record a skip reason. |
67
67
  | `govern-agent-native-pull-request` | Apply PR evidence and ownership gates. |
68
68
  | `list-factory-automations` / `save-factory-automation` / `run-factory-automation` | Inspect or edit org-owned automations. |
69
69
  | `list-factory-audit` | Inspect automation runs, evidence, decisions, and provider actions. |
@@ -41,6 +41,10 @@ export default defineAction({
41
41
  timezone: meta.timezone ?? null,
42
42
  condition: meta.condition ?? null,
43
43
  createdBy: meta.createdBy ?? null,
44
+ updatedAt:
45
+ Number.isFinite(resource.updatedAt) && resource.updatedAt > 0
46
+ ? new Date(resource.updatedAt).toISOString()
47
+ : null,
44
48
  canUpdate,
45
49
  runs: await listAutomationRuns({
46
50
  owners: [resource.owner],
@@ -153,7 +153,7 @@ async function writeMetadata(
153
153
  .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
154
154
  }
155
155
 
156
- async function recordDecision(input: {
156
+ export async function recordAutomaticBuilderDecision(input: {
157
157
  itemId: string;
158
158
  userEmail: string;
159
159
  orgId: string;
@@ -167,6 +167,7 @@ async function recordDecision(input: {
167
167
  input.itemId,
168
168
  "automatic-builder",
169
169
  );
170
+ const now = new Date().toISOString();
170
171
  await getDb()
171
172
  .insert(triageDecisions)
172
173
  .values({
@@ -179,17 +180,26 @@ async function recordDecision(input: {
179
180
  guardResultsJson: JSON.stringify(input.guardResults),
180
181
  model: "factory-automation",
181
182
  promptVersion: 1,
182
- createdAt: new Date().toISOString(),
183
+ createdAt: now,
183
184
  ownerEmail: input.userEmail,
184
185
  orgId: input.orgId,
185
186
  })
186
- .onConflictDoNothing();
187
+ .onConflictDoUpdate({
188
+ target: triageDecisions.id,
189
+ set: {
190
+ outcome: input.outcome,
191
+ reason: input.reason,
192
+ guardResultsJson: JSON.stringify(input.guardResults),
193
+ createdAt: now,
194
+ ownerEmail: input.userEmail,
195
+ },
196
+ });
187
197
  return id;
188
198
  }
189
199
 
190
200
  export default defineAction({
191
201
  description:
192
- "Start the governed clear-bug Builder flow for a Factory item. Slack items are kept in-thread by adding 👀 and tagging @builder.io; grouped Slack repeats share one Builder thread. GitHub issues and Sentry errors use the Builder agent run API. Owner-managed Clips, Design, and Content items are always left for their owner.",
202
+ "Start the governed clear-bug Builder flow for a Factory item, or record a skip with a reason when clearBug is false. Slack items are kept in-thread by adding 👀 and tagging @builder.io; grouped Slack repeats share one Builder thread. GitHub issues and Sentry errors use the Builder agent run API. Owner-managed Clips, Design, and Content items are always left for their owner.",
193
203
  schema: z.object({
194
204
  itemId: z.string().min(1),
195
205
  clearBug: z.boolean(),
@@ -340,17 +350,12 @@ export default defineAction({
340
350
  : []),
341
351
  ];
342
352
  const blocked = guardResults.some((guard) => !guard.passed);
343
- const decisionId = await recordDecision({
353
+ const decisionId = await recordAutomaticBuilderDecision({
344
354
  itemId,
345
355
  userEmail,
346
356
  orgId,
347
357
  outcome: blocked ? "needs_manual" : "propose_fix",
348
- reason: blocked
349
- ? guardResults
350
- .filter((guard) => !guard.passed)
351
- .map((guard) => guard.reason)
352
- .join(" ")
353
- : reason,
358
+ reason,
354
359
  guardResults,
355
360
  });
356
361
  await recordFactoryAudit(
@@ -363,7 +368,7 @@ export default defineAction({
363
368
  source: item.source,
364
369
  sourceUrl: item.sourceUrl,
365
370
  status: blocked ? "skipped" : "success",
366
- summary: blocked ? "Factory kept this item for manual review." : reason,
371
+ summary: reason,
367
372
  details: {
368
373
  decisionId,
369
374
  clearBug,
@@ -383,10 +388,7 @@ export default defineAction({
383
388
  started: false,
384
389
  needsManual: true,
385
390
  decisionId,
386
- reason: guardResults
387
- .filter((guard) => !guard.passed)
388
- .map((guard) => guard.reason)
389
- .join(" "),
391
+ reason,
390
392
  };
391
393
  }
392
394
 
@@ -550,7 +552,7 @@ export default defineAction({
550
552
  .where(
551
553
  and(eq(triageItems.id, related.id), eq(triageItems.orgId, orgId)),
552
554
  );
553
- await recordDecision({
555
+ await recordAutomaticBuilderDecision({
554
556
  itemId: related.id,
555
557
  userEmail,
556
558
  orgId,
@@ -1,3 +1,4 @@
1
+ import { requestAgentChatThreadOpen } from "@agent-native/core/client/agent-chat";
1
2
  import { useActionQuery } from "@agent-native/core/client/hooks";
2
3
  import { useT } from "@agent-native/core/client/i18n";
3
4
  import {
@@ -8,7 +9,7 @@ import {
8
9
  IconExternalLink,
9
10
  IconSearch,
10
11
  } from "@tabler/icons-react";
11
- import { useEffect, useMemo } from "react";
12
+ import { useEffect, useMemo, useRef } from "react";
12
13
  import { useSearchParams } from "react-router";
13
14
 
14
15
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -70,6 +71,7 @@ export function FactoryAuditView({
70
71
  }) {
71
72
  const t = useT();
72
73
  const [searchParams, setSearchParams] = useSearchParams();
74
+ const viewRef = useRef<HTMLDivElement>(null);
73
75
  const selectedRunId = searchParams.get("auditRunId");
74
76
  const auditQuery = useActionQuery<FactoryAuditResponse>(
75
77
  "list-factory-audit",
@@ -109,8 +111,13 @@ export function FactoryAuditView({
109
111
  );
110
112
  }
111
113
 
114
+ useEffect(() => {
115
+ if (!selectedRunId) return;
116
+ viewRef.current?.scrollIntoView({ block: "start" });
117
+ }, [selectedRunId]);
118
+
112
119
  return (
113
- <div className="p-4 lg:p-6">
120
+ <div ref={viewRef} className="p-4 lg:p-6">
114
121
  {auditQuery.isError ? (
115
122
  <Card>
116
123
  <CardContent className="flex items-start gap-2 p-4 text-sm text-destructive">
@@ -144,30 +151,38 @@ export function FactoryAuditView({
144
151
  </CardHeader>
145
152
  <CardContent className="p-0">
146
153
  <div className="grid gap-1.5 p-2">
147
- {runs.map((run) => (
148
- <button
149
- key={run.id}
150
- type="button"
151
- className={`w-full cursor-pointer rounded-lg bg-muted/20 p-3 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${selectedRun?.id === run.id ? "bg-muted/60" : ""}`}
152
- onClick={() => selectRun(run.id)}
153
- >
154
- <div className="flex items-center justify-between gap-3">
155
- <span className="min-w-0 truncate text-sm font-medium">
156
- {formatAutomationName(run.automation)}
157
- </span>
158
- <AuditStatus status={run.status} />
159
- </div>
160
- <div className="mt-1 flex items-center justify-between gap-2 text-xs text-muted-foreground">
161
- <span className="shrink-0">
162
- {formatAuditAge(run.startedAt)}
163
- </span>
164
- <span className="shrink-0">
165
- {run.events.length}{" "}
166
- {formatAuditCountLabel(run.events.length)}
167
- </span>
168
- </div>
169
- </button>
170
- ))}
154
+ {runs.map((run) => {
155
+ const selected = selectedRun?.id === run.id;
156
+ return (
157
+ <button
158
+ key={run.id}
159
+ type="button"
160
+ aria-current={selected ? "true" : undefined}
161
+ className={`w-full cursor-pointer rounded-lg p-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${
162
+ selected
163
+ ? "bg-primary/10 ring-1 ring-inset ring-primary/40"
164
+ : "bg-muted/20 hover:bg-muted/50"
165
+ }`}
166
+ onClick={() => selectRun(run.id)}
167
+ >
168
+ <div className="flex items-center justify-between gap-3">
169
+ <span className="min-w-0 truncate text-sm font-medium">
170
+ {formatAutomationName(run.automation)}
171
+ </span>
172
+ <AuditStatus status={run.status} />
173
+ </div>
174
+ <div className="mt-1 flex items-center justify-between gap-2 text-xs text-muted-foreground">
175
+ <span className="shrink-0">
176
+ {formatAuditAge(run.startedAt)}
177
+ </span>
178
+ <span className="shrink-0">
179
+ {run.events.length}{" "}
180
+ {formatAuditCountLabel(run.events.length)}
181
+ </span>
182
+ </div>
183
+ </button>
184
+ );
185
+ })}
171
186
  </div>
172
187
  </CardContent>
173
188
  </Card>
@@ -190,7 +205,9 @@ export function FactoryAuditView({
190
205
  </div>
191
206
  </CardHeader>
192
207
  <CardContent className="pt-4">
193
- {selectedRun && <AuditRunDetail run={selectedRun} />}
208
+ {selectedRun && (
209
+ <AuditRunDetail run={selectedRun} factoryId={factoryId} />
210
+ )}
194
211
  </CardContent>
195
212
  </Card>
196
213
  </div>
@@ -199,7 +216,13 @@ export function FactoryAuditView({
199
216
  );
200
217
  }
201
218
 
202
- function AuditRunDetail({ run }: { run: FactoryAuditRun }) {
219
+ function AuditRunDetail({
220
+ run,
221
+ factoryId,
222
+ }: {
223
+ run: FactoryAuditRun;
224
+ factoryId: string;
225
+ }) {
203
226
  const t = useT();
204
227
  const groups = useMemo(() => groupAuditEvents(run.events), [run.events]);
205
228
  const itemCount = new Set(
@@ -229,10 +252,22 @@ function AuditRunDetail({ run }: { run: FactoryAuditRun }) {
229
252
  {run.threadId && (
230
253
  <a
231
254
  href={`/chat/${encodeURIComponent(run.threadId)}`}
232
- className="ml-auto inline-flex items-center gap-1 text-primary hover:underline"
255
+ className="ml-auto inline-flex items-center text-primary hover:underline"
256
+ onClick={(event) => {
257
+ if (
258
+ event.metaKey ||
259
+ event.ctrlKey ||
260
+ event.shiftKey ||
261
+ event.altKey ||
262
+ event.button !== 0
263
+ ) {
264
+ return;
265
+ }
266
+ event.preventDefault();
267
+ requestAgentChatThreadOpen({ threadId: run.threadId! });
268
+ }}
233
269
  >
234
270
  {t("factoryRoute.auditOpenThread")}
235
- <IconExternalLink className="size-3" />
236
271
  </a>
237
272
  )}
238
273
  </div>
@@ -260,7 +295,11 @@ function AuditRunDetail({ run }: { run: FactoryAuditRun }) {
260
295
  ) : (
261
296
  <div className="grid gap-1.5">
262
297
  {groups.map((group) => (
263
- <AuditItemRow key={group.key} group={group} />
298
+ <AuditItemRow
299
+ key={group.key}
300
+ group={group}
301
+ factoryId={factoryId}
302
+ />
264
303
  ))}
265
304
  </div>
266
305
  )}
@@ -269,15 +308,20 @@ function AuditRunDetail({ run }: { run: FactoryAuditRun }) {
269
308
  );
270
309
  }
271
310
 
272
- function AuditItemRow({ group }: { group: AuditItemGroup }) {
311
+ function AuditItemRow({
312
+ group,
313
+ factoryId,
314
+ }: {
315
+ group: AuditItemGroup;
316
+ factoryId: string;
317
+ }) {
273
318
  const t = useT();
274
319
  const decision = group.decision;
275
320
  const rationale = decision?.summary ?? group.externalAction?.summary ?? null;
276
- const sourceLink =
277
- group.sourceUrl ?? group.events.find((event) => event.sourceUrl)?.sourceUrl;
321
+ const sourceLink = resolveAuditSourceLink(group);
278
322
 
279
323
  return (
280
- <details className="group rounded-lg bg-muted/20">
324
+ <details className="group overflow-hidden rounded-lg border border-border bg-muted/20">
281
325
  <summary className="flex cursor-pointer list-none items-center gap-3 rounded-lg px-3 py-3 hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring [&::-webkit-details-marker]:hidden">
282
326
  <AuditSourceIcon source={group.source} />
283
327
  <div className="min-w-0 flex-1">
@@ -336,7 +380,7 @@ function AuditItemRow({ group }: { group: AuditItemGroup }) {
336
380
  <div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-2 bg-background/40 px-3 py-3 text-xs">
337
381
  {group.itemId && (
338
382
  <a
339
- href={`/factory?tab=inbox&itemId=${encodeURIComponent(group.itemId)}`}
383
+ href={`/factory?factoryId=${encodeURIComponent(factoryId)}&tab=inbox&itemId=${encodeURIComponent(group.itemId)}`}
340
384
  className="text-primary hover:underline"
341
385
  >
342
386
  {t("factoryRoute.auditOpenItem")}
@@ -579,6 +623,26 @@ function formatAuditSource(source: string | null): string {
579
623
  return source ? formatAuditLabel(source) : "Factory";
580
624
  }
581
625
 
626
+ function resolveAuditSourceLink(group: AuditItemGroup): string | null {
627
+ const storedUrl =
628
+ group.sourceUrl ??
629
+ group.events.find((event) => event.sourceUrl)?.sourceUrl ??
630
+ null;
631
+ if (storedUrl) return storedUrl;
632
+
633
+ for (const event of group.events) {
634
+ const channelId = readStringDetail(event.details, "channelId");
635
+ const threadTs = readStringDetail(event.details, "threadTs");
636
+ if (channelId && threadTs) return slackThreadUrl(channelId, threadTs);
637
+ }
638
+ return null;
639
+ }
640
+
641
+ function slackThreadUrl(channelId: string, threadTs: string): string {
642
+ const compactTs = threadTs.replace(".", "");
643
+ return `https://slack.com/archives/${encodeURIComponent(channelId)}/p${compactTs}?thread_ts=${encodeURIComponent(threadTs)}`;
644
+ }
645
+
582
646
  function formatAutomationName(value: string): string {
583
647
  const words = value
584
648
  .replace(/^factory-/, "")
@@ -69,6 +69,7 @@ const messages = {
69
69
  status: "Status",
70
70
  coverage: "Coverage",
71
71
  reason: "Reason",
72
+ updatedAt: "Updated",
72
73
  detailTitle: "Decision detail",
73
74
  selectItem: "Select an item to inspect its evidence and decisions.",
74
75
  detailError: "Could not load this observation.",
@@ -221,11 +222,14 @@ const messages = {
221
222
  automationSchedulePlaceholder: "For example, every weekday at 9:00 AM",
222
223
  automationEnabledLabel: "Automation is enabled",
223
224
  automationPrompt: "Prompt",
225
+ automationLastUpdated: "last updated",
224
226
  automationPromptPlaceholder: "What should Factory do when this runs?",
225
227
  promptEditorHint: "Edit here, then save the automation.",
226
228
  pastRuns: "Past runs",
227
229
  pastRunsEmpty: "No runs recorded yet.",
228
230
  saveAutomation: "Save",
231
+ automationSaved: "Saved",
232
+ automationSaveFailed: "Could not save the automation.",
229
233
  runNow: "Run now",
230
234
  automationHealthTitle: "Scheduler health",
231
235
  automationHealthDescription: