@agent-native/core 0.141.3 → 0.141.4

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 (152) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
  3. package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
  4. package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
  5. package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
  7. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  8. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  9. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  10. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
  12. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
  13. package/corpus/templates/analytics/server/lib/gong.ts +132 -33
  14. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  15. package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
  16. package/corpus/templates/analytics/server/plugins/db.ts +11 -3
  17. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  18. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
  19. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  20. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  21. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  22. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  23. package/corpus/templates/clips/actions/invite-member.ts +39 -16
  24. package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
  25. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  28. package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
  29. package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
  31. package/corpus/templates/clips/desktop/src/styles.css +0 -3
  32. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  33. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  34. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  35. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
  36. package/corpus/templates/clips/shared/email-preview.ts +61 -0
  37. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  38. package/corpus/templates/content/server/lib/emails.ts +70 -0
  39. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  40. package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
  41. package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
  42. package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
  43. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  44. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  45. package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
  46. package/corpus/templates/factory/AGENTS.md +18 -17
  47. package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
  48. package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
  49. package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
  50. package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
  51. package/corpus/templates/factory/app/routes/factory.tsx +376 -206
  52. package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
  53. package/corpus/templates/factory/server/db/schema.ts +6 -0
  54. package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
  55. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
  56. package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
  57. package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
  58. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  59. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  60. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  61. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  62. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  63. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  64. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  65. package/corpus/templates/slides/actions/import-from-url.ts +1 -0
  66. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
  67. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
  68. package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
  69. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
  70. package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
  71. package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
  72. package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
  73. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
  74. package/corpus/templates/slides/app/pages/Index.tsx +127 -137
  75. package/corpus/templates/slides/app/root.tsx +8 -0
  76. package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
  77. package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
  79. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  80. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  81. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  82. package/dist/brand-kit/index.d.ts +1 -0
  83. package/dist/brand-kit/index.js +1 -0
  84. package/dist/brand-kit/tokens.d.ts +91 -0
  85. package/dist/brand-kit/tokens.js +279 -0
  86. package/dist/brand-kit/types.d.ts +25 -0
  87. package/dist/client/AssistantChat.js +7 -7
  88. package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
  89. package/dist/client/onboarding/first-run-registry.d.ts +25 -0
  90. package/dist/client/onboarding/first-run-registry.js +18 -0
  91. package/dist/client/onboarding/index.d.ts +2 -0
  92. package/dist/client/onboarding/index.js +1 -0
  93. package/dist/collab/routes.d.ts +1 -1
  94. package/dist/deploy/build.d.ts +8 -0
  95. package/dist/deploy/build.js +92 -3
  96. package/dist/deploy/workspace-deploy.js +86 -3
  97. package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
  98. package/dist/email-catalog/actions/list-email-activity.js +19 -0
  99. package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
  100. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  101. package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
  102. package/dist/email-catalog/actions/list-email-log.js +23 -0
  103. package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
  104. package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
  105. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
  106. package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
  107. package/dist/email-catalog/authorize.d.ts +1 -0
  108. package/dist/email-catalog/authorize.js +9 -0
  109. package/dist/email-catalog/log.d.ts +60 -0
  110. package/dist/email-catalog/log.js +142 -0
  111. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  112. package/dist/email-catalog/provider-metrics.js +182 -0
  113. package/dist/email-catalog/registry.d.ts +78 -0
  114. package/dist/email-catalog/registry.js +70 -0
  115. package/dist/email-catalog/schema.d.ts +231 -0
  116. package/dist/email-catalog/schema.js +53 -0
  117. package/dist/email-catalog/system-emails.d.ts +13 -0
  118. package/dist/email-catalog/system-emails.js +67 -0
  119. package/dist/jobs/index.d.ts +2 -0
  120. package/dist/jobs/index.js +2 -0
  121. package/dist/jobs/run-history.js +48 -1
  122. package/dist/jobs/scheduler-dispatch.d.ts +7 -0
  123. package/dist/jobs/scheduler-dispatch.js +7 -0
  124. package/dist/jobs/scheduler-health.d.ts +22 -0
  125. package/dist/jobs/scheduler-health.js +172 -0
  126. package/dist/jobs/scheduler.js +67 -2
  127. package/dist/localization/default-messages.d.ts +45 -0
  128. package/dist/localization/default-messages.js +45 -0
  129. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  130. package/dist/notifications/routes.d.ts +3 -3
  131. package/dist/observability/routes.d.ts +5 -5
  132. package/dist/org/handlers.js +9 -1
  133. package/dist/progress/routes.d.ts +1 -1
  134. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  135. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  136. package/dist/secrets/routes.d.ts +9 -9
  137. package/dist/server/action-discovery.js +22 -0
  138. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
  139. package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
  140. package/dist/server/agent-chat-plugin.d.ts +2 -1
  141. package/dist/server/agent-chat-plugin.js +39 -1
  142. package/dist/server/better-auth-instance.js +17 -2
  143. package/dist/server/design-token-utils.d.ts +6 -1
  144. package/dist/server/design-token-utils.js +252 -59
  145. package/dist/server/email.d.ts +11 -0
  146. package/dist/server/email.js +54 -3
  147. package/dist/server/index.d.ts +1 -0
  148. package/dist/server/index.js +1 -0
  149. package/dist/server/realtime-token.d.ts +1 -1
  150. package/dist/server/transcribe-voice.d.ts +1 -1
  151. package/dist/vite/client.js +20 -14
  152. package/package.json +2 -1
@@ -24,6 +24,7 @@ import { and, eq, sql } from "drizzle-orm";
24
24
  import { z } from "zod";
25
25
 
26
26
  import { getDb } from "../server/db/index.js";
27
+ import { CLIPS_ORGANIZATION_INVITE_EMAIL_ID } from "../server/lib/emails.js";
27
28
  import {
28
29
  getCurrentOwnerEmail,
29
30
  nanoid,
@@ -67,6 +68,36 @@ async function fetchOrgName(orgId: string): Promise<string> {
67
68
  return row?.name ?? "Organization";
68
69
  }
69
70
 
71
+ export function renderClipsInviteEmail({
72
+ appName,
73
+ orgName,
74
+ inviter,
75
+ role,
76
+ inviteUrl,
77
+ }: {
78
+ appName: string;
79
+ orgName: string;
80
+ inviter: string;
81
+ role: "admin" | "member";
82
+ inviteUrl: string;
83
+ }) {
84
+ return {
85
+ subject: `You're invited to ${orgName} on ${appName}`,
86
+ ...renderEmail({
87
+ brandName: appName,
88
+ preheader: `${inviter} invited you to ${orgName} on ${appName}.`,
89
+ heading: `You're invited to join ${orgName}`,
90
+ paragraphs: [
91
+ `${emailStrong(inviter)} invited you to the ${emailStrong(orgName)} organization on ${emailStrong(appName)} as ${emailStrong(role)}.`,
92
+ `Click the button below to accept the invite and start collaborating.`,
93
+ ],
94
+ cta: { label: "Accept invite", url: inviteUrl },
95
+ // guard:allow-raw-color - email HTML cannot reference CSS theme tokens.
96
+ brandColor: "#18181B",
97
+ }),
98
+ };
99
+ }
100
+
70
101
  export default defineAction({
71
102
  description:
72
103
  "Invite someone to the active organization by email. Creates a pending invitation. Role 'admin' maps to admin; all other Clips roles collapse to 'member'. Sends an email when a provider is configured.",
@@ -123,25 +154,17 @@ export default defineAction({
123
154
  const orgName = await fetchOrgName(organizationId);
124
155
  const inviteUrl = `${baseUrl()}/invite/${token}`;
125
156
 
126
- const appName = getAppName();
127
- const { html, text } = renderEmail({
128
- brandName: appName,
129
- preheader: `${inviter} invited you to ${orgName} on ${appName}.`,
130
- heading: `You're invited to join ${orgName}`,
131
- paragraphs: [
132
- `${emailStrong(inviter)} invited you to the ${emailStrong(orgName)} organization on ${emailStrong(appName)} as ${emailStrong(role)}.`,
133
- `Click the button below to accept the invite and start collaborating.`,
134
- ],
135
- cta: { label: "Accept invite", url: inviteUrl },
136
- // guard:allow-raw-color - email HTML cannot reference CSS theme tokens.
137
- brandColor: "#18181B",
138
- });
139
157
  try {
140
158
  await sendEmail({
159
+ ...renderClipsInviteEmail({
160
+ appName: getAppName(),
161
+ orgName,
162
+ inviter,
163
+ role,
164
+ inviteUrl,
165
+ }),
141
166
  to: args.email,
142
- subject: `You're invited to ${orgName} on ${appName}`,
143
- html,
144
- text,
167
+ templateId: CLIPS_ORGANIZATION_INVITE_EMAIL_ID,
145
168
  });
146
169
  } catch (err) {
147
170
  console.warn("[invite-member] email send failed:", err);
@@ -1,4 +1,5 @@
1
1
  import { appPath } from "@agent-native/core/client/api-path";
2
+ import { writeClipboardText } from "@agent-native/core/client/clipboard";
2
3
  import {
3
4
  useActionMutation,
4
5
  useActionQuery,
@@ -19,6 +20,7 @@ import {
19
20
  useState,
20
21
  type ReactNode,
21
22
  } from "react";
23
+ import { toast } from "sonner";
22
24
 
23
25
  import {
24
26
  CopyField,
@@ -44,6 +46,7 @@ import {
44
46
  import { Switch } from "@/components/ui/switch";
45
47
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
46
48
 
49
+ import { buildEmailPreviewMarkup } from "../../../shared/email-preview";
47
50
  import { isLoomEmbedUrl } from "../../../shared/loom";
48
51
  import { withShareAttribution } from "../../../shared/share-attribution";
49
52
 
@@ -58,6 +61,7 @@ export interface ShareRecordingPopoverProps {
58
61
  initialVisibility?: Visibility | null;
59
62
  initialRole?: "owner" | "admin" | "editor" | "viewer";
60
63
  videoUrl?: string | null;
64
+ thumbnailUrl?: string | null;
61
65
  animatedThumbnailUrl?: string | null;
62
66
  isLoomRecording?: boolean;
63
67
  hasPassword?: boolean;
@@ -88,8 +92,10 @@ export function ShareRecordingPopover({
88
92
  initialVisibility,
89
93
  initialRole,
90
94
  videoUrl,
95
+ thumbnailUrl,
91
96
  animatedThumbnailUrl,
92
97
  isLoomRecording = false,
98
+ hasPassword,
93
99
  children,
94
100
  open,
95
101
  onOpenChange,
@@ -108,8 +114,10 @@ export function ShareRecordingPopover({
108
114
  initialVisibility={initialVisibility}
109
115
  initialRole={initialRole}
110
116
  videoUrl={videoUrl}
117
+ thumbnailUrl={thumbnailUrl}
111
118
  animatedThumbnailUrl={animatedThumbnailUrl}
112
119
  isLoomRecording={isLoomRecording}
120
+ hasPassword={hasPassword}
113
121
  />
114
122
  </PopoverContent>
115
123
  </Popover>
@@ -127,10 +135,12 @@ export function ShareRecordingDialog({
127
135
  initialVisibility,
128
136
  initialRole,
129
137
  videoUrl,
138
+ thumbnailUrl,
130
139
  animatedThumbnailUrl,
131
140
  isLoomRecording = false,
132
141
  open,
133
142
  onOpenChange,
143
+ hasPassword,
134
144
  }: ShareRecordingDialogProps) {
135
145
  const t = useT();
136
146
  return (
@@ -147,8 +157,10 @@ export function ShareRecordingDialog({
147
157
  initialVisibility={initialVisibility}
148
158
  initialRole={initialRole}
149
159
  videoUrl={videoUrl}
160
+ thumbnailUrl={thumbnailUrl}
150
161
  animatedThumbnailUrl={animatedThumbnailUrl}
151
162
  isLoomRecording={isLoomRecording}
163
+ hasPassword={hasPassword}
152
164
  reserveCloseButton
153
165
  />
154
166
  </DialogContent>
@@ -162,18 +174,22 @@ function ShareRecordingContent({
162
174
  initialVisibility,
163
175
  initialRole,
164
176
  videoUrl,
177
+ thumbnailUrl,
165
178
  animatedThumbnailUrl,
166
179
  isLoomRecording = false,
167
180
  reserveCloseButton = false,
181
+ hasPassword,
168
182
  }: {
169
183
  recordingId: string;
170
184
  recordingTitle?: string;
171
185
  initialVisibility?: Visibility | null;
172
186
  initialRole?: "owner" | "admin" | "editor" | "viewer";
173
187
  videoUrl?: string | null;
188
+ thumbnailUrl?: string | null;
174
189
  animatedThumbnailUrl?: string | null;
175
190
  isLoomRecording?: boolean;
176
191
  reserveCloseButton?: boolean;
192
+ hasPassword?: boolean;
177
193
  }) {
178
194
  const t = useT();
179
195
  const sharesQuery = useActionQuery<SharesResponse>("list-resource-shares", {
@@ -236,13 +252,16 @@ function ShareRecordingContent({
236
252
  <TabsContent value="link" className="mt-3">
237
253
  <LinkTab
238
254
  recordingId={recordingId}
255
+ recordingTitle={recordingTitle}
239
256
  shareUrl={shareUrl}
240
257
  sharesQuery={sharesQuery}
241
258
  visibility={visibility}
242
259
  canManage={canManage}
243
260
  videoUrl={videoUrl}
261
+ thumbnailUrl={thumbnailUrl}
244
262
  animatedThumbnailUrl={animatedThumbnailUrl}
245
263
  isLoomRecording={isLoomRecording}
264
+ hasPassword={hasPassword}
246
265
  />
247
266
  </TabsContent>
248
267
 
@@ -276,22 +295,28 @@ function ShareRecordingContent({
276
295
 
277
296
  function LinkTab({
278
297
  recordingId,
298
+ recordingTitle,
279
299
  shareUrl,
280
300
  sharesQuery,
281
301
  visibility,
282
302
  canManage,
283
303
  videoUrl,
304
+ thumbnailUrl,
284
305
  animatedThumbnailUrl,
285
306
  isLoomRecording: isLoomRecordingProp,
307
+ hasPassword,
286
308
  }: {
287
309
  recordingId: string;
310
+ recordingTitle?: string;
288
311
  shareUrl: string;
289
312
  sharesQuery: SharesQuery;
290
313
  visibility: Visibility | null;
291
314
  canManage: boolean;
292
315
  videoUrl?: string | null;
316
+ thumbnailUrl?: string | null;
293
317
  animatedThumbnailUrl?: string | null;
294
318
  isLoomRecording?: boolean;
319
+ hasPassword?: boolean;
295
320
  }) {
296
321
  const t = useT();
297
322
  const { setResourceVisibility, isPending } = useResourceVisibilityMutation(
@@ -303,6 +328,44 @@ function LinkTab({
303
328
  const sharesLoaded = visibility !== null;
304
329
  const visibilityPending = isPending || sharesQuery.isLoading;
305
330
  const isLoomRecording = isLoomRecordingProp || isLoomEmbedUrl(videoUrl);
331
+ const emailPreviewThumbnailUrl = useMemo(() => {
332
+ if (!isPublic || hasPassword !== false || !sharesLoaded) return null;
333
+ const candidate = animatedThumbnailUrl || thumbnailUrl;
334
+ if (!candidate) return null;
335
+
336
+ const query = animatedThumbnailUrl ? "?animated=1" : "";
337
+ return absoluteAppUrl(
338
+ `/api/thumbnail/${encodeURIComponent(recordingId)}${query}`,
339
+ );
340
+ }, [
341
+ animatedThumbnailUrl,
342
+ hasPassword,
343
+ isPublic,
344
+ recordingId,
345
+ sharesLoaded,
346
+ thumbnailUrl,
347
+ ]);
348
+ const copyEmailPreview = useCallback(async () => {
349
+ if (!emailPreviewThumbnailUrl || !shareUrl) return;
350
+
351
+ try {
352
+ const markup = buildEmailPreviewMarkup({
353
+ title: recordingTitle?.trim() || t("recordingPage.untitledClip"),
354
+ shareUrl,
355
+ thumbnailUrl: emailPreviewThumbnailUrl,
356
+ });
357
+ const copied = await writeClipboardText(markup.plainText, {
358
+ html: markup.html,
359
+ });
360
+ if (copied) {
361
+ toast.success(t("shareDialog.emailPreviewCopied"));
362
+ } else {
363
+ toast.error(t("shareDialog.emailPreviewCopyFailed"));
364
+ }
365
+ } catch {
366
+ toast.error(t("shareDialog.emailPreviewCopyFailed"));
367
+ }
368
+ }, [emailPreviewThumbnailUrl, recordingTitle, shareUrl, t]);
306
369
  const createAgentLink = useActionMutation(
307
370
  "create-recording-agent-link" as any,
308
371
  );
@@ -439,8 +502,17 @@ function LinkTab({
439
502
  />
440
503
  ) : null}
441
504
 
442
- {videoUrl || animatedThumbnailUrl ? (
505
+ {emailPreviewThumbnailUrl || videoUrl || animatedThumbnailUrl ? (
443
506
  <div className="flex flex-wrap gap-2">
507
+ {emailPreviewThumbnailUrl ? (
508
+ <Button
509
+ variant="outline"
510
+ size="sm"
511
+ onClick={() => void copyEmailPreview()}
512
+ >
513
+ {t("shareDialog.copyEmailPreview")}
514
+ </Button>
515
+ ) : null}
444
516
  {animatedThumbnailUrl ? (
445
517
  <Button
446
518
  variant="outline"
@@ -452,6 +452,9 @@ const messages = {
452
452
  agentLinkUnavailable: "Couldn't create the agent link.",
453
453
  retryAgentLink: "Retry",
454
454
  gifPreview: "GIF preview",
455
+ copyEmailPreview: "Copy email preview",
456
+ emailPreviewCopied: "Email preview copied",
457
+ emailPreviewCopyFailed: "Couldn't copy email preview",
455
458
  openPlayer: "Open player",
456
459
  chooseFile: "Choose file",
457
460
  remove: "Remove",
@@ -235,7 +235,7 @@ export default function RecordingPage() {
235
235
  const { session, isLoading: sessionLoading } = useSession();
236
236
  const playerRef = useRef<VideoPlayerHandle | null>(null);
237
237
 
238
- const [panel, setPanel] = useState<SidePanel>("agent");
238
+ const [panel, setPanel] = useState<SidePanel>("comments");
239
239
  const [theaterMode, setTheaterMode] = useState(false);
240
240
  const [editing, setEditing] = useState(false);
241
241
  const [currentMs, setCurrentMs] = useState(0);
@@ -311,15 +311,6 @@ export default function RecordingPage() {
311
311
  }
312
312
  }, [panelParam]);
313
313
 
314
- const appliedCompactDefaultRef = useRef(false);
315
- useEffect(() => {
316
- if (appliedCompactDefaultRef.current) return;
317
- if (!panelParam && isCompactLayout) {
318
- appliedCompactDefaultRef.current = true;
319
- setPanel("comments");
320
- }
321
- }, [isCompactLayout, panelParam]);
322
-
323
314
  const playerDataQ = useActionQuery<any>(
324
315
  "get-recording-player-data",
325
316
  {
@@ -1154,21 +1145,13 @@ export default function RecordingPage() {
1154
1145
  {label}
1155
1146
  </TabsTrigger>
1156
1147
  );
1157
- const triggers = compact
1158
- ? [
1159
- trigger("comments", t("recordingPage.activity")),
1160
- trigger("transcript", t("recordingPage.transcript")),
1161
- trigger("agent", t("recordingPage.agent")),
1162
- trigger("insights", t("recordingPage.insights")),
1163
- canEdit ? trigger("settings", t("recordingPage.settings")) : null,
1164
- ]
1165
- : [
1166
- trigger("agent", t("recordingPage.agent")),
1167
- trigger("comments", t("recordingPage.activity")),
1168
- trigger("transcript", t("recordingPage.transcript")),
1169
- trigger("insights", t("recordingPage.insights")),
1170
- canEdit ? trigger("settings", t("recordingPage.settings")) : null,
1171
- ];
1148
+ const triggers = [
1149
+ trigger("comments", t("recordingPage.activity")),
1150
+ trigger("transcript", t("recordingPage.transcript")),
1151
+ trigger("agent", t("recordingPage.agent")),
1152
+ trigger("insights", t("recordingPage.insights")),
1153
+ canEdit ? trigger("settings", t("recordingPage.settings")) : null,
1154
+ ];
1172
1155
 
1173
1156
  return (
1174
1157
  <Tabs
@@ -1197,6 +1180,7 @@ export default function RecordingPage() {
1197
1180
  scope={recordingScope}
1198
1181
  showHeader={false}
1199
1182
  showTabBar={false}
1183
+ missingApiKeySetupLayout="sidebar"
1200
1184
  emptyStateText={t("recordingPage.askAboutClip")}
1201
1185
  dynamicSuggestions={false}
1202
1186
  suggestions={
@@ -1553,6 +1537,7 @@ export default function RecordingPage() {
1553
1537
  initialVisibility={recording.visibility}
1554
1538
  initialRole={role}
1555
1539
  videoUrl={recording.videoUrl}
1540
+ thumbnailUrl={recording.thumbnailUrl}
1556
1541
  animatedThumbnailUrl={recording.animatedThumbnailUrl}
1557
1542
  isLoomRecording={isLoomEmbedBacked}
1558
1543
  hasPassword={Boolean(recording.hasPassword)}
@@ -985,6 +985,7 @@ export default function ShareRoute() {
985
985
  initialVisibility={recording.visibility}
986
986
  initialRole={viewerIsOwner ? "owner" : undefined}
987
987
  videoUrl={recording.videoUrl}
988
+ thumbnailUrl={recording.thumbnailUrl}
988
989
  animatedThumbnailUrl={recording.animatedThumbnailUrl}
989
990
  isLoomRecording={isLoomEmbedBacked}
990
991
  hasPassword={Boolean(recording.hasPassword)}
@@ -1128,6 +1129,7 @@ export default function ShareRoute() {
1128
1129
  <AgentPanel
1129
1130
  emptyStateText={t("recordingPage.askAboutClip")}
1130
1131
  dynamicSuggestions={false}
1132
+ missingApiKeySetupLayout="sidebar"
1131
1133
  suggestions={[
1132
1134
  t("recordingPage.summarizeClip"),
1133
1135
  t("recordingPage.findKeyMoments"),
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Activity is now the default recording tab, and Clips only shows AI setup after confirming no provider is connected.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Clips can copy a linked email preview for public recordings
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Preparing recording now uses a clean, shadow-free status card
@@ -3634,9 +3634,6 @@ body[data-clips-route="recording-pill"] #root {
3634
3634
  backdrop-filter: blur(20px);
3635
3635
  -webkit-backdrop-filter: blur(20px);
3636
3636
  border: 1px solid rgba(255, 255, 255, 0.08);
3637
- box-shadow:
3638
- 0 14px 40px rgba(0, 0, 0, 0.45),
3639
- 0 2px 6px rgba(0, 0, 0, 0.3);
3640
3637
  color: white;
3641
3638
  font-size: 13px;
3642
3639
  font-weight: 600;
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Catalog entries for the transactional emails Clips sends.
3
+ *
4
+ * Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
5
+ * and preview them without the app having sent anything yet.
6
+ *
7
+ * Every entry except the organization invite renders through
8
+ * `renderClipsTransactionalEmail`, so a preview shows the real template rather
9
+ * than a copy of it.
10
+ */
11
+
12
+ import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
13
+
14
+ import { renderClipsInviteEmail } from "../../actions/invite-member.js";
15
+ import {
16
+ CLIPS_ACTIVITY_COMMENT_EMAIL_ID,
17
+ CLIPS_ACTIVITY_REACTION_EMAIL_ID,
18
+ CLIPS_FIRST_AGENT_VIEW_EMAIL_ID,
19
+ CLIPS_FIRST_IMPORT_EMAIL_ID,
20
+ CLIPS_FIRST_VIEW_EMAIL_ID,
21
+ CLIPS_MONTHLY_RECAP_EMAIL_ID,
22
+ CLIPS_TWO_CLIPS_EMAIL_ID,
23
+ CLIPS_UNVIEWED_REMINDER_EMAIL_ID,
24
+ renderClipsTransactionalEmail,
25
+ type ClipsTransactionalEmailInput,
26
+ type ClipsTransactionalEmailRenderOptions,
27
+ } from "./transactional-email-templates.js";
28
+
29
+ /** Obviously-fake sample data — these render in a preview pane, never send. */
30
+ const PREVIEW_OPTIONS: ClipsTransactionalEmailRenderOptions = {
31
+ appUrl: "https://example.com",
32
+ };
33
+ const SAMPLE_RECORDING_ID = "rec_sample";
34
+ const SAMPLE_TITLE = "Onboarding walkthrough";
35
+ const SAMPLE_TO = "sam.rivera@example.com";
36
+
37
+ function preview(input: ClipsTransactionalEmailInput) {
38
+ return renderClipsTransactionalEmail(input, PREVIEW_OPTIONS);
39
+ }
40
+
41
+ export const CLIPS_ORGANIZATION_INVITE_EMAIL_ID = "clips.organization-invite";
42
+
43
+ /**
44
+ * How the shared Clips sender resolves From and Reply-To for every kind it
45
+ * renders, so each entry can say so without restating the mechanism.
46
+ */
47
+ const CLIPS_SENDER =
48
+ 'From is the configured EMAIL_FROM with the display name "Agent-Native Clips"; on first-party agent-native.com deployments it becomes clips@agent-native.com. Reply-to is hello@agent-native.com.';
49
+
50
+ let registered = false;
51
+
52
+ export function registerClipsEmails(): void {
53
+ if (registered) return;
54
+ registered = true;
55
+
56
+ defineTransactionalEmail({
57
+ id: CLIPS_FIRST_VIEW_EMAIL_ID,
58
+ name: "First view on a Clip",
59
+ trigger:
60
+ "The background transactional-email sweep finds the first counted view of a Clip by someone other than its owner, recorded after transactional email was switched on. One per Clip.",
61
+ recipientLabel: "Clip owner",
62
+ recipient:
63
+ "The Clip's owner. Suppressed recipient addresses are skipped, and the send is dropped if the owner is no longer the recording's owner at send time.",
64
+ senderLabel: "Agent-Native Clips",
65
+ sender: CLIPS_SENDER,
66
+ preview: () =>
67
+ preview({
68
+ kind: "first-view",
69
+ to: SAMPLE_TO,
70
+ recordingId: SAMPLE_RECORDING_ID,
71
+ title: SAMPLE_TITLE,
72
+ viewerEmail: "alex.chen@example.com",
73
+ }),
74
+ });
75
+
76
+ defineTransactionalEmail({
77
+ id: CLIPS_UNVIEWED_REMINDER_EMAIL_ID,
78
+ name: "Unviewed Clip reminder",
79
+ trigger:
80
+ "A direct share is 48 hours old and the recipient still has no counted view of that Clip. Re-checked at send time, so a view in the meantime cancels it.",
81
+ recipientLabel: "Shared-with address",
82
+ recipient:
83
+ "The address the Clip was directly shared with. Requires the share to still exist.",
84
+ senderLabel: "Sharer via Clips",
85
+ sender:
86
+ 'Same as the other Clips emails, except the display name becomes "<sharer> (via Agent-Native Clips)" and reply-to is the sharer\'s own address when it is a valid address, falling back to hello@agent-native.com.',
87
+ preview: () =>
88
+ preview({
89
+ kind: "unviewed-reminder",
90
+ to: SAMPLE_TO,
91
+ recordingId: SAMPLE_RECORDING_ID,
92
+ title: SAMPLE_TITLE,
93
+ senderEmail: "dana.hill@example.com",
94
+ senderName: "Dana Hill",
95
+ }),
96
+ });
97
+
98
+ defineTransactionalEmail({
99
+ id: CLIPS_FIRST_AGENT_VIEW_EMAIL_ID,
100
+ name: "First agent read of a Clip",
101
+ trigger:
102
+ "An AI agent reads one of an owner's Clips for the first time since transactional email was switched on. One per owner, not per Clip.",
103
+ recipientLabel: "Clip owner",
104
+ recipient:
105
+ "The owner of the Clip the agent read. Re-checked at send time against the owner's actual first agent view.",
106
+ senderLabel: "Agent-Native Clips",
107
+ sender: CLIPS_SENDER,
108
+ preview: () =>
109
+ preview({
110
+ kind: "first-agent-view",
111
+ to: SAMPLE_TO,
112
+ recordingId: SAMPLE_RECORDING_ID,
113
+ title: SAMPLE_TITLE,
114
+ agentName: "Claude Code",
115
+ }),
116
+ });
117
+
118
+ defineTransactionalEmail({
119
+ id: CLIPS_FIRST_IMPORT_EMAIL_ID,
120
+ name: "First imported video is ready",
121
+ trigger:
122
+ "An owner's first imported (rather than recorded) video finishes processing and becomes ready. One per owner.",
123
+ recipientLabel: "Clip owner",
124
+ recipient:
125
+ "The owner of the imported recording, re-checked at send time so a later import does not resend.",
126
+ senderLabel: "Agent-Native Clips",
127
+ sender: CLIPS_SENDER,
128
+ preview: () =>
129
+ preview({
130
+ kind: "first-import",
131
+ to: SAMPLE_TO,
132
+ recordingId: SAMPLE_RECORDING_ID,
133
+ title: "Quarterly demo recording",
134
+ }),
135
+ });
136
+
137
+ defineTransactionalEmail({
138
+ id: CLIPS_MONTHLY_RECAP_EMAIL_ID,
139
+ name: "Monthly Clips recap",
140
+ trigger:
141
+ "Once per owner per calendar month, from 14:00 UTC on the 1st, for owners whose Clips had any human views or agent reads in the closed month. Metrics and the top Clip are recomputed at send time.",
142
+ recipientLabel: "Clip owner",
143
+ recipient:
144
+ "The Clip owner whose audience the recap reports. Suppressed addresses are skipped.",
145
+ senderLabel: "Agent-Native Clips",
146
+ sender: CLIPS_SENDER,
147
+ preview: () =>
148
+ preview({
149
+ kind: "monthly-recap",
150
+ to: SAMPLE_TO,
151
+ month: "2025-02",
152
+ humanViews: 42,
153
+ agentSessions: 7,
154
+ topClip: {
155
+ recordingId: SAMPLE_RECORDING_ID,
156
+ title: SAMPLE_TITLE,
157
+ thumbnailUrl: "https://example.com/thumbnails/sample.png",
158
+ durationMs: 214_000,
159
+ recordedAt: "2025-02-11T16:00:00.000Z",
160
+ humanViews: 18,
161
+ agentSessions: 4,
162
+ },
163
+ copy: {
164
+ heroLine: "Your clips were watched 42 times. 7 agents read them.",
165
+ completionNote: "68% average completion · most stopped at 2:31",
166
+ agentBreakdown: "4 from Claude Code · 3 unidentified",
167
+ },
168
+ }),
169
+ });
170
+
171
+ defineTransactionalEmail({
172
+ id: CLIPS_TWO_CLIPS_EMAIL_ID,
173
+ name: "Two Clips received",
174
+ trigger:
175
+ "A one-time nudge once someone who owns no Clips of their own has had two distinct Clips directly shared with them. The summary line is AI-generated before the job is released to send.",
176
+ recipientLabel: "Shared-with address",
177
+ recipient:
178
+ "The shared-with address. Dropped at send time if they now own a Clip or no longer hold both shares.",
179
+ senderLabel: "Agent-Native Clips",
180
+ sender: CLIPS_SENDER,
181
+ preview: () =>
182
+ preview({
183
+ kind: "two-clips",
184
+ to: SAMPLE_TO,
185
+ generatedSummary:
186
+ "Two teammates walked you through the new onboarding flow.",
187
+ }),
188
+ });
189
+
190
+ defineTransactionalEmail({
191
+ id: CLIPS_ACTIVITY_COMMENT_EMAIL_ID,
192
+ name: "Clip comment",
193
+ trigger:
194
+ "Someone comments or replies on a Clip. Subject and copy differ slightly for a reply; both send under this id.",
195
+ recipientLabel: "Owner and thread authors",
196
+ recipient:
197
+ "The recording owner, plus every prior author in the thread when the comment is a reply. The list is re-checked against the recording's live ACL and filtered by each user's `emailNotifications` preference; the comment's own author never receives it.",
198
+ senderLabel: "Agent-Native Clips",
199
+ sender: CLIPS_SENDER,
200
+ preview: () =>
201
+ preview({
202
+ kind: "activity-comment",
203
+ to: SAMPLE_TO,
204
+ recordingId: SAMPLE_RECORDING_ID,
205
+ title: SAMPLE_TITLE,
206
+ authorEmail: "alex.chen@example.com",
207
+ authorName: "Alex Chen",
208
+ content: "The step at 1:20 is the part new hires always miss.",
209
+ videoTimestampMs: 80_000,
210
+ isReply: false,
211
+ }),
212
+ });
213
+
214
+ defineTransactionalEmail({
215
+ id: CLIPS_ACTIVITY_REACTION_EMAIL_ID,
216
+ name: "Clip reaction",
217
+ trigger: "A viewer reacts with an emoji on a Clip.",
218
+ recipientLabel: "Clip owner",
219
+ recipient:
220
+ "The recording owner plus any extra recipients the caller passes, re-checked against the recording's live ACL and filtered by each user's `emailNotifications` preference. The reacting viewer never receives it.",
221
+ senderLabel: "Agent-Native Clips",
222
+ sender: CLIPS_SENDER,
223
+ preview: () =>
224
+ preview({
225
+ kind: "activity-reaction",
226
+ to: SAMPLE_TO,
227
+ recordingId: SAMPLE_RECORDING_ID,
228
+ title: SAMPLE_TITLE,
229
+ emoji: "🎉",
230
+ authorEmail: "alex.chen@example.com",
231
+ authorName: "Alex Chen",
232
+ videoTimestampMs: 45_000,
233
+ }),
234
+ });
235
+
236
+ defineTransactionalEmail({
237
+ id: CLIPS_ORGANIZATION_INVITE_EMAIL_ID,
238
+ name: "Organization invitation",
239
+ trigger:
240
+ "An organization admin runs `invite-member`. Any earlier pending invite for the same address is cancelled first, so a re-invite sends a fresh email with a new token.",
241
+ recipientLabel: "Invited address",
242
+ recipient:
243
+ "The address passed to the action, exactly as typed. One email per invite.",
244
+ senderLabel: "Default sender",
245
+ sender:
246
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`, so unlike the other Clips emails it does not send as Agent-Native Clips.",
247
+ preview: () =>
248
+ renderClipsInviteEmail({
249
+ appName: "Clips",
250
+ orgName: "Northwind Design",
251
+ inviter: "dana.hill@example.com",
252
+ role: "member",
253
+ inviteUrl: "https://example.com/invite/sample-token",
254
+ }),
255
+ });
256
+ }