@agent-native/core 0.122.0 → 0.122.2

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 (187) hide show
  1. package/README.md +5 -25
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +15 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/a2a/client.ts +57 -4
  6. package/corpus/core/src/agent/production-agent.ts +65 -1
  7. package/corpus/core/src/agent/run-store.ts +61 -1
  8. package/corpus/core/src/client/settings/SettingsPanel.tsx +9 -9
  9. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +12 -2
  10. package/corpus/core/src/extensions/url-safety.ts +67 -4
  11. package/corpus/core/src/server/builder-design-systems.ts +1 -1
  12. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  13. package/corpus/core/src/templates/default/app/root.tsx +1 -1
  14. package/corpus/core/src/templates/default/public/manifest.json +2 -2
  15. package/corpus/core/src/vite/client.ts +64 -3
  16. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +15 -0
  17. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -23
  18. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +3 -3
  19. package/corpus/templates/analytics/app/routes/_index.tsx +1 -1
  20. package/corpus/templates/analytics/changelog/2026-07-25-analytics-dashboards-and-scheduled-reports-are-faster-and-mo.md +6 -0
  21. package/corpus/templates/analytics/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  22. package/corpus/templates/analytics/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  23. package/corpus/templates/analytics/package.json +1 -1
  24. package/corpus/templates/analytics/public/manifest.json +1 -1
  25. package/corpus/templates/analytics/server/db/schema.ts +12 -0
  26. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +8 -4
  27. package/corpus/templates/analytics/server/lib/dashboard-report.ts +136 -5
  28. package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +250 -1
  29. package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +167 -0
  30. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
  31. package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +105 -23
  32. package/corpus/templates/analytics/server/lib/first-party-unbounded-panel-repair.ts +169 -0
  33. package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
  34. package/corpus/templates/analytics/server/plugins/db.ts +41 -1
  35. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +6 -3
  36. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +0 -2
  37. package/corpus/templates/assets/app/routes/_index.tsx +1 -1
  38. package/corpus/templates/assets/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  39. package/corpus/templates/assets/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  40. package/corpus/templates/assets/server/plugins/auth.ts +1 -1
  41. package/corpus/templates/brain/app/lib/brain.ts +0 -7
  42. package/corpus/templates/brain/app/routes/_index.tsx +1 -2
  43. package/corpus/templates/brain/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  44. package/corpus/templates/brain/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  45. package/corpus/templates/brain/package.json +1 -1
  46. package/corpus/templates/brain/public/manifest.json +1 -1
  47. package/corpus/templates/brain/server/plugins/auth.ts +1 -1
  48. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -6
  49. package/corpus/templates/calendar/app/routes/_app._index.tsx +1 -1
  50. package/corpus/templates/calendar/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  51. package/corpus/templates/calendar/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  52. package/corpus/templates/calendar/package.json +1 -1
  53. package/corpus/templates/calendar/public/manifest.json +1 -1
  54. package/corpus/templates/calendar/server/plugins/auth.ts +1 -1
  55. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  56. package/corpus/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  57. package/corpus/templates/clips/actions/import-loom-recording.ts +53 -98
  58. package/corpus/templates/clips/actions/lib/loom-import-job.ts +260 -0
  59. package/corpus/templates/clips/app/components/library/library-layout.tsx +0 -7
  60. package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -1
  61. package/corpus/templates/clips/app/routes/_index.tsx +1 -1
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +6 -0
  63. package/corpus/templates/clips/app/routes/record.tsx +5 -0
  64. package/corpus/templates/clips/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  65. package/corpus/templates/clips/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  66. package/corpus/templates/clips/server/db/schema.ts +2 -0
  67. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +2 -1
  68. package/corpus/templates/clips/server/plugins/auth.ts +1 -1
  69. package/corpus/templates/clips/server/plugins/db.ts +8 -0
  70. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +56 -3
  71. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -39
  72. package/corpus/templates/content/app/routes/_app._index.tsx +1 -2
  73. package/corpus/templates/content/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  74. package/corpus/templates/content/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  75. package/corpus/templates/content/package.json +1 -1
  76. package/corpus/templates/content/public/manifest.json +1 -1
  77. package/corpus/templates/content/server/plugins/auth.ts +1 -1
  78. package/corpus/templates/design/actions/edit-design.ts +66 -27
  79. package/corpus/templates/design/app/components/layout/Sidebar.tsx +0 -2
  80. package/corpus/templates/design/app/routes/_index.tsx +1 -1
  81. package/corpus/templates/design/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  82. package/corpus/templates/design/changelog/2026-07-25-design-edits-retry-concurrent-changes.md +6 -0
  83. package/corpus/templates/design/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  84. package/corpus/templates/design/server/plugins/auth.ts +1 -1
  85. package/corpus/templates/design/server/source-workspace.ts +5 -1
  86. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -10
  87. package/corpus/templates/dispatch/app/routes/overview.tsx +1 -1
  88. package/corpus/templates/dispatch/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  89. package/corpus/templates/dispatch/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  90. package/corpus/templates/dispatch/package.json +1 -1
  91. package/corpus/templates/dispatch/public/manifest.json +1 -1
  92. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +6 -4
  93. package/corpus/templates/forms/actions/create-form.ts +5 -1
  94. package/corpus/templates/forms/actions/export-responses.ts +22 -11
  95. package/corpus/templates/forms/actions/update-form.ts +5 -1
  96. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +0 -35
  97. package/corpus/templates/forms/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  98. package/corpus/templates/forms/changelog/2026-07-25-fields-can-be-created-without-ids.md +6 -0
  99. package/corpus/templates/forms/changelog/2026-07-25-response-exports-use-file-storage.md +6 -0
  100. package/corpus/templates/forms/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  101. package/corpus/templates/forms/package.json +1 -1
  102. package/corpus/templates/forms/public/manifest.json +1 -1
  103. package/corpus/templates/forms/server/lib/validate-fields.ts +43 -0
  104. package/corpus/templates/forms/server/plugins/auth.ts +1 -1
  105. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +0 -2
  106. package/corpus/templates/macros/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  107. package/corpus/templates/macros/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  108. package/corpus/templates/macros/package.json +1 -1
  109. package/corpus/templates/macros/server/plugins/auth.ts +2 -2
  110. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -39
  111. package/corpus/templates/mail/app/routes/$view.$threadId.tsx +1 -1
  112. package/corpus/templates/mail/app/routes/$view.tsx +1 -1
  113. package/corpus/templates/mail/app/routes/_index.tsx +1 -1
  114. package/corpus/templates/mail/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  115. package/corpus/templates/mail/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  116. package/corpus/templates/mail/package.json +1 -1
  117. package/corpus/templates/mail/public/manifest.json +1 -1
  118. package/corpus/templates/mail/server/plugins/auth.ts +1 -1
  119. package/corpus/templates/plan/agent-native.app-skill.json +1 -1
  120. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +4 -9
  121. package/corpus/templates/plan/app/lib/app-config.ts +1 -1
  122. package/corpus/templates/plan/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  123. package/corpus/templates/plan/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  124. package/corpus/templates/plan/package.json +1 -1
  125. package/corpus/templates/plan/public/manifest.json +1 -1
  126. package/corpus/templates/plan/server/plugins/auth.ts +1 -1
  127. package/corpus/templates/slides/actions/extract-pdf.ts +4 -4
  128. package/corpus/templates/slides/actions/import-file.ts +3 -4
  129. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
  130. package/corpus/templates/slides/app/routes/_index.tsx +1 -1
  131. package/corpus/templates/slides/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  132. package/corpus/templates/slides/changelog/2026-07-25-gemini-image-models-updated.md +6 -0
  133. package/corpus/templates/slides/changelog/2026-07-25-pdf-imports-tolerate-missing-canvas.md +6 -0
  134. package/corpus/templates/slides/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  135. package/corpus/templates/slides/package.json +1 -1
  136. package/corpus/templates/slides/public/manifest.json +1 -1
  137. package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +5 -2
  138. package/corpus/templates/slides/server/lib/pdf-parse-setup.ts +150 -0
  139. package/corpus/templates/slides/server/plugins/auth.ts +1 -1
  140. package/dist/a2a/client.d.ts.map +1 -1
  141. package/dist/a2a/client.js +58 -4
  142. package/dist/a2a/client.js.map +1 -1
  143. package/dist/agent/production-agent.d.ts.map +1 -1
  144. package/dist/agent/production-agent.js +63 -1
  145. package/dist/agent/production-agent.js.map +1 -1
  146. package/dist/agent/run-store.d.ts.map +1 -1
  147. package/dist/agent/run-store.js +49 -0
  148. package/dist/agent/run-store.js.map +1 -1
  149. package/dist/client/settings/SettingsPanel.js +9 -9
  150. package/dist/client/settings/SettingsPanel.js.map +1 -1
  151. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  152. package/dist/client/settings/SettingsTabsPage.js +7 -4
  153. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  154. package/dist/collab/awareness.d.ts +2 -2
  155. package/dist/collab/awareness.d.ts.map +1 -1
  156. package/dist/collab/routes.d.ts +1 -1
  157. package/dist/extensions/url-safety.d.ts +9 -16
  158. package/dist/extensions/url-safety.d.ts.map +1 -1
  159. package/dist/extensions/url-safety.js +48 -4
  160. package/dist/extensions/url-safety.js.map +1 -1
  161. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  162. package/dist/notifications/routes.d.ts +3 -3
  163. package/dist/observability/routes.d.ts +3 -3
  164. package/dist/secrets/routes.d.ts +9 -9
  165. package/dist/server/builder-design-systems.js +1 -1
  166. package/dist/server/builder-design-systems.js.map +1 -1
  167. package/dist/server/transcribe-voice.d.ts +1 -1
  168. package/dist/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  169. package/dist/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  170. package/dist/templates/default/app/root.tsx +1 -1
  171. package/dist/templates/default/public/manifest.json +2 -2
  172. package/dist/vite/client.d.ts.map +1 -1
  173. package/dist/vite/client.js +64 -1
  174. package/dist/vite/client.js.map +1 -1
  175. package/package.json +3 -3
  176. package/src/a2a/client.ts +57 -4
  177. package/src/agent/production-agent.ts +65 -1
  178. package/src/agent/run-store.ts +61 -1
  179. package/src/client/settings/SettingsPanel.tsx +9 -9
  180. package/src/client/settings/SettingsTabsPage.tsx +12 -2
  181. package/src/extensions/url-safety.ts +67 -4
  182. package/src/server/builder-design-systems.ts +1 -1
  183. package/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  184. package/src/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  185. package/src/templates/default/app/root.tsx +1 -1
  186. package/src/templates/default/public/manifest.json +2 -2
  187. package/src/vite/client.ts +64 -3
@@ -1,14 +1,13 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
3
  import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
4
- import { uploadFile } from "@agent-native/core/file-upload";
5
4
  import { buildDeepLink } from "@agent-native/core/server";
6
5
  import { extractLoomVideoId, normalizeLoomShareUrl } from "@shared/loom.js";
7
6
  import { and, eq } from "drizzle-orm";
8
7
  import { z } from "zod";
9
8
 
10
9
  import { getDb, schema } from "../server/db/index.js";
11
- import { queueBuilderMediaCompression } from "../server/lib/builder-media-compression.js";
10
+ import { dispatchPostFinalizeJob } from "../server/lib/post-finalize-dispatch.js";
12
11
  import {
13
12
  getCurrentOwnerEmail,
14
13
  getOrganizationDefaultVisibility,
@@ -19,11 +18,7 @@ import {
19
18
  stringifySpaceIds,
20
19
  } from "../server/lib/recordings.js";
21
20
  import { hasRequestVideoStorage } from "../server/lib/video-storage.js";
22
- import {
23
- fetchLoomTranscript,
24
- loomTranscriptUnavailableMessage,
25
- } from "./lib/loom-transcript.js";
26
- import { downloadLoomVideo } from "./lib/loom-video.js";
21
+ import { failLoomImport } from "./lib/loom-import-job.js";
27
22
 
28
23
  const LoomOembedSchema = z
29
24
  .object({
@@ -132,7 +127,7 @@ async function fetchLoomOembed(shareUrl: string) {
132
127
 
133
128
  export default defineAction({
134
129
  description:
135
- "Import a public Loom share URL into Clips as a playable recording. Downloads Loom's public MP4, reuploads it to the configured Clips storage provider, and imports Loom's public transcript when available. If storage is not connected, creates a waiting recording that can be retried after storage setup.",
130
+ "Import a public Loom share URL into Clips as a playable recording. Creates the recording immediately and downloads Loom's public MP4, reuploads it to the configured Clips storage provider, and imports Loom's public transcript in the background (Loom's CDN plus a reupload can take longer than a single request should block on). If storage is not connected, creates a waiting recording that can be retried after storage setup.",
136
131
  schema: ImportLoomRecordingSchema,
137
132
  run: async (args) => {
138
133
  const shareUrl = normalizeLoomShareUrl(args.url);
@@ -163,15 +158,17 @@ export default defineAction({
163
158
  const existingSourceUrl = normalizeLoomShareUrl(
164
159
  existingRecording.sourceWindowTitle ?? "",
165
160
  );
166
- const isWaitingStorageRetry =
167
- existingRecording.status === "uploading" &&
161
+ const isRetryableLoomImport =
168
162
  !existingRecording.videoUrl &&
169
- existingRecording.failureReason ===
170
- LOOM_STORAGE_SETUP_REQUIRED_REASON &&
171
- existingSourceUrl === shareUrl;
172
- if (!isWaitingStorageRetry) {
163
+ existingSourceUrl === shareUrl &&
164
+ (existingRecording.status === "processing" ||
165
+ existingRecording.status === "failed" ||
166
+ (existingRecording.status === "uploading" &&
167
+ existingRecording.failureReason ===
168
+ LOOM_STORAGE_SETUP_REQUIRED_REASON));
169
+ if (!isRetryableLoomImport) {
173
170
  throw new Error(
174
- "Only a waiting-storage Loom import can be retried in place.",
171
+ "Only an incomplete Loom import can be retried in place.",
175
172
  );
176
173
  }
177
174
  }
@@ -241,6 +238,8 @@ export default defineAction({
241
238
  status: "uploading",
242
239
  videoUrl: null,
243
240
  failureReason: LOOM_STORAGE_SETUP_REQUIRED_REASON,
241
+ loomImportClaimId: null,
242
+ loomImportClaimedAt: null,
244
243
  })
245
244
  .where(eq(schema.recordings.id, id));
246
245
  } else {
@@ -293,123 +292,79 @@ export default defineAction({
293
292
  );
294
293
  }
295
294
 
296
- const media = await downloadLoomVideo({ loomId, shareUrl });
297
- const upload = await uploadFile({
298
- data: media.bytes,
299
- filename: `${id}.mp4`,
300
- mimeType: media.mimeType,
301
- ownerEmail,
302
- stableUrl: true,
303
- recordAsset: false,
304
- });
305
-
306
- const recordingValues = buildRecordingValues(media.sizeBytes);
307
- if (upload === null) {
308
- return await saveWaitingForStorage(media.sizeBytes);
309
- }
310
-
311
- if (!upload?.url) {
312
- throw new Error(
313
- "File upload returned no URL. Check your storage provider configuration.",
314
- );
315
- }
316
-
317
- const videoUrl = upload.url;
295
+ // Storage is connected: create the row now and hand the slow Loom
296
+ // download + reupload + transcript off to a durable background job.
297
+ // Loom's CDN plus a reupload can outlast the platform's request timeout;
298
+ // doing it inline previously crashed the function mid-download instead of
299
+ // returning a clean error (see post-finalize-worker.post.ts's "loom-import"
300
+ // kind / runLoomImportJob).
301
+ const recordingValues = buildRecordingValues(
302
+ existingRecording?.videoSizeBytes ?? 0,
303
+ );
318
304
  if (existingRecording) {
319
305
  await db
320
306
  .update(schema.recordings)
321
307
  .set({
322
308
  ...recordingValues,
323
- videoUrl,
324
- status: "ready",
309
+ status: "processing",
310
+ videoUrl: null,
325
311
  failureReason: null,
312
+ loomImportClaimId: null,
313
+ loomImportClaimedAt: null,
326
314
  })
327
315
  .where(eq(schema.recordings.id, id));
328
316
  } else {
329
317
  await db.insert(schema.recordings).values({
330
318
  id,
331
319
  ...recordingValues,
332
- videoUrl,
333
- status: "ready",
320
+ videoUrl: null,
321
+ status: "processing",
334
322
  failureReason: null,
335
323
  ownerEmail,
336
324
  createdAt: now,
337
325
  });
338
326
  }
339
327
 
340
- void queueBuilderMediaCompression({
328
+ await writeAppState(`recording-upload-${id}`, {
341
329
  recordingId: id,
342
- ownerEmail,
343
- videoUrl,
344
- mimeType: media.mimeType,
345
- providerId: upload.provider,
346
- assetDbId: upload.id,
347
- sourceSizeBytes: media.sizeBytes,
348
- }).catch((err) => {
349
- console.warn("[clips] Loom media compression queue failed", {
350
- recordingId: id,
351
- error: err instanceof Error ? err.message : String(err),
352
- });
330
+ status: "processing",
331
+ progress: 100,
332
+ provider: "loom",
333
+ sourceUrl: shareUrl,
334
+ durationMs,
335
+ width,
336
+ height,
337
+ hasAudio: true,
338
+ hasCamera: false,
339
+ updatedAt: now,
353
340
  });
341
+ await writeAppState("refresh-signal", { ts: Date.now() });
342
+ await writeAppState("navigate", { view: "recording", recordingId: id });
354
343
 
355
- let transcript: Awaited<ReturnType<typeof fetchLoomTranscript>> = null;
356
344
  try {
357
- transcript = await fetchLoomTranscript({ shareUrl, durationMs });
358
- } catch (err) {
359
- console.warn(
360
- `[clips] Loom transcript import skipped for ${loomId}:`,
361
- (err as Error)?.message ?? String(err),
362
- );
363
- }
364
-
365
- const transcriptValues = {
366
- ownerEmail,
367
- language: transcript?.language ?? "en",
368
- segmentsJson: transcript ? JSON.stringify(transcript.segments) : "[]",
369
- fullText: transcript?.fullText ?? "",
370
- status: transcript ? ("ready" as const) : ("failed" as const),
371
- failureReason: transcript ? null : loomTranscriptUnavailableMessage(),
372
- updatedAt: now,
373
- };
374
- const [existingTranscript] = await db
375
- .select({ recordingId: schema.recordingTranscripts.recordingId })
376
- .from(schema.recordingTranscripts)
377
- .where(eq(schema.recordingTranscripts.recordingId, id));
378
- if (existingTranscript) {
379
- await db
380
- .update(schema.recordingTranscripts)
381
- .set(transcriptValues)
382
- .where(eq(schema.recordingTranscripts.recordingId, id));
383
- } else {
384
- await db.insert(schema.recordingTranscripts).values({
345
+ await dispatchPostFinalizeJob({
385
346
  recordingId: id,
386
- ...transcriptValues,
387
- createdAt: now,
347
+ kind: "loom-import",
348
+ requireAccepted: true,
388
349
  });
350
+ } catch (err) {
351
+ const failureReason = `Could not start the Loom import: ${
352
+ err instanceof Error ? err.message : String(err)
353
+ }`;
354
+ await failLoomImport(id, failureReason);
355
+ throw new Error(failureReason);
389
356
  }
390
357
 
391
- await writeAppState("refresh-signal", { ts: Date.now() });
392
- await writeAppState("navigate", { view: "recording", recordingId: id });
393
-
394
358
  return {
395
359
  recordingId: id,
396
360
  title,
397
- status: "ready" as const,
361
+ status: "processing" as const,
398
362
  provider: "loom" as const,
399
363
  sourceUrl: shareUrl,
400
- videoUrl,
401
- embedUrl: videoUrl,
402
364
  thumbnailUrl: oembed.thumbnail_url ?? null,
403
365
  durationMs,
404
- transcriptStatus: transcript
405
- ? ("ready" as const)
406
- : ("unavailable" as const),
407
366
  importMode: "reuploaded" as const,
408
- storageProvider: upload.provider,
409
- videoSizeBytes: media.sizeBytes,
410
- note: transcript
411
- ? "Imported as a Clips-hosted MP4 with Loom's public transcript."
412
- : "Imported as a Clips-hosted MP4. Loom did not expose an importable transcript; use request-transcript to transcribe the uploaded media.",
367
+ note: "Downloading and importing this Loom recording in the background.",
413
368
  };
414
369
  },
415
370
  link: ({ result }) => {
@@ -0,0 +1,260 @@
1
+ import { writeAppState } from "@agent-native/core/application-state";
2
+ import { uploadFile } from "@agent-native/core/file-upload";
3
+ import { and, eq } from "drizzle-orm";
4
+
5
+ import { getDb, schema } from "../../server/db/index.js";
6
+ import { queueBuilderMediaCompression } from "../../server/lib/builder-media-compression.js";
7
+ import {
8
+ extractLoomVideoId,
9
+ normalizeLoomShareUrl,
10
+ } from "../../shared/loom.js";
11
+ import {
12
+ fetchLoomTranscript,
13
+ loomTranscriptUnavailableMessage,
14
+ } from "./loom-transcript.js";
15
+ import { downloadLoomVideo } from "./loom-video.js";
16
+
17
+ export type LoomImportJobResult = {
18
+ status: "ready" | "failed";
19
+ failureReason?: string;
20
+ };
21
+
22
+ export async function failLoomImport(
23
+ recordingId: string,
24
+ failureReason: string,
25
+ claimId?: string,
26
+ ): Promise<LoomImportJobResult> {
27
+ const now = new Date().toISOString();
28
+ const [updated] = await getDb()
29
+ .update(schema.recordings)
30
+ .set({
31
+ status: "failed",
32
+ failureReason,
33
+ loomImportClaimId: null,
34
+ loomImportClaimedAt: null,
35
+ updatedAt: now,
36
+ })
37
+ .where(
38
+ claimId
39
+ ? and(
40
+ eq(schema.recordings.id, recordingId),
41
+ eq(schema.recordings.loomImportClaimId, claimId),
42
+ )
43
+ : eq(schema.recordings.id, recordingId),
44
+ )
45
+ .returning({ id: schema.recordings.id });
46
+ if (!updated) return { status: "failed", failureReason };
47
+
48
+ try {
49
+ await writeAppState(`recording-upload-${recordingId}`, {
50
+ recordingId,
51
+ status: "failed",
52
+ failureReason,
53
+ updatedAt: now,
54
+ });
55
+ await writeAppState("refresh-signal", { ts: Date.now() });
56
+ } catch (err) {
57
+ console.warn("[clips] Loom failure state update failed", {
58
+ recordingId,
59
+ error: err instanceof Error ? err.message : String(err),
60
+ });
61
+ }
62
+ return { status: "failed", failureReason };
63
+ }
64
+
65
+ /**
66
+ * Downloads a Loom video and re-uploads it to Clips storage, off the request
67
+ * that created the "processing" row. Loom's CDN plus a reupload can outlast a
68
+ * synchronous serverless function's execution ceiling; running it here keeps
69
+ * import-loom-recording's own request fast regardless of Loom video length.
70
+ */
71
+ export async function runLoomImportJob({
72
+ recordingId,
73
+ ownerEmail,
74
+ claimId,
75
+ }: {
76
+ recordingId: string;
77
+ ownerEmail: string;
78
+ claimId: string;
79
+ }): Promise<LoomImportJobResult> {
80
+ const db = getDb();
81
+ const [recording] = await db
82
+ .select({
83
+ id: schema.recordings.id,
84
+ durationMs: schema.recordings.durationMs,
85
+ sourceWindowTitle: schema.recordings.sourceWindowTitle,
86
+ loomImportClaimId: schema.recordings.loomImportClaimId,
87
+ })
88
+ .from(schema.recordings)
89
+ .where(eq(schema.recordings.id, recordingId));
90
+
91
+ const shareUrl = normalizeLoomShareUrl(recording?.sourceWindowTitle ?? "");
92
+ const loomId = shareUrl ? extractLoomVideoId(shareUrl) : null;
93
+ if (
94
+ !recording ||
95
+ recording.loomImportClaimId !== claimId ||
96
+ !shareUrl ||
97
+ !loomId
98
+ ) {
99
+ return failLoomImport(
100
+ recordingId,
101
+ "This Loom recording is missing its source URL.",
102
+ claimId,
103
+ );
104
+ }
105
+
106
+ let media: Awaited<ReturnType<typeof downloadLoomVideo>>;
107
+ try {
108
+ media = await downloadLoomVideo({ loomId, shareUrl });
109
+ } catch (err) {
110
+ return failLoomImport(
111
+ recordingId,
112
+ err instanceof Error ? err.message : String(err),
113
+ claimId,
114
+ );
115
+ }
116
+
117
+ try {
118
+ const upload = await uploadFile({
119
+ data: media.bytes,
120
+ filename: `${recordingId}.mp4`,
121
+ mimeType: media.mimeType,
122
+ ownerEmail,
123
+ stableUrl: true,
124
+ recordAsset: false,
125
+ });
126
+ if (!upload?.url) {
127
+ return failLoomImport(
128
+ recordingId,
129
+ "File upload returned no URL. Check your storage provider configuration.",
130
+ claimId,
131
+ );
132
+ }
133
+
134
+ const now = new Date().toISOString();
135
+ const [mediaReady] = await db
136
+ .update(schema.recordings)
137
+ .set({
138
+ videoUrl: upload.url,
139
+ videoSizeBytes: media.sizeBytes,
140
+ status: "ready",
141
+ failureReason: null,
142
+ updatedAt: now,
143
+ })
144
+ .where(
145
+ and(
146
+ eq(schema.recordings.id, recordingId),
147
+ eq(schema.recordings.loomImportClaimId, claimId),
148
+ ),
149
+ )
150
+ .returning({ id: schema.recordings.id });
151
+ if (!mediaReady) {
152
+ return {
153
+ status: "failed",
154
+ failureReason: "The Loom import lease was lost before media was saved.",
155
+ };
156
+ }
157
+
158
+ void queueBuilderMediaCompression({
159
+ recordingId,
160
+ ownerEmail,
161
+ videoUrl: upload.url,
162
+ mimeType: media.mimeType,
163
+ providerId: upload.provider,
164
+ assetDbId: upload.id,
165
+ sourceSizeBytes: media.sizeBytes,
166
+ }).catch((err) => {
167
+ console.warn("[clips] Loom media compression queue failed", {
168
+ recordingId,
169
+ error: err instanceof Error ? err.message : String(err),
170
+ });
171
+ });
172
+
173
+ try {
174
+ let transcript: Awaited<ReturnType<typeof fetchLoomTranscript>> = null;
175
+ try {
176
+ transcript = await fetchLoomTranscript({
177
+ shareUrl,
178
+ durationMs: recording.durationMs,
179
+ });
180
+ } catch (err) {
181
+ console.warn(
182
+ `[clips] Loom transcript import skipped for ${loomId}:`,
183
+ err instanceof Error ? err.message : String(err),
184
+ );
185
+ }
186
+
187
+ const transcriptValues = {
188
+ ownerEmail,
189
+ language: transcript?.language ?? "en",
190
+ segmentsJson: transcript ? JSON.stringify(transcript.segments) : "[]",
191
+ fullText: transcript?.fullText ?? "",
192
+ status: transcript ? ("ready" as const) : ("failed" as const),
193
+ failureReason: transcript ? null : loomTranscriptUnavailableMessage(),
194
+ updatedAt: now,
195
+ };
196
+ const [existingTranscript] = await db
197
+ .select({ recordingId: schema.recordingTranscripts.recordingId })
198
+ .from(schema.recordingTranscripts)
199
+ .where(eq(schema.recordingTranscripts.recordingId, recordingId));
200
+ if (existingTranscript) {
201
+ await db
202
+ .update(schema.recordingTranscripts)
203
+ .set(transcriptValues)
204
+ .where(eq(schema.recordingTranscripts.recordingId, recordingId));
205
+ } else {
206
+ await db.insert(schema.recordingTranscripts).values({
207
+ recordingId,
208
+ ...transcriptValues,
209
+ createdAt: now,
210
+ });
211
+ }
212
+ } catch (err) {
213
+ console.warn("[clips] Loom transcript persistence skipped", {
214
+ recordingId,
215
+ error: err instanceof Error ? err.message : String(err),
216
+ });
217
+ }
218
+
219
+ try {
220
+ await writeAppState(`recording-upload-${recordingId}`, {
221
+ recordingId,
222
+ status: "ready",
223
+ progress: 100,
224
+ videoUrl: upload.url,
225
+ updatedAt: now,
226
+ });
227
+ await writeAppState("refresh-signal", { ts: Date.now() });
228
+ } catch (err) {
229
+ console.warn("[clips] Loom ready state update skipped", {
230
+ recordingId,
231
+ error: err instanceof Error ? err.message : String(err),
232
+ });
233
+ }
234
+
235
+ try {
236
+ await db
237
+ .update(schema.recordings)
238
+ .set({ loomImportClaimId: null, loomImportClaimedAt: null })
239
+ .where(
240
+ and(
241
+ eq(schema.recordings.id, recordingId),
242
+ eq(schema.recordings.loomImportClaimId, claimId),
243
+ ),
244
+ );
245
+ } catch (err) {
246
+ console.warn("[clips] Loom import lease release failed", {
247
+ recordingId,
248
+ error: err instanceof Error ? err.message : String(err),
249
+ });
250
+ }
251
+
252
+ return { status: "ready" };
253
+ } catch (err) {
254
+ return failLoomImport(
255
+ recordingId,
256
+ err instanceof Error ? err.message : String(err),
257
+ claimId,
258
+ );
259
+ }
260
+ }
@@ -30,7 +30,6 @@ import {
30
30
  IconLayoutSidebarLeftExpand,
31
31
  IconPlus,
32
32
  IconShare,
33
- IconHierarchy2,
34
33
  IconSettings,
35
34
  IconSearch,
36
35
  } from "@tabler/icons-react";
@@ -304,12 +303,6 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
304
303
  icon: React.ComponentType<{ className?: string }>;
305
304
  match: (path: string) => boolean;
306
305
  }[] = [
307
- {
308
- to: "/agent",
309
- label: t("settings.agentTitle"),
310
- icon: IconHierarchy2,
311
- match: (p) => p.startsWith("/agent"),
312
- },
313
306
  {
314
307
  to: "/settings",
315
308
  label: t("navigation.settings"),
@@ -6,7 +6,7 @@ import { LibraryGrid } from "@/components/library/library-grid";
6
6
  import { usePageHeaderLayout } from "@/components/library/page-header";
7
7
  import { Button } from "@/components/ui/button";
8
8
 
9
- const SEO_TITLE = "Agent-Native Clips - Open Source screen recorder";
9
+ const SEO_TITLE = "Clips - Open Source screen recorder";
10
10
  const SEO_DESCRIPTION =
11
11
  "Open Source screen recorder and meeting-notes app with AI transcripts, summaries, search, dictation, and agent-readable share links.";
12
12
 
@@ -1,7 +1,7 @@
1
1
  import { DefaultSpinner } from "@agent-native/core/client/ui";
2
2
  import { redirect, type LoaderFunctionArgs } from "react-router";
3
3
 
4
- const SEO_TITLE = "Agent-Native Clips - Open Source screen recorder";
4
+ const SEO_TITLE = "Clips - Open Source screen recorder";
5
5
  const SEO_DESCRIPTION =
6
6
  "Open Source screen recorder and meeting-notes app with AI transcripts, summaries, search, dictation, and agent-readable share links.";
7
7
 
@@ -596,6 +596,12 @@ export default function RecordingPage() {
596
596
  });
597
597
  return;
598
598
  }
599
+ if (retryingLoomImport && result?.status === "processing") {
600
+ // Download + reupload now run as a background job; this request only
601
+ // confirms the retry was accepted, not that the clip is ready yet.
602
+ toast.info(t("recordingPage.importingLoom"));
603
+ return;
604
+ }
599
605
  toast.success(
600
606
  retryingLoomImport
601
607
  ? t("recordingPage.loomImportResumed")
@@ -1850,6 +1850,11 @@ export default function RecordRoute() {
1850
1850
  description: t("recordRoute.connectStorageToRetryLoom"),
1851
1851
  duration: 12_000,
1852
1852
  });
1853
+ } else if (result?.status === "processing") {
1854
+ // The video download + reupload run as a background job (see
1855
+ // import-loom-recording.ts) so this request stays fast regardless of
1856
+ // Loom video length; the recording page polls until it is ready.
1857
+ toast.info(t("recordingPage.importingLoom"));
1853
1858
  } else {
1854
1859
  showSavedToast(
1855
1860
  t("recordRoute.loomImported"),
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ App branding now uses the product name without the Agent-Native prefix.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ Settings navigation now keeps Manage agent as a dedicated linked destination at the bottom.
@@ -166,6 +166,8 @@ export const recordings = table("recordings", {
166
166
  .default("uploading"),
167
167
  uploadProgress: integer("upload_progress").notNull().default(0),
168
168
  failureReason: text("failure_reason"),
169
+ loomImportClaimId: text("loom_import_claim_id"),
170
+ loomImportClaimedAt: text("loom_import_claimed_at"),
169
171
 
170
172
  // Non-destructive edits: JSON `{ trims: [{startMs,endMs,excluded}], blurs: [...], speed: [...] }`
171
173
  editsJson: text("edits_json").notNull().default("{}"),
@@ -11,7 +11,8 @@ export type PostFinalizeJobKind =
11
11
  | "media-ready"
12
12
  | "seekable"
13
13
  | "transcript"
14
- | "brain-export";
14
+ | "brain-export"
15
+ | "loom-import";
15
16
 
16
17
  export const POST_FINALIZE_JOB_TOKEN_KIND = "clips-post-finalize-job";
17
18
 
@@ -7,7 +7,7 @@ export default createAuthPlugin({
7
7
  // callback can handle both normal sign-in and the Calendar connect flow.
8
8
  mountGoogleOAuthRoutes: false,
9
9
  marketing: {
10
- appName: "Agent-Native Clips",
10
+ appName: "Clips",
11
11
  tagline:
12
12
  "Your AI agent transcribes, summarizes, and searches everything you record alongside you.",
13
13
  features: [
@@ -873,6 +873,14 @@ const migrations = runMigrations(
873
873
  `CREATE INDEX IF NOT EXISTS recording_agent_views_recording_idx ON recording_agent_views (recording_id, last_seen_at)`,
874
874
  ].join("; "),
875
875
  },
876
+ {
877
+ version: 52,
878
+ name: "recording-loom-import-claim-lease",
879
+ sql: [
880
+ `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS loom_import_claim_id TEXT`,
881
+ `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS loom_import_claimed_at TEXT`,
882
+ ].join("; "),
883
+ },
876
884
  ],
877
885
  { table: "clips_migrations" },
878
886
  );