@agent-native/core 0.84.20 → 0.84.22

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 (193) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/caller-auth.ts +37 -8
  5. package/corpus/core/src/a2a/client.ts +178 -67
  6. package/corpus/core/src/agent/production-agent.ts +19 -3
  7. package/corpus/core/src/agent/types.ts +1 -1
  8. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  9. package/corpus/core/src/cli/recap.ts +43 -34
  10. package/corpus/core/src/client/AssistantChat.tsx +27 -4
  11. package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
  12. package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
  13. package/corpus/core/src/client/guided-questions.tsx +11 -6
  14. package/corpus/core/src/client/i18n.tsx +10 -5
  15. package/corpus/core/src/client/sse-event-processor.ts +147 -5
  16. package/corpus/core/src/client/tool-display.ts +8 -0
  17. package/corpus/core/src/file-upload/builder.ts +40 -14
  18. package/corpus/core/src/file-upload/types.ts +3 -0
  19. package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
  20. package/corpus/core/src/mcp/builtin-tools.ts +3 -0
  21. package/corpus/core/src/mcp/org-directory.ts +66 -20
  22. package/corpus/core/src/notifications/channels.ts +235 -38
  23. package/corpus/core/src/scripts/call-agent.ts +7 -2
  24. package/corpus/core/src/server/analytics.ts +32 -6
  25. package/corpus/core/src/server/ssr-handler.ts +31 -12
  26. package/corpus/templates/analytics/AGENTS.md +21 -0
  27. package/corpus/templates/analytics/README.md +35 -0
  28. package/corpus/templates/analytics/actions/delete-analytics-alert-rule.ts +23 -0
  29. package/corpus/templates/analytics/actions/list-analytics-alert-rules.ts +20 -0
  30. package/corpus/templates/analytics/actions/run-analytics-alerts.ts +23 -0
  31. package/corpus/templates/analytics/actions/save-analytics-alert-rule.ts +69 -0
  32. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
  33. package/corpus/templates/analytics/changelog/2026-07-01-analytics-can-alert-when-first-party-events-spike.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-play-back-in-production-instead-of-showing.md +6 -0
  36. package/corpus/templates/analytics/netlify.toml +3 -0
  37. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
  38. package/corpus/templates/analytics/server/db/schema.ts +55 -0
  39. package/corpus/templates/analytics/server/handlers/session-replay.ts +5 -2
  40. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +71 -0
  41. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +734 -0
  42. package/corpus/templates/analytics/server/lib/session-replay.ts +10 -3
  43. package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +54 -0
  44. package/corpus/templates/analytics/server/plugins/db.ts +100 -0
  45. package/corpus/templates/analytics/server/routes/api/analytics-alerts/run.post.ts +60 -0
  46. package/corpus/templates/assets/README.md +30 -8
  47. package/corpus/templates/brain/README.md +20 -552
  48. package/corpus/templates/calendar/README.md +22 -28
  49. package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
  50. package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
  51. package/corpus/templates/chat/README.md +32 -0
  52. package/corpus/templates/clips/README.md +37 -0
  53. package/corpus/templates/clips/actions/create-recording.ts +7 -2
  54. package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
  55. package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
  56. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
  57. package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
  58. package/corpus/templates/clips/app/components/library/library-grid.tsx +18 -7
  59. package/corpus/templates/clips/app/components/library/recording-card.tsx +42 -5
  60. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
  61. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
  62. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
  63. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +106 -2
  64. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
  65. package/corpus/templates/clips/app/routes/record.tsx +48 -8
  66. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
  67. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
  68. package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
  69. package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
  70. package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
  71. package/corpus/templates/clips/changelog/2026-07-01-move-to-folder-now-opens-folder-choices-directly-instead-of.md +6 -0
  72. package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
  73. package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
  74. package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
  75. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  76. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
  77. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +64 -6
  78. package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
  79. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
  80. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +51 -1
  81. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
  82. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
  83. package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
  84. package/corpus/templates/content/AGENTS.md +7 -3
  85. package/corpus/templates/content/README.md +22 -34
  86. package/corpus/templates/content/actions/list-trashed-content-databases.ts +4 -0
  87. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +58 -6
  88. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +257 -24
  89. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +41 -15
  90. package/corpus/templates/content/app/i18n/zh-TW.ts +25 -7
  91. package/corpus/templates/content/app/i18n-data.ts +278 -40
  92. package/corpus/templates/content/changelog/2026-06-30-database-once-again-creates-an-inline-database-in-the-curren.md +6 -0
  93. package/corpus/templates/content/shared/api.ts +1 -0
  94. package/corpus/templates/design/AGENTS.md +7 -6
  95. package/corpus/templates/design/README.md +33 -0
  96. package/corpus/templates/design/actions/delete-file.ts +86 -10
  97. package/corpus/templates/design/actions/get-design-snapshot.ts +13 -0
  98. package/corpus/templates/design/actions/present-design-variants.ts +11 -13
  99. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  100. package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
  101. package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
  102. package/corpus/templates/dispatch/README.md +34 -0
  103. package/corpus/templates/forms/README.md +34 -0
  104. package/corpus/templates/macros/README.md +28 -0
  105. package/corpus/templates/mail/README.md +35 -0
  106. package/corpus/templates/plan/README.md +30 -118
  107. package/corpus/templates/plan/app/global.css +1 -1
  108. package/corpus/templates/slides/README.md +34 -0
  109. package/corpus/templates/videos/README.md +22 -201
  110. package/dist/a2a/caller-auth.d.ts +1 -0
  111. package/dist/a2a/caller-auth.d.ts.map +1 -1
  112. package/dist/a2a/caller-auth.js +34 -8
  113. package/dist/a2a/caller-auth.js.map +1 -1
  114. package/dist/a2a/client.d.ts +4 -0
  115. package/dist/a2a/client.d.ts.map +1 -1
  116. package/dist/a2a/client.js +148 -58
  117. package/dist/a2a/client.js.map +1 -1
  118. package/dist/agent/production-agent.d.ts.map +1 -1
  119. package/dist/agent/production-agent.js +19 -4
  120. package/dist/agent/production-agent.js.map +1 -1
  121. package/dist/agent/types.d.ts +1 -0
  122. package/dist/agent/types.d.ts.map +1 -1
  123. package/dist/agent/types.js.map +1 -1
  124. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  125. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  126. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  127. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  128. package/dist/cli/recap.d.ts +3 -3
  129. package/dist/cli/recap.d.ts.map +1 -1
  130. package/dist/cli/recap.js +41 -34
  131. package/dist/cli/recap.js.map +1 -1
  132. package/dist/client/AssistantChat.d.ts.map +1 -1
  133. package/dist/client/AssistantChat.js +28 -4
  134. package/dist/client/AssistantChat.js.map +1 -1
  135. package/dist/client/PoweredByBadge.d.ts +9 -3
  136. package/dist/client/PoweredByBadge.d.ts.map +1 -1
  137. package/dist/client/PoweredByBadge.js +47 -15
  138. package/dist/client/PoweredByBadge.js.map +1 -1
  139. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  140. package/dist/client/agent-chat-adapter.js +5 -1
  141. package/dist/client/agent-chat-adapter.js.map +1 -1
  142. package/dist/client/guided-questions.d.ts.map +1 -1
  143. package/dist/client/guided-questions.js +10 -6
  144. package/dist/client/guided-questions.js.map +1 -1
  145. package/dist/client/i18n.d.ts +1 -1
  146. package/dist/client/i18n.d.ts.map +1 -1
  147. package/dist/client/i18n.js +6 -2
  148. package/dist/client/i18n.js.map +1 -1
  149. package/dist/client/sse-event-processor.d.ts +2 -0
  150. package/dist/client/sse-event-processor.d.ts.map +1 -1
  151. package/dist/client/sse-event-processor.js +121 -5
  152. package/dist/client/sse-event-processor.js.map +1 -1
  153. package/dist/client/tool-display.d.ts.map +1 -1
  154. package/dist/client/tool-display.js +8 -0
  155. package/dist/client/tool-display.js.map +1 -1
  156. package/dist/collab/awareness.d.ts +2 -2
  157. package/dist/collab/awareness.d.ts.map +1 -1
  158. package/dist/collab/routes.d.ts +1 -1
  159. package/dist/file-upload/builder.d.ts.map +1 -1
  160. package/dist/file-upload/builder.js +26 -8
  161. package/dist/file-upload/builder.js.map +1 -1
  162. package/dist/file-upload/types.d.ts +5 -1
  163. package/dist/file-upload/types.d.ts.map +1 -1
  164. package/dist/file-upload/types.js.map +1 -1
  165. package/dist/integrations/a2a-continuation-processor.js +43 -18
  166. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  167. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  168. package/dist/mcp/builtin-tools.js +3 -0
  169. package/dist/mcp/builtin-tools.js.map +1 -1
  170. package/dist/mcp/org-directory.d.ts.map +1 -1
  171. package/dist/mcp/org-directory.js +58 -20
  172. package/dist/mcp/org-directory.js.map +1 -1
  173. package/dist/notifications/channels.d.ts +10 -0
  174. package/dist/notifications/channels.d.ts.map +1 -1
  175. package/dist/notifications/channels.js +172 -20
  176. package/dist/notifications/channels.js.map +1 -1
  177. package/dist/notifications/routes.d.ts +3 -3
  178. package/dist/observability/routes.d.ts +5 -5
  179. package/dist/progress/routes.d.ts +1 -1
  180. package/dist/resources/handlers.d.ts +1 -1
  181. package/dist/scripts/call-agent.d.ts.map +1 -1
  182. package/dist/scripts/call-agent.js +2 -2
  183. package/dist/scripts/call-agent.js.map +1 -1
  184. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  185. package/dist/server/analytics.d.ts +16 -0
  186. package/dist/server/analytics.d.ts.map +1 -1
  187. package/dist/server/analytics.js +30 -7
  188. package/dist/server/analytics.js.map +1 -1
  189. package/dist/server/ssr-handler.d.ts.map +1 -1
  190. package/dist/server/ssr-handler.js +28 -12
  191. package/dist/server/ssr-handler.js.map +1 -1
  192. package/dist/server/transcribe-voice.d.ts +1 -1
  193. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  OpenSourceBadge,
3
3
  PoweredByBadge,
4
+ LanguagePicker,
4
5
  StarfieldBackground,
5
6
  useT,
6
7
  } from "@agent-native/core/client";
@@ -53,13 +54,23 @@ function BookingPageShell({
53
54
  return (
54
55
  <div
55
56
  className={cn(
56
- "relative min-h-screen overflow-x-hidden bg-background p-4",
57
+ "relative min-h-screen bg-background dark:bg-black",
57
58
  className,
58
59
  )}
59
60
  >
60
61
  <StarfieldBackground className="fixed inset-0 opacity-25 dark:opacity-60" />
61
- <div className="pointer-events-none fixed inset-0 bg-[radial-gradient(ellipse_at_center,hsl(var(--background)/0.35)_0%,hsl(var(--background)/0.88)_72%)]" />
62
- <div className="relative z-10 flow-root">{children}</div>
62
+ <div className="pointer-events-none fixed inset-0 bg-[radial-gradient(ellipse_at_center,hsl(var(--background)/0.35)_0%,hsl(var(--background)/0.88)_72%)] dark:bg-[radial-gradient(ellipse_at_center,hsl(var(--background)/0.35)_0%,#000000_100%)]" />
63
+ <div className="fixed top-4 right-4 z-50 flex items-center gap-1">
64
+ <LanguagePicker variant="ghost-icon" />
65
+ <ThemeToggle />
66
+ </div>
67
+ <div className="fixed bottom-[21px] left-4 z-50 flex flex-col items-start gap-2 max-sm:static max-sm:mx-auto max-sm:mt-8">
68
+ <PoweredByBadge variant="plain" embedded />
69
+ <OpenSourceBadge embedded />
70
+ </div>
71
+ <div className="relative z-10 min-h-screen overflow-x-hidden p-4">
72
+ {children}
73
+ </div>
63
74
  </div>
64
75
  );
65
76
  }
@@ -281,9 +292,6 @@ export default function BookingPage() {
281
292
 
282
293
  return (
283
294
  <BookingPageShell className="pb-20">
284
- <div className="absolute top-4 right-4 z-20">
285
- <ThemeToggle />
286
- </div>
287
295
  <div className="mx-auto mt-[7.5vh] w-full max-w-lg">
288
296
  {/* Header */}
289
297
  <div className="mb-8 text-center">
@@ -509,8 +517,6 @@ export default function BookingPage() {
509
517
  )}
510
518
  </div>
511
519
  </div>
512
- <OpenSourceBadge />
513
- <PoweredByBadge />
514
520
  </BookingPageShell>
515
521
  );
516
522
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Meeting invite pages have cleaner branding, a black dark-mode backdrop, and a language picker.
@@ -0,0 +1,32 @@
1
+ # Chat
2
+
3
+ The minimal agent-native starter app — a clean, ChatGPT-style shell with chat at
4
+ the center, durable threads, standard app navigation, auth, live sync, and
5
+ actions. Start here when you want a real browser app to build on without
6
+ committing to a domain template.
7
+
8
+ **Live app: [chat.agent-native.com](https://chat.agent-native.com)**
9
+
10
+ Chat is the basic agent-native app starting point. It gives you the app-agent
11
+ loop wired end to end and one example action, so you can add your own UI, data,
12
+ and actions on top.
13
+
14
+ ## Features
15
+
16
+ - ChatGPT-style shell with a threads list and durable chat history.
17
+ - Auth, live sync, and application state wired out of the box.
18
+ - The action surface the agent and UI share, plus one example action to copy.
19
+ - A minimal, brandable base for any domain app.
20
+
21
+ ## Develop locally
22
+
23
+ Scaffold your own copy and run it:
24
+
25
+ ```bash
26
+ npx @agent-native/core@latest create my-app --standalone --template chat
27
+ cd my-app
28
+ pnpm install
29
+ pnpm dev
30
+ ```
31
+
32
+ Full docs: [agent-native.com/docs/template-chat](https://agent-native.com/docs/template-chat).
@@ -0,0 +1,37 @@
1
+ # Clips
2
+
3
+ An open-source, agent-native alternative to Loom, Granola, and Wispr Flow rolled
4
+ into one app. Screen recording, calendar-synced meeting notes, and push-to-talk
5
+ voice dictation — all transcribed, searchable, and yours to own.
6
+
7
+ **Live app: [clips.agent-native.com](https://clips.agent-native.com)**
8
+
9
+ The agent transcribes, titles, summarizes, and indexes everything you capture,
10
+ then lets you ask "find the clip where we discussed the rollout plan" across every
11
+ transcript. Shared clips are agent-readable: paste a share link into an agent and
12
+ it can read the transcript and see timestamped frames without the raw video.
13
+
14
+ ## Features
15
+
16
+ - Screen recording with webcam overlay, audio capture, and pause/trim.
17
+ - Calendar-synced meeting recordings with AI summary, notes, and action items.
18
+ - Push-to-talk (Fn-hold) dictation with searchable history and vocabulary biasing.
19
+ - Auto-generated titles, summaries, and chapter markers for every recording.
20
+ - Full-text search across recordings, meetings, and dictations in one library.
21
+ - Sharing with per-clip permissions, agent-readable links, and Slack unfurls.
22
+ - Chrome extension for capturing browser logs with a recording.
23
+
24
+ ## Develop locally
25
+
26
+ Scaffold your own copy and run it:
27
+
28
+ ```bash
29
+ npx @agent-native/core@latest create my-clips --standalone --template clips
30
+ cd my-clips
31
+ pnpm install
32
+ pnpm dev
33
+ ```
34
+
35
+ Clips needs a video storage backend (Builder.io or an S3-compatible bucket)
36
+ before recordings can upload — see the docs for setup.
37
+ Full docs: [agent-native.com/docs/template-clips](https://agent-native.com/docs/template-clips).
@@ -23,6 +23,7 @@ import {
23
23
  stringifySpaceIds,
24
24
  } from "../server/lib/recordings.js";
25
25
  import { setResumableSession } from "../server/lib/resumable-session.js";
26
+ import { isStreamingUploadDisabled } from "../server/lib/streaming-upload-mode.js";
26
27
  import { createRecordingSchema } from "./lib/create-recording-schema.js";
27
28
  import { DEFAULT_RECORDING_TITLE } from "./lib/title-source.js";
28
29
 
@@ -86,7 +87,11 @@ export default defineAction({
86
87
  // init fails.
87
88
  let uploadMode: UploadMode = "buffered";
88
89
  const uploadProvider = await getActiveFileUploadProviderForRequest();
89
- if (args.requestStreaming && uploadProvider?.resumable) {
90
+ if (
91
+ args.requestStreaming &&
92
+ !isStreamingUploadDisabled() &&
93
+ uploadProvider?.resumable
94
+ ) {
90
95
  try {
91
96
  const recordingMimeType =
92
97
  args.mimeType?.split(";")[0]?.trim() || "video/webm";
@@ -103,7 +108,7 @@ export default defineAction({
103
108
  await setResumableSession(id, {
104
109
  providerId: uploadProvider.id,
105
110
  sessionId: session.sessionId,
106
- meta: session.meta,
111
+ meta: { ...session.meta, skipCompressionWait: true },
107
112
  bytesUploaded: 0,
108
113
  lastCommittedIndex: -1,
109
114
  });
@@ -39,6 +39,7 @@ import {
39
39
  deleteResumableSession,
40
40
  getResumableSession,
41
41
  } from "../server/lib/resumable-session.js";
42
+ import { isStreamingUploadDisabled } from "../server/lib/streaming-upload-mode.js";
42
43
  import {
43
44
  requiresConfiguredVideoStorage,
44
45
  STORAGE_SETUP_REQUIRED_REASON,
@@ -341,6 +342,11 @@ export default defineAction({
341
342
  // Resumable path: create-recording initialized a session and chunk.post.ts
342
343
  // forwarded all chunks to the provider. Complete the session to get the CDN URL.
343
344
  const resumableSession = await getResumableSession(id);
345
+ if (resumableSession && isStreamingUploadDisabled()) {
346
+ console.warn(
347
+ `[finalize] streaming uploads are disabled, but completing existing resumable session for in-flight recording: ${id}`,
348
+ );
349
+ }
344
350
  if (resumableSession) {
345
351
  debugLog("[finalize] resumable session found, completing upload", {
346
352
  id,
@@ -351,6 +357,9 @@ export default defineAction({
351
357
  if (!uploadProvider?.resumable) {
352
358
  throw new Error("No resumable upload provider configured");
353
359
  }
360
+ if (resumableSession.bytesUploaded <= 0) {
361
+ throw new Error("Recording upload contained no video bytes");
362
+ }
354
363
  const videoUrl = await uploadProvider.resumable.completeSession(
355
364
  {
356
365
  sessionId: resumableSession.sessionId,
@@ -359,6 +368,7 @@ export default defineAction({
359
368
  typeof resumableSession.meta.filename === "string"
360
369
  ? resumableSession.meta.filename
361
370
  : "",
371
+ { skipCompressionWait: true },
362
372
  );
363
373
  debugLog("[finalize] resumable upload completed", { id, videoUrl });
364
374
  const result = await markRecordingReady({
@@ -560,6 +570,7 @@ export default defineAction({
560
570
  filename: `${id}.${videoFormat}`,
561
571
  mimeType,
562
572
  ownerEmail,
573
+ skipCompressionWait: true,
563
574
  });
564
575
  } catch (err) {
565
576
  // Capture structured context so a "Builder.io upload failed (500)" can
@@ -233,10 +233,12 @@ export default defineAction({
233
233
  // `?password=<pw>` (legacy fallback) so old share pages keep
234
234
  // working during rollout. (audit 11 F-07)
235
235
  // Owners are skipped — the blob route bypasses the password gate
236
- // for them, so they don't need the token. Non-Loom provider URLs
237
- // (R2/S3/Builder) are left untouched; those are already signed.
236
+ // for them, so they don't need the token. Remote provider URLs are
237
+ // still proxied through same-origin media serving so CORS, range
238
+ // requests, and signed URL quirks match public share playback.
238
239
  const resolvedVideoUrl = resolvePlayerVideoUrl(rec, {
239
240
  addPasswordToken: access.role !== "owner",
241
+ proxyRemoteMedia: true,
240
242
  });
241
243
 
242
244
  return {
@@ -295,6 +295,7 @@ export default defineAction({
295
295
  filename: `${id}.mp4`,
296
296
  mimeType: media.mimeType,
297
297
  ownerEmail,
298
+ skipCompressionWait: true,
298
299
  });
299
300
 
300
301
  const recordingValues = buildRecordingValues(media.sizeBytes);
@@ -17,6 +17,18 @@ export type LoomVideoDownload = {
17
17
  sourceUrl: string;
18
18
  };
19
19
 
20
+ const LOOM_VIDEO_UNAVAILABLE_MESSAGE =
21
+ "Loom did not provide a downloadable MP4 for this video. Download the original from Loom and use Upload video in Clips.";
22
+
23
+ export class LoomVideoUnavailableError extends Error {
24
+ statusCode = 422;
25
+
26
+ constructor(message = LOOM_VIDEO_UNAVAILABLE_MESSAGE) {
27
+ super(message);
28
+ this.name = "LoomVideoUnavailableError";
29
+ }
30
+ }
31
+
20
32
  function formatBytes(bytes: number): string {
21
33
  if (bytes >= 1024 * 1024) {
22
34
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
@@ -129,6 +141,10 @@ async function fetchTranscodedVideoUrl({
129
141
  { maxRedirects: 2 },
130
142
  );
131
143
 
144
+ if (response.status === 204) {
145
+ throw new LoomVideoUnavailableError();
146
+ }
147
+
132
148
  if (!response.ok) {
133
149
  throw new Error(
134
150
  `Loom did not provide a downloadable MP4 (${response.status} ${response.statusText}). Make sure the link is public and allows playback.`,
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  getBrowserTabId,
3
- sendToAgentChat,
4
3
  setClientAppState,
5
4
  useSession,
6
5
  useT,
@@ -225,6 +224,21 @@ export function LibraryGrid({
225
224
  }
226
225
  };
227
226
 
227
+ const moveSingle = async (
228
+ rec: RecordingSummary,
229
+ targetFolderId: string | null,
230
+ ) => {
231
+ try {
232
+ await moveRecording.mutateAsync({
233
+ id: rec.id,
234
+ folderId: targetFolderId,
235
+ });
236
+ toast.success(t("libraryGrid.clipsMoved", { count: 1 }));
237
+ } catch (err: any) {
238
+ toast.error(err?.message ?? t("libraryGrid.moveFailed"));
239
+ }
240
+ };
241
+
228
242
  const openRenameDialog = (rec: RecordingSummary) => {
229
243
  setRenamingRec(rec);
230
244
  setRenameValue(isDefaultTitle(rec.title) ? "" : (rec.title ?? ""));
@@ -385,12 +399,9 @@ export function LibraryGrid({
385
399
  ? openRenameDialog
386
400
  : undefined
387
401
  }
388
- onMove={(rec) => {
389
- sendToAgentChat({
390
- message: `Move the clip "${rec.title}" (id: ${rec.id}) to a folder. Ask me which folder to move it to, or list available folders.`,
391
- background: false,
392
- });
393
- }}
402
+ moveTargets={moveTargets}
403
+ onMove={canMoveSelection ? moveSingle : undefined}
404
+ isMovePending={moveRecording.isPending}
394
405
  onTrash={(rec) => {
395
406
  trashRecording.mutate(
396
407
  { id: rec.id },
@@ -24,6 +24,9 @@ import {
24
24
  DropdownMenuContent,
25
25
  DropdownMenuItem,
26
26
  DropdownMenuSeparator,
27
+ DropdownMenuSub,
28
+ DropdownMenuSubContent,
29
+ DropdownMenuSubTrigger,
27
30
  DropdownMenuTrigger,
28
31
  } from "@/components/ui/dropdown-menu";
29
32
  import { isDefaultTitle } from "@/hooks/use-auto-title";
@@ -32,6 +35,8 @@ import { isStaleRecordingUpload } from "@/lib/recording-status";
32
35
  import { isStorageSetupFailureReason } from "@/lib/storage-failures";
33
36
  import { cn } from "@/lib/utils";
34
37
 
38
+ import type { BulkMoveTarget } from "./bulk-action-toolbar";
39
+
35
40
  function formatDuration(ms: number): string {
36
41
  const totalSeconds = Math.max(0, Math.floor(ms / 1000));
37
42
  const m = Math.floor(totalSeconds / 60);
@@ -59,7 +64,9 @@ interface RecordingCardProps {
59
64
  selectionMode?: boolean;
60
65
  onToggleSelect?: (id: string) => void;
61
66
  onShare?: (rec: RecordingSummary) => void;
62
- onMove?: (rec: RecordingSummary) => void;
67
+ onMove?: (rec: RecordingSummary, folderId: string | null) => void;
68
+ moveTargets?: BulkMoveTarget[];
69
+ isMovePending?: boolean;
63
70
  onRename?: (rec: RecordingSummary) => void;
64
71
  onArchive?: (rec: RecordingSummary) => void;
65
72
  onTrash?: (rec: RecordingSummary) => void;
@@ -73,6 +80,8 @@ export function RecordingCard({
73
80
  onToggleSelect,
74
81
  onShare,
75
82
  onMove,
83
+ moveTargets = [],
84
+ isMovePending = false,
76
85
  onRename,
77
86
  onArchive,
78
87
  onTrash,
@@ -112,6 +121,7 @@ export function RecordingCard({
112
121
  /native recording|native fullscreen|screencapture|avconvert/i.test(
113
122
  recording.failureReason ?? "",
114
123
  );
124
+ const canMove = Boolean(onMove && moveTargets.length > 0);
115
125
 
116
126
  const displayThumbnail = useMemo(() => {
117
127
  if (hovered && recording.animatedThumbnailUrl)
@@ -332,10 +342,37 @@ export function RecordingCard({
332
342
  <IconShare className="h-4 w-4 me-2" />{" "}
333
343
  {t("recordingPage.share")}
334
344
  </DropdownMenuItem>
335
- <DropdownMenuItem onSelect={() => onMove?.(recording)}>
336
- <IconFolder className="h-4 w-4 me-2" />{" "}
337
- {t("clipsFinalRaw.moveToFolder")}
338
- </DropdownMenuItem>
345
+ {canMove ? (
346
+ <DropdownMenuSub>
347
+ <DropdownMenuSubTrigger>
348
+ <IconFolder className="h-4 w-4 me-2" />{" "}
349
+ {t("clipsFinalRaw.moveToFolder")}
350
+ </DropdownMenuSubTrigger>
351
+ <DropdownMenuSubContent className="w-64">
352
+ {moveTargets.map((target, index) => (
353
+ <DropdownMenuItem
354
+ key={target.id ?? `root-${index}`}
355
+ disabled={target.disabled || isMovePending}
356
+ onSelect={() => onMove?.(recording, target.id)}
357
+ >
358
+ <span
359
+ className="truncate"
360
+ style={{
361
+ paddingInlineStart: (target.depth ?? 0) * 12,
362
+ }}
363
+ >
364
+ {target.name}
365
+ </span>
366
+ {target.disabled && (
367
+ <span className="ms-auto text-xs text-muted-foreground">
368
+ {t("clipsFinalRaw.current")}
369
+ </span>
370
+ )}
371
+ </DropdownMenuItem>
372
+ ))}
373
+ </DropdownMenuSubContent>
374
+ </DropdownMenuSub>
375
+ ) : null}
339
376
  {onRename ? (
340
377
  <>
341
378
  <DropdownMenuSeparator />
@@ -153,11 +153,11 @@ export function Scrubber(props: ScrubberProps) {
153
153
  <div
154
154
  ref={barRef}
155
155
  data-player-scrubber-bar
156
- className="relative w-full h-1.5 bg-white/20 rounded-full cursor-pointer group/bar"
156
+ className="relative w-full h-1.5 bg-white/35 rounded-full cursor-pointer group/bar shadow-[0_0_0_1px_rgba(0,0,0,0.16)]"
157
157
  >
158
158
  {/* Filled portion */}
159
159
  <div
160
- className="absolute inset-y-0 left-0 bg-primary rounded-full"
160
+ className="absolute inset-y-0 left-0 bg-white rounded-full shadow-[0_0_10px_rgba(255,255,255,0.45)]"
161
161
  style={{ width: pct + "%" }}
162
162
  />
163
163
 
@@ -95,10 +95,11 @@ export function CountdownOverlay({
95
95
 
96
96
  <div
97
97
  key={remaining}
98
- className="flex h-44 w-44 items-center justify-center rounded-full text-[112px] font-bold leading-none text-white shadow-2xl duration-200 animate-in zoom-in-75 fade-in"
98
+ className="flex min-w-32 items-center justify-center text-[clamp(96px,22vmin,240px)] font-extrabold leading-none text-white duration-200 animate-in zoom-in-75 fade-in"
99
99
  style={{
100
- background:
101
- "radial-gradient(circle at 30% 30%, rgba(255,255,255,0.18), transparent 60%), hsl(var(--primary))",
100
+ fontVariantNumeric: "tabular-nums",
101
+ textShadow:
102
+ "1px 0 1px rgba(17,24,39,0.22), -1px 0 1px rgba(17,24,39,0.22), 0 1px 1px rgba(17,24,39,0.22), 0 -1px 1px rgba(17,24,39,0.22), 0 2px 4px rgba(0,0,0,0.48), 0 12px 34px rgba(0,0,0,0.36), 0 0 2px rgba(0,0,0,0.72)",
102
103
  }}
103
104
  >
104
105
  {remaining > 0 ? remaining : "Go"}
@@ -58,6 +58,8 @@ import {
58
58
  import {
59
59
  NO_CAMERA_DEVICE_ID,
60
60
  NO_MIC_DEVICE_ID,
61
+ normalizeDisplaySurfaceForRuntime,
62
+ supportsBrowserTabCapture,
61
63
  type DisplaySurface,
62
64
  type RecordingMode,
63
65
  } from "./recorder-engine";
@@ -157,14 +159,20 @@ export function PreRecordPanel({
157
159
  const t = useT();
158
160
  const fileInputRef = useRef<HTMLInputElement>(null);
159
161
  const loomInputRef = useRef<HTMLInputElement>(null);
162
+ const browserTabCaptureSupported = useMemo(
163
+ () => supportsBrowserTabCapture(),
164
+ [],
165
+ );
160
166
  // Saved selections from the last visit. A `?mode=`/`?surface=` deep link
161
167
  // (initialMode/initialDisplaySurface) still takes precedence over them.
162
168
  const savedPrefs = useMemo(() => loadRecorderPreferences(), []);
163
169
  const [mode, setMode] = useState<RecordingMode>(
164
170
  () => initialMode ?? savedPrefs.mode ?? "screen+camera",
165
171
  );
166
- const [displaySurface, setDisplaySurface] = useState<DisplaySurface>(
167
- () => initialDisplaySurface ?? savedPrefs.displaySurface ?? "window",
172
+ const [displaySurface, setDisplaySurface] = useState<DisplaySurface>(() =>
173
+ normalizeDisplaySurfaceForRuntime(
174
+ initialDisplaySurface ?? savedPrefs.displaySurface ?? "window",
175
+ ),
168
176
  );
169
177
  const [sourceOpen, setSourceOpen] = useState(false);
170
178
  const [deviceSettingsOpen, setDeviceSettingsOpen] = useState(false);
@@ -223,8 +231,8 @@ export function PreRecordPanel({
223
231
  [isMobile, modeOptions],
224
232
  );
225
233
 
226
- const surfaceOptions = useMemo<SurfaceOption[]>(
227
- () => [
234
+ const surfaceOptions = useMemo<SurfaceOption[]>(() => {
235
+ const options: SurfaceOption[] = [
228
236
  {
229
237
  value: "window",
230
238
  label: t("preRecord.surfaceWindow"),
@@ -243,9 +251,11 @@ export function PreRecordPanel({
243
251
  icon: IconDeviceScreen,
244
252
  sub: t("preRecord.surfaceScreenDescription"),
245
253
  },
246
- ],
247
- [t],
248
- );
254
+ ];
255
+ return browserTabCaptureSupported
256
+ ? options
257
+ : options.filter((option) => option.value !== "browser");
258
+ }, [browserTabCaptureSupported, t]);
249
259
 
250
260
  useEffect(() => {
251
261
  if (isMobile) {
@@ -258,9 +268,19 @@ export function PreRecordPanel({
258
268
  }, [initialMode, isMobile]);
259
269
 
260
270
  useEffect(() => {
261
- if (initialDisplaySurface) setDisplaySurface(initialDisplaySurface);
271
+ if (initialDisplaySurface) {
272
+ setDisplaySurface(
273
+ normalizeDisplaySurfaceForRuntime(initialDisplaySurface),
274
+ );
275
+ }
262
276
  }, [initialDisplaySurface]);
263
277
 
278
+ useEffect(() => {
279
+ if (displaySurface !== "browser" || browserTabCaptureSupported) return;
280
+ setDisplaySurface("window");
281
+ saveRecorderPreferences({ displaySurface: "window" });
282
+ }, [browserTabCaptureSupported, displaySurface]);
283
+
264
284
  const enumerateDevices = useCallback(async () => {
265
285
  try {
266
286
  if (!navigator.mediaDevices?.enumerateDevices) {
@@ -350,8 +370,9 @@ export function PreRecordPanel({
350
370
  saveRecorderPreferences({ mode: value });
351
371
  }, []);
352
372
  const chooseDisplaySurface = useCallback((value: DisplaySurface) => {
353
- setDisplaySurface(value);
354
- saveRecorderPreferences({ displaySurface: value });
373
+ const next = normalizeDisplaySurfaceForRuntime(value);
374
+ setDisplaySurface(next);
375
+ saveRecorderPreferences({ displaySurface: next });
355
376
  }, []);
356
377
  const chooseMic = useCallback((value: string) => {
357
378
  setMicId(value);
@@ -676,7 +697,12 @@ export function PreRecordPanel({
676
697
  </button>
677
698
  </CollapsibleTrigger>
678
699
  <CollapsibleContent>
679
- <div className="grid grid-cols-3 gap-2 px-6 pb-5">
700
+ <div
701
+ className={cn(
702
+ "grid gap-2 px-6 pb-5",
703
+ surfaceOptions.length === 2 ? "grid-cols-2" : "grid-cols-3",
704
+ )}
705
+ >
680
706
  {surfaceOptions.map((opt) => {
681
707
  const Icon = opt.icon;
682
708
  const active = opt.value === displaySurface;
@@ -913,7 +939,8 @@ export function PreRecordPanel({
913
939
  // to acquire a webcam stream.
914
940
  mode:
915
941
  mode === "screen+camera" && !needsCamera ? "screen" : mode,
916
- displaySurface,
942
+ displaySurface:
943
+ normalizeDisplaySurfaceForRuntime(displaySurface),
917
944
  micDeviceId: micId === "default" ? null : micId,
918
945
  cameraDeviceId:
919
946
  needsCamera && cameraId !== "default" ? cameraId : null,