@agent-native/core 0.122.0 → 0.122.1

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 (143) hide show
  1. package/README.md +4 -12
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +9 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/a2a/client.ts +57 -4
  6. package/corpus/core/src/client/settings/SettingsPanel.tsx +9 -9
  7. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +12 -2
  8. package/corpus/core/src/extensions/url-safety.ts +67 -4
  9. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  10. package/corpus/core/src/templates/default/app/root.tsx +1 -1
  11. package/corpus/core/src/templates/default/public/manifest.json +2 -2
  12. package/corpus/core/src/vite/client.ts +64 -3
  13. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -23
  14. package/corpus/templates/analytics/app/routes/_index.tsx +1 -1
  15. package/corpus/templates/analytics/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  17. package/corpus/templates/analytics/package.json +1 -1
  18. package/corpus/templates/analytics/public/manifest.json +1 -1
  19. package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
  20. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +0 -2
  21. package/corpus/templates/assets/app/routes/_index.tsx +1 -1
  22. package/corpus/templates/assets/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  23. package/corpus/templates/assets/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  24. package/corpus/templates/assets/server/plugins/auth.ts +1 -1
  25. package/corpus/templates/brain/app/lib/brain.ts +0 -7
  26. package/corpus/templates/brain/app/routes/_index.tsx +1 -2
  27. package/corpus/templates/brain/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  28. package/corpus/templates/brain/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  29. package/corpus/templates/brain/package.json +1 -1
  30. package/corpus/templates/brain/public/manifest.json +1 -1
  31. package/corpus/templates/brain/server/plugins/auth.ts +1 -1
  32. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -6
  33. package/corpus/templates/calendar/app/routes/_app._index.tsx +1 -1
  34. package/corpus/templates/calendar/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  35. package/corpus/templates/calendar/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  36. package/corpus/templates/calendar/package.json +1 -1
  37. package/corpus/templates/calendar/public/manifest.json +1 -1
  38. package/corpus/templates/calendar/server/plugins/auth.ts +1 -1
  39. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  40. package/corpus/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  41. package/corpus/templates/clips/actions/import-loom-recording.ts +53 -98
  42. package/corpus/templates/clips/actions/lib/loom-import-job.ts +260 -0
  43. package/corpus/templates/clips/app/components/library/library-layout.tsx +0 -7
  44. package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -1
  45. package/corpus/templates/clips/app/routes/_index.tsx +1 -1
  46. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +6 -0
  47. package/corpus/templates/clips/app/routes/record.tsx +5 -0
  48. package/corpus/templates/clips/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  50. package/corpus/templates/clips/server/db/schema.ts +2 -0
  51. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +2 -1
  52. package/corpus/templates/clips/server/plugins/auth.ts +1 -1
  53. package/corpus/templates/clips/server/plugins/db.ts +8 -0
  54. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +56 -3
  55. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -39
  56. package/corpus/templates/content/app/routes/_app._index.tsx +1 -2
  57. package/corpus/templates/content/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  58. package/corpus/templates/content/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  59. package/corpus/templates/content/package.json +1 -1
  60. package/corpus/templates/content/public/manifest.json +1 -1
  61. package/corpus/templates/content/server/plugins/auth.ts +1 -1
  62. package/corpus/templates/design/app/components/layout/Sidebar.tsx +0 -2
  63. package/corpus/templates/design/app/routes/_index.tsx +1 -1
  64. package/corpus/templates/design/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  65. package/corpus/templates/design/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  66. package/corpus/templates/design/server/plugins/auth.ts +1 -1
  67. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -10
  68. package/corpus/templates/dispatch/app/routes/overview.tsx +1 -1
  69. package/corpus/templates/dispatch/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  70. package/corpus/templates/dispatch/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  71. package/corpus/templates/dispatch/package.json +1 -1
  72. package/corpus/templates/dispatch/public/manifest.json +1 -1
  73. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +0 -35
  74. package/corpus/templates/forms/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  75. package/corpus/templates/forms/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  76. package/corpus/templates/forms/package.json +1 -1
  77. package/corpus/templates/forms/public/manifest.json +1 -1
  78. package/corpus/templates/forms/server/plugins/auth.ts +1 -1
  79. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +0 -2
  80. package/corpus/templates/macros/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  81. package/corpus/templates/macros/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  82. package/corpus/templates/macros/package.json +1 -1
  83. package/corpus/templates/macros/server/plugins/auth.ts +2 -2
  84. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -39
  85. package/corpus/templates/mail/app/routes/$view.$threadId.tsx +1 -1
  86. package/corpus/templates/mail/app/routes/$view.tsx +1 -1
  87. package/corpus/templates/mail/app/routes/_index.tsx +1 -1
  88. package/corpus/templates/mail/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  89. package/corpus/templates/mail/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  90. package/corpus/templates/mail/package.json +1 -1
  91. package/corpus/templates/mail/public/manifest.json +1 -1
  92. package/corpus/templates/mail/server/plugins/auth.ts +1 -1
  93. package/corpus/templates/plan/agent-native.app-skill.json +1 -1
  94. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +4 -9
  95. package/corpus/templates/plan/app/lib/app-config.ts +1 -1
  96. package/corpus/templates/plan/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  97. package/corpus/templates/plan/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  98. package/corpus/templates/plan/package.json +1 -1
  99. package/corpus/templates/plan/public/manifest.json +1 -1
  100. package/corpus/templates/plan/server/plugins/auth.ts +1 -1
  101. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
  102. package/corpus/templates/slides/app/routes/_index.tsx +1 -1
  103. package/corpus/templates/slides/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  104. package/corpus/templates/slides/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  105. package/corpus/templates/slides/package.json +1 -1
  106. package/corpus/templates/slides/public/manifest.json +1 -1
  107. package/corpus/templates/slides/server/plugins/auth.ts +1 -1
  108. package/dist/a2a/client.d.ts.map +1 -1
  109. package/dist/a2a/client.js +58 -4
  110. package/dist/a2a/client.js.map +1 -1
  111. package/dist/client/settings/SettingsPanel.js +9 -9
  112. package/dist/client/settings/SettingsPanel.js.map +1 -1
  113. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  114. package/dist/client/settings/SettingsTabsPage.js +7 -4
  115. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  116. package/dist/collab/struct-routes.d.ts +1 -1
  117. package/dist/extensions/url-safety.d.ts +9 -16
  118. package/dist/extensions/url-safety.d.ts.map +1 -1
  119. package/dist/extensions/url-safety.js +48 -4
  120. package/dist/extensions/url-safety.js.map +1 -1
  121. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  122. package/dist/notifications/routes.d.ts +2 -2
  123. package/dist/observability/routes.d.ts +3 -3
  124. package/dist/resources/handlers.d.ts +1 -1
  125. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  126. package/dist/server/transcribe-voice.d.ts +1 -1
  127. package/dist/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  128. package/dist/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  129. package/dist/templates/default/app/root.tsx +1 -1
  130. package/dist/templates/default/public/manifest.json +2 -2
  131. package/dist/vite/client.d.ts.map +1 -1
  132. package/dist/vite/client.js +64 -1
  133. package/dist/vite/client.js.map +1 -1
  134. package/package.json +1 -1
  135. package/src/a2a/client.ts +57 -4
  136. package/src/client/settings/SettingsPanel.tsx +9 -9
  137. package/src/client/settings/SettingsTabsPage.tsx +12 -2
  138. package/src/extensions/url-safety.ts +67 -4
  139. package/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  140. package/src/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  141. package/src/templates/default/app/root.tsx +1 -1
  142. package/src/templates/default/public/manifest.json +2 -2
  143. package/src/vite/client.ts +64 -3
@@ -9,8 +9,7 @@ import { useEffect } from "react";
9
9
  import { shouldEnableBrainProviderStatusChecks } from "@/lib/brain-chat-readiness";
10
10
  import { TAB_ID } from "@/lib/tab-id";
11
11
 
12
- const SEO_TITLE =
13
- "Agent-Native Brain - Open Source company knowledge base for AI agents";
12
+ const SEO_TITLE = "Brain - Open Source company knowledge base for AI agents";
14
13
  const SEO_DESCRIPTION =
15
14
  "Open Source company knowledge base that turns Slack, meetings, transcripts, docs, and decisions into cited answers for AI agents.";
16
15
 
@@ -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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brain",
3
- "displayName": "Agent-Native Brain",
3
+ "displayName": "Brain",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "Agent-Native Brain",
2
+ "name": "Brain",
3
3
  "short_name": "Brain",
4
4
  "start_url": "/",
5
5
  "display": "standalone",
@@ -2,7 +2,7 @@ import { createAuthPlugin } from "@agent-native/core/server";
2
2
 
3
3
  export default createAuthPlugin({
4
4
  marketing: {
5
- appName: "Agent-Native Brain",
5
+ appName: "Brain",
6
6
  tagline:
7
7
  "A company knowledge layer where raw conversations become reviewed, searchable institutional knowledge.",
8
8
  features: [
@@ -6,7 +6,6 @@ import { OrgSwitcher } from "@agent-native/core/client/org";
6
6
  import { FeedbackButton } from "@agent-native/core/client/ui";
7
7
  import { SidebarFooterActions } from "@agent-native/toolkit/app-shell";
8
8
  import {
9
- IconHierarchy2,
10
9
  IconCalendar,
11
10
  IconSettings,
12
11
  IconLink,
@@ -101,11 +100,6 @@ const navItems = [
101
100
  ];
102
101
 
103
102
  const bottomNavItems = [
104
- {
105
- path: "/agent",
106
- labelKey: "settings.agentTitle",
107
- icon: IconHierarchy2,
108
- },
109
103
  { path: "/settings", labelKey: "navigation.settings", icon: IconSettings },
110
104
  ];
111
105
 
@@ -1,7 +1,7 @@
1
1
  import CalendarView from "@/pages/CalendarView";
2
2
 
3
3
  const SEO_TITLE =
4
- "Agent-Native Calendar - Open Source AI scheduling and Google Calendar automation";
4
+ "Calendar - Open Source AI scheduling and Google Calendar automation";
5
5
  const SEO_DESCRIPTION =
6
6
  "Open Source AI calendar app for Google Calendar scheduling, booking links, meeting coordination, and agent-managed event updates.";
7
7
 
@@ -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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calendar",
3
- "displayName": "Agent-Native Calendar",
3
+ "displayName": "Calendar",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "Agent Native Calendar",
2
+ "name": "Calendar",
3
3
  "short_name": "Calendar",
4
4
  "description": "Google Calendar integration",
5
5
  "start_url": ".",
@@ -8,7 +8,7 @@ export default createAuthPlugin({
8
8
  googleOnly: true,
9
9
  mountGoogleOAuthRoutes: false,
10
10
  marketing: {
11
- appName: "Agent-Native Calendar",
11
+ appName: "Calendar",
12
12
  tagline:
13
13
  "Your AI agent schedules, reschedules, and manages your calendar so you never have to.",
14
14
  features: [
@@ -14,7 +14,6 @@ import {
14
14
  type ChatHistoryItem,
15
15
  } from "@agent-native/toolkit/chat-history";
16
16
  import {
17
- IconHierarchy2,
18
17
  IconLayoutSidebarLeftCollapse,
19
18
  IconLayoutSidebarLeftExpand,
20
19
  IconMessageCircle,
@@ -43,12 +42,6 @@ const navItems = [
43
42
  ];
44
43
 
45
44
  const bottomNavItems = [
46
- {
47
- icon: IconHierarchy2,
48
- labelKey: "settings.agentTitle",
49
- href: "/agent",
50
- view: "agent",
51
- },
52
45
  {
53
46
  icon: IconSettings,
54
47
  labelKey: "navigation.settings",
@@ -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.
@@ -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"),