@agent-native/core 0.141.2 → 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 (157) 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-durable-a2a-workers-avoid-startup-migration-contention.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
  7. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
  8. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  9. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  10. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
  12. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
  13. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
  14. package/corpus/templates/analytics/server/lib/gong.ts +132 -33
  15. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  16. package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
  17. package/corpus/templates/analytics/server/plugins/db.ts +35 -10
  18. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  19. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
  20. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  21. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  22. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  23. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  24. package/corpus/templates/clips/actions/invite-member.ts +39 -16
  25. package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
  26. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  27. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
  28. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  29. package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
  31. package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
  32. package/corpus/templates/clips/desktop/src/styles.css +0 -3
  33. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  34. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  35. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  36. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
  37. package/corpus/templates/clips/shared/email-preview.ts +61 -0
  38. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  39. package/corpus/templates/content/server/lib/emails.ts +70 -0
  40. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  41. package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
  42. package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
  43. package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
  44. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  45. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  46. package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
  47. package/corpus/templates/factory/AGENTS.md +18 -17
  48. package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
  49. package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
  50. package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
  51. package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
  52. package/corpus/templates/factory/app/routes/factory.tsx +376 -206
  53. package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
  54. package/corpus/templates/factory/server/db/schema.ts +6 -0
  55. package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
  56. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
  57. package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
  58. package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
  59. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  60. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  61. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  62. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  63. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  64. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  65. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  66. package/corpus/templates/slides/actions/import-from-url.ts +1 -0
  67. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
  68. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
  69. package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
  70. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
  71. package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
  72. package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
  73. package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
  74. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
  75. package/corpus/templates/slides/app/pages/Index.tsx +127 -137
  76. package/corpus/templates/slides/app/root.tsx +8 -0
  77. package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
  79. package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
  80. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  81. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  82. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  83. package/dist/brand-kit/index.d.ts +1 -0
  84. package/dist/brand-kit/index.js +1 -0
  85. package/dist/brand-kit/tokens.d.ts +91 -0
  86. package/dist/brand-kit/tokens.js +279 -0
  87. package/dist/brand-kit/types.d.ts +25 -0
  88. package/dist/client/AssistantChat.js +7 -7
  89. package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
  90. package/dist/client/onboarding/first-run-registry.d.ts +25 -0
  91. package/dist/client/onboarding/first-run-registry.js +18 -0
  92. package/dist/client/onboarding/index.d.ts +2 -0
  93. package/dist/client/onboarding/index.js +1 -0
  94. package/dist/collab/awareness.d.ts +2 -2
  95. package/dist/collab/routes.d.ts +1 -1
  96. package/dist/collab/struct-routes.d.ts +1 -1
  97. package/dist/db/index.d.ts +1 -1
  98. package/dist/db/index.js +1 -1
  99. package/dist/db/migrations.d.ts +13 -3
  100. package/dist/db/migrations.js +19 -4
  101. package/dist/deploy/build.d.ts +8 -0
  102. package/dist/deploy/build.js +92 -3
  103. package/dist/deploy/workspace-deploy.js +86 -3
  104. package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
  105. package/dist/email-catalog/actions/list-email-activity.js +19 -0
  106. package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
  107. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  108. package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
  109. package/dist/email-catalog/actions/list-email-log.js +23 -0
  110. package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
  111. package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
  112. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
  113. package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
  114. package/dist/email-catalog/authorize.d.ts +1 -0
  115. package/dist/email-catalog/authorize.js +9 -0
  116. package/dist/email-catalog/log.d.ts +60 -0
  117. package/dist/email-catalog/log.js +142 -0
  118. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  119. package/dist/email-catalog/provider-metrics.js +182 -0
  120. package/dist/email-catalog/registry.d.ts +78 -0
  121. package/dist/email-catalog/registry.js +70 -0
  122. package/dist/email-catalog/schema.d.ts +231 -0
  123. package/dist/email-catalog/schema.js +53 -0
  124. package/dist/email-catalog/system-emails.d.ts +13 -0
  125. package/dist/email-catalog/system-emails.js +67 -0
  126. package/dist/jobs/index.d.ts +2 -0
  127. package/dist/jobs/index.js +2 -0
  128. package/dist/jobs/run-history.js +48 -1
  129. package/dist/jobs/scheduler-dispatch.d.ts +7 -0
  130. package/dist/jobs/scheduler-dispatch.js +7 -0
  131. package/dist/jobs/scheduler-health.d.ts +22 -0
  132. package/dist/jobs/scheduler-health.js +172 -0
  133. package/dist/jobs/scheduler.js +67 -2
  134. package/dist/localization/default-messages.d.ts +45 -0
  135. package/dist/localization/default-messages.js +45 -0
  136. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  137. package/dist/notifications/routes.d.ts +3 -3
  138. package/dist/observability/routes.d.ts +3 -3
  139. package/dist/org/handlers.js +9 -1
  140. package/dist/progress/routes.d.ts +1 -1
  141. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  142. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  143. package/dist/server/action-discovery.js +22 -0
  144. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
  145. package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
  146. package/dist/server/agent-chat-plugin.d.ts +2 -1
  147. package/dist/server/agent-chat-plugin.js +69 -22
  148. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  149. package/dist/server/better-auth-instance.js +17 -2
  150. package/dist/server/design-token-utils.d.ts +6 -1
  151. package/dist/server/design-token-utils.js +252 -59
  152. package/dist/server/email.d.ts +11 -0
  153. package/dist/server/email.js +54 -3
  154. package/dist/server/index.d.ts +3 -2
  155. package/dist/server/index.js +3 -2
  156. package/dist/vite/client.js +20 -14
  157. package/package.json +2 -1
@@ -6,6 +6,10 @@ import {
6
6
  } from "@agent-native/core/server";
7
7
 
8
8
  import type { CalendarEvent, DeleteEventScope } from "../../shared/api.js";
9
+ import {
10
+ CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID,
11
+ CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID,
12
+ } from "./emails.js";
9
13
 
10
14
  export interface GuestNotificationResult {
11
15
  requested: boolean;
@@ -96,6 +100,53 @@ function scopeLabel(scope: DeleteEventScope | undefined): string | undefined {
96
100
  return "this and following events";
97
101
  }
98
102
 
103
+ export function renderEventGuestNote({
104
+ title,
105
+ organizer,
106
+ message,
107
+ when,
108
+ kind,
109
+ appliesTo,
110
+ htmlLink,
111
+ }: {
112
+ title: string;
113
+ organizer: string;
114
+ message: string;
115
+ when: string;
116
+ kind: GuestNotificationKind;
117
+ appliesTo?: string;
118
+ htmlLink?: string | null;
119
+ }) {
120
+ const heading =
121
+ kind === "cancellation" ? "Event cancellation note" : "Event update note";
122
+ const subjectPrefix =
123
+ kind === "cancellation" ? "Cancellation note" : "Update note";
124
+ const eventAction = kind === "cancellation" ? "cancelling" : "updating";
125
+ const paragraphs = [
126
+ `${emailStrong(organizer)} added this note while ${eventAction} ${emailStrong(title)}.`,
127
+ messageParagraph(message),
128
+ `When: ${emailStrong(when)}.`,
129
+ ];
130
+ if (appliesTo) {
131
+ paragraphs.push(`Applies to: ${emailStrong(appliesTo)}.`);
132
+ }
133
+
134
+ return {
135
+ subject: `${subjectPrefix}: ${title}`,
136
+ ...renderEmail({
137
+ preheader: `${subjectPrefix}: ${title}`,
138
+ heading,
139
+ paragraphs,
140
+ cta:
141
+ kind === "update" && htmlLink
142
+ ? { label: "Open in Google Calendar", url: htmlLink }
143
+ : undefined,
144
+ footer:
145
+ "Google Calendar sends the calendar update separately. This message carries the organizer note.",
146
+ }),
147
+ };
148
+ }
149
+
99
150
  export async function sendEventGuestNotificationNote({
100
151
  event,
101
152
  organizerEmail,
@@ -138,33 +189,14 @@ export async function sendEventGuestNotificationNote({
138
189
  };
139
190
  }
140
191
 
141
- const title = stripCrlf(event.title) || "Calendar event";
142
- const organizer = stripCrlf(event.organizer?.displayName) || organizerEmail;
143
- const heading =
144
- kind === "cancellation" ? "Event cancellation note" : "Event update note";
145
- const subjectPrefix =
146
- kind === "cancellation" ? "Cancellation note" : "Update note";
147
- const eventAction = kind === "cancellation" ? "cancelling" : "updating";
148
- const paragraphs = [
149
- `${emailStrong(organizer)} added this note while ${eventAction} ${emailStrong(title)}.`,
150
- messageParagraph(normalizedMessage),
151
- `When: ${emailStrong(formatWhen(event))}.`,
152
- ];
153
- const appliesTo = scopeLabel(scope);
154
- if (appliesTo) {
155
- paragraphs.push(`Applies to: ${emailStrong(appliesTo)}.`);
156
- }
157
-
158
- const rendered = renderEmail({
159
- preheader: `${subjectPrefix}: ${title}`,
160
- heading,
161
- paragraphs,
162
- cta:
163
- kind === "update" && event.htmlLink
164
- ? { label: "Open in Google Calendar", url: event.htmlLink }
165
- : undefined,
166
- footer:
167
- "Google Calendar sends the calendar update separately. This message carries the organizer note.",
192
+ const rendered = renderEventGuestNote({
193
+ title: stripCrlf(event.title) || "Calendar event",
194
+ organizer: stripCrlf(event.organizer?.displayName) || organizerEmail,
195
+ message: normalizedMessage,
196
+ when: formatWhen(event),
197
+ kind,
198
+ appliesTo: scopeLabel(scope),
199
+ htmlLink: event.htmlLink,
168
200
  });
169
201
 
170
202
  let sentCount = 0;
@@ -174,10 +206,14 @@ export async function sendEventGuestNotificationNote({
174
206
  try {
175
207
  await sendEmail({
176
208
  to,
177
- subject: `${subjectPrefix}: ${title}`,
209
+ subject: rendered.subject,
178
210
  html: rendered.html,
179
211
  text: rendered.text,
180
212
  replyTo: organizerEmail,
213
+ templateId:
214
+ kind === "cancellation"
215
+ ? CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID
216
+ : CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID,
181
217
  });
182
218
  sentCount += 1;
183
219
  } catch (error) {
@@ -0,0 +1,5 @@
1
+ import { registerCalendarEmails } from "../lib/emails.js";
2
+
3
+ export default function registerTransactionalEmails(): void {
4
+ registerCalendarEmails();
5
+ }
@@ -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;