@agent-native/core 0.140.0 → 0.141.0

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 (128) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  3. package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-stay-complete-and-atomic.md +6 -0
  4. package/corpus/templates/analytics/server/lib/dashboard-report.ts +0 -2
  5. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +11 -6
  6. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +8 -18
  7. package/corpus/templates/analytics/server/plugins/db.ts +174 -0
  8. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +38 -38
  9. package/corpus/templates/calendar/changelog/2026-08-05-out-of-office-blocks-now-open-from-their-scheduled-time-rang.md +6 -0
  10. package/corpus/templates/calendar/server/lib/booking-emails.ts +70 -154
  11. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +28 -64
  12. package/corpus/templates/clips/actions/get-recording-player-data.ts +1 -0
  13. package/corpus/templates/clips/actions/invite-member.ts +16 -39
  14. package/corpus/templates/clips/app/components/player/comments-panel.tsx +2 -3
  15. package/corpus/templates/clips/app/components/player/reaction-emojis.ts +10 -0
  16. package/corpus/templates/clips/app/components/player/reactions-tray.tsx +2 -2
  17. package/corpus/templates/clips/app/components/player/video-player.tsx +20 -6
  18. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +33 -0
  19. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +5 -1
  20. package/corpus/templates/clips/app/routes/share.$shareId.tsx +27 -1
  21. package/corpus/templates/clips/changelog/2026-08-05-comment-and-video-reaction-pickers-now-include-eyes-and-mind.md +6 -0
  22. package/corpus/templates/clips/changelog/2026-08-05-fresh-recordings-now-open-with-the-complete-video.md +6 -0
  23. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +0 -24
  24. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  25. package/corpus/templates/content/server/lib/comment-notifications.ts +26 -57
  26. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +1 -44
  27. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1 -265
  28. package/corpus/templates/design/actions/create-design-system.ts +10 -9
  29. package/corpus/templates/design/actions/get-design-system.ts +0 -53
  30. package/corpus/templates/design/actions/import-design-tokens.ts +71 -5
  31. package/corpus/templates/design/actions/index-design-tokens.ts +80 -61
  32. package/corpus/templates/design/actions/update-design-system.ts +10 -7
  33. package/corpus/templates/design/app/components/design/EditPanel.tsx +2 -45
  34. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +0 -12
  35. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +6 -10
  36. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +3 -19
  37. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +10 -36
  38. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +0 -15
  39. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +1 -19
  40. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +0 -6
  41. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +23 -53
  42. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +4 -142
  43. package/corpus/templates/design/app/components/design/types.ts +0 -6
  44. package/corpus/templates/design/app/i18n-data.ts +0 -20
  45. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +1 -1
  46. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -78
  47. package/corpus/templates/design/app/pages/design-editor/html-layer-positioning.ts +1 -6
  48. package/corpus/templates/design/e2e/helpers.ts +0 -17
  49. package/corpus/templates/design/shared/resolve-tweaks.ts +18 -10
  50. package/corpus/templates/forms/server/lib/response-email.ts +0 -2
  51. package/corpus/templates/plan/actions/request-plan-access.ts +12 -33
  52. package/corpus/templates/plan/server/lib/comment-notifications.ts +27 -53
  53. package/corpus/templates/slides/server/lib/comment-notifications.ts +21 -44
  54. package/dist/a2a/handlers.js +9 -10
  55. package/dist/brand-kit/index.d.ts +0 -1
  56. package/dist/brand-kit/index.js +0 -1
  57. package/dist/brand-kit/types.d.ts +0 -25
  58. package/dist/collab/awareness.d.ts +2 -2
  59. package/dist/collab/struct-routes.d.ts +1 -1
  60. package/dist/db/client.js +8 -8
  61. package/dist/db/migrations.js +39 -19
  62. package/dist/localization/default-messages.d.ts +0 -45
  63. package/dist/localization/default-messages.js +0 -45
  64. package/dist/notifications/routes.d.ts +6 -6
  65. package/dist/observability/routes.d.ts +3 -3
  66. package/dist/org/handlers.js +1 -8
  67. package/dist/progress/routes.d.ts +1 -1
  68. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  69. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  70. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  71. package/dist/resources/handlers.d.ts +1 -1
  72. package/dist/secrets/routes.d.ts +9 -9
  73. package/dist/server/action-discovery.js +0 -22
  74. package/dist/server/auth.js +3 -1
  75. package/dist/server/better-auth-instance.js +2 -17
  76. package/dist/server/email.d.ts +0 -9
  77. package/dist/server/email.js +3 -44
  78. package/dist/server/index.d.ts +0 -1
  79. package/dist/server/index.js +0 -1
  80. package/dist/server/transcribe-voice.d.ts +1 -1
  81. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +3 -2
  82. package/docs/content/authentication.mdx +25 -19
  83. package/package.json +1 -1
  84. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +3 -2
  85. package/corpus/templates/analytics/server/lib/emails.ts +0 -51
  86. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +0 -5
  87. package/corpus/templates/calendar/server/lib/emails.ts +0 -167
  88. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +0 -5
  89. package/corpus/templates/clips/server/lib/emails.ts +0 -256
  90. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +0 -5
  91. package/corpus/templates/content/server/lib/emails.ts +0 -70
  92. package/corpus/templates/content/server/plugins/transactional-emails.ts +0 -5
  93. package/corpus/templates/design/app/components/design/edit-panel/design-system-swatches.ts +0 -215
  94. package/corpus/templates/design/changelog/2026-08-04-design-system-tokens-show-by-name-in-the-editor.md +0 -6
  95. package/corpus/templates/design/server/lib/design-system-tokens.ts +0 -35
  96. package/corpus/templates/design/shared/authored-color-styles.ts +0 -401
  97. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +0 -4
  98. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +0 -4
  99. package/corpus/templates/forms/server/lib/emails.ts +0 -52
  100. package/corpus/templates/forms/server/plugins/transactional-emails.ts +0 -5
  101. package/corpus/templates/plan/server/lib/emails.ts +0 -67
  102. package/corpus/templates/plan/server/plugins/transactional-emails.ts +0 -5
  103. package/corpus/templates/slides/server/lib/emails.ts +0 -40
  104. package/corpus/templates/slides/server/plugins/transactional-emails.ts +0 -5
  105. package/dist/brand-kit/tokens.d.ts +0 -92
  106. package/dist/brand-kit/tokens.js +0 -280
  107. package/dist/email-catalog/actions/list-email-activity.d.ts +0 -6
  108. package/dist/email-catalog/actions/list-email-activity.js +0 -15
  109. package/dist/email-catalog/actions/list-email-engagement.d.ts +0 -6
  110. package/dist/email-catalog/actions/list-email-engagement.js +0 -15
  111. package/dist/email-catalog/actions/list-email-log.d.ts +0 -8
  112. package/dist/email-catalog/actions/list-email-log.js +0 -22
  113. package/dist/email-catalog/actions/list-transactional-emails.d.ts +0 -23
  114. package/dist/email-catalog/actions/list-transactional-emails.js +0 -61
  115. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +0 -10
  116. package/dist/email-catalog/actions/render-transactional-email-preview.js +0 -24
  117. package/dist/email-catalog/authorize.d.ts +0 -2
  118. package/dist/email-catalog/authorize.js +0 -10
  119. package/dist/email-catalog/log.d.ts +0 -57
  120. package/dist/email-catalog/log.js +0 -117
  121. package/dist/email-catalog/provider-metrics.d.ts +0 -34
  122. package/dist/email-catalog/provider-metrics.js +0 -165
  123. package/dist/email-catalog/registry.d.ts +0 -79
  124. package/dist/email-catalog/registry.js +0 -71
  125. package/dist/email-catalog/schema.d.ts +0 -212
  126. package/dist/email-catalog/schema.js +0 -49
  127. package/dist/email-catalog/system-emails.d.ts +0 -14
  128. package/dist/email-catalog/system-emails.js +0 -68
@@ -11,12 +11,6 @@ import {
11
11
  DEFAULT_BOOKING_TIMEZONE,
12
12
  safeBookingTimeZone,
13
13
  } from "./booking-timezone.js";
14
- import {
15
- CALENDAR_BOOKING_CANCELLED_EMAIL_ID,
16
- CALENDAR_BOOKING_CANCELLED_HOST_EMAIL_ID,
17
- CALENDAR_BOOKING_CONFIRMED_EMAIL_ID,
18
- CALENDAR_BOOKING_RECEIVED_EMAIL_ID,
19
- } from "./emails.js";
20
14
 
21
15
  function stripCrlf(value: string | undefined): string {
22
16
  return (value ?? "").replace(/[\r\n]+/g, " ").trim();
@@ -63,128 +57,6 @@ async function sendBestEffort(
63
57
  }
64
58
  }
65
59
 
66
- export function renderBookingConfirmedEmail({
67
- title,
68
- when,
69
- host,
70
- manageUrl,
71
- meetingLink,
72
- }: {
73
- title: string;
74
- when: string;
75
- host: string;
76
- manageUrl: string;
77
- meetingLink?: string | null;
78
- }) {
79
- const paragraphs = [
80
- `You're booked for ${emailStrong(title)} with ${emailStrong(host)}.`,
81
- `Time: ${emailStrong(when)}.`,
82
- ];
83
- if (meetingLink) {
84
- paragraphs.push(`Meeting link: ${emailLink("Join meeting", meetingLink)}.`);
85
- }
86
-
87
- return {
88
- subject: `Confirmed: ${title}`,
89
- ...renderEmail({
90
- preheader: `You're booked for ${title} on ${when}.`,
91
- heading: "Your meeting is booked",
92
- paragraphs,
93
- cta: { label: "Manage booking", url: manageUrl },
94
- footer:
95
- "Use the manage link if you need to cancel or reschedule this meeting.",
96
- }),
97
- };
98
- }
99
-
100
- export function renderBookingReceivedEmail({
101
- title,
102
- when,
103
- attendeeName,
104
- attendee,
105
- manageUrl,
106
- meetingLink,
107
- }: {
108
- title: string;
109
- when: string;
110
- attendeeName: string;
111
- attendee: string;
112
- manageUrl: string;
113
- meetingLink?: string | null;
114
- }) {
115
- return {
116
- subject: `New booking: ${title}`,
117
- ...renderEmail({
118
- preheader: `${attendeeName} booked ${title} on ${when}.`,
119
- heading: "New booking",
120
- paragraphs: [
121
- `${emailStrong(attendeeName)} booked ${emailStrong(title)}.`,
122
- `Time: ${emailStrong(when)}.`,
123
- `Guest: ${emailStrong(attendee)}.`,
124
- ...(meetingLink
125
- ? [`Meeting link: ${emailLink("Join meeting", meetingLink)}.`]
126
- : []),
127
- ],
128
- cta: { label: "View booking", url: manageUrl },
129
- footer: "This booking was created from your calendar booking link.",
130
- }),
131
- };
132
- }
133
-
134
- export function renderBookingCancelledEmail({
135
- title,
136
- when,
137
- host,
138
- bookAgainUrl,
139
- }: {
140
- title: string;
141
- when: string;
142
- host: string;
143
- bookAgainUrl?: string;
144
- }) {
145
- return {
146
- subject: `Cancelled: ${title}`,
147
- ...renderEmail({
148
- preheader: `${title} on ${when} was cancelled.`,
149
- heading: "Your meeting was cancelled",
150
- paragraphs: [
151
- `${emailStrong(title)} with ${emailStrong(host || "the host")} has been cancelled.`,
152
- `Original time: ${emailStrong(when)}.`,
153
- ],
154
- cta: bookAgainUrl
155
- ? { label: "Book another time", url: bookAgainUrl }
156
- : undefined,
157
- footer: "If this was unexpected, contact the meeting host.",
158
- }),
159
- };
160
- }
161
-
162
- export function renderBookingCancelledHostEmail({
163
- title,
164
- when,
165
- attendeeName,
166
- attendee,
167
- }: {
168
- title: string;
169
- when: string;
170
- attendeeName: string;
171
- attendee: string;
172
- }) {
173
- return {
174
- subject: `Cancelled booking: ${title}`,
175
- ...renderEmail({
176
- preheader: `${attendeeName}'s booking for ${title} was cancelled.`,
177
- heading: "Booking cancelled",
178
- paragraphs: [
179
- `${emailStrong(attendeeName)}'s booking for ${emailStrong(title)} was cancelled.`,
180
- `Original time: ${emailStrong(when)}.`,
181
- `Guest: ${emailStrong(attendee)}.`,
182
- ],
183
- footer: "No further action is needed.",
184
- }),
185
- };
186
- }
187
-
188
60
  export async function sendBookingConfirmationEmails({
189
61
  booking,
190
62
  hostEmail,
@@ -202,31 +74,54 @@ export async function sendBookingConfirmationEmails({
202
74
  const attendee = stripCrlf(booking.email);
203
75
  const attendeeName = stripCrlf(booking.name) || "there";
204
76
 
77
+ const attendeeParagraphs = [
78
+ `You're booked for ${emailStrong(title)} with ${emailStrong(host)}.`,
79
+ `Time: ${emailStrong(when)}.`,
80
+ ];
81
+ if (booking.meetingLink) {
82
+ attendeeParagraphs.push(
83
+ `Meeting link: ${emailLink("Join meeting", booking.meetingLink)}.`,
84
+ );
85
+ }
86
+
87
+ const attendeeEmail = renderEmail({
88
+ preheader: `You're booked for ${title} on ${when}.`,
89
+ heading: "Your meeting is booked",
90
+ paragraphs: attendeeParagraphs,
91
+ cta: { label: "Manage booking", url: manageUrl },
92
+ footer:
93
+ "Use the manage link if you need to cancel or reschedule this meeting.",
94
+ });
95
+
205
96
  await sendBestEffort("attendee confirmation", {
206
97
  to: attendee,
207
- ...renderBookingConfirmedEmail({
208
- title,
209
- when,
210
- host,
211
- manageUrl,
212
- meetingLink: booking.meetingLink,
213
- }),
98
+ subject: `Confirmed: ${title}`,
99
+ html: attendeeEmail.html,
100
+ text: attendeeEmail.text,
214
101
  replyTo: host,
215
- templateId: CALENDAR_BOOKING_CONFIRMED_EMAIL_ID,
102
+ });
103
+
104
+ const hostEmailMessage = renderEmail({
105
+ preheader: `${attendeeName} booked ${title} on ${when}.`,
106
+ heading: "New booking",
107
+ paragraphs: [
108
+ `${emailStrong(attendeeName)} booked ${emailStrong(title)}.`,
109
+ `Time: ${emailStrong(when)}.`,
110
+ `Guest: ${emailStrong(attendee)}.`,
111
+ ...(booking.meetingLink
112
+ ? [`Meeting link: ${emailLink("Join meeting", booking.meetingLink)}.`]
113
+ : []),
114
+ ],
115
+ cta: { label: "View booking", url: manageUrl },
116
+ footer: "This booking was created from your calendar booking link.",
216
117
  });
217
118
 
218
119
  await sendBestEffort("host notification", {
219
120
  to: host,
220
- ...renderBookingReceivedEmail({
221
- title,
222
- when,
223
- attendeeName,
224
- attendee,
225
- manageUrl,
226
- meetingLink: booking.meetingLink,
227
- }),
121
+ subject: `New booking: ${title}`,
122
+ html: hostEmailMessage.html,
123
+ text: hostEmailMessage.text,
228
124
  replyTo: attendee,
229
- templateId: CALENDAR_BOOKING_RECEIVED_EMAIL_ID,
230
125
  });
231
126
  }
232
127
 
@@ -247,24 +142,45 @@ export async function sendBookingCancellationEmails({
247
142
  const attendee = stripCrlf(booking.email);
248
143
  const attendeeName = stripCrlf(booking.name) || "The guest";
249
144
 
145
+ const attendeeEmail = renderEmail({
146
+ preheader: `${title} on ${when} was cancelled.`,
147
+ heading: "Your meeting was cancelled",
148
+ paragraphs: [
149
+ `${emailStrong(title)} with ${emailStrong(host || "the host")} has been cancelled.`,
150
+ `Original time: ${emailStrong(when)}.`,
151
+ ],
152
+ cta: bookAgainUrl
153
+ ? { label: "Book another time", url: bookAgainUrl }
154
+ : undefined,
155
+ footer: "If this was unexpected, contact the meeting host.",
156
+ });
157
+
250
158
  await sendBestEffort("attendee cancellation", {
251
159
  to: attendee,
252
- ...renderBookingCancelledEmail({ title, when, host, bookAgainUrl }),
160
+ subject: `Cancelled: ${title}`,
161
+ html: attendeeEmail.html,
162
+ text: attendeeEmail.text,
253
163
  replyTo: host || undefined,
254
- templateId: CALENDAR_BOOKING_CANCELLED_EMAIL_ID,
255
164
  });
256
165
 
257
166
  if (!host) return;
258
167
 
168
+ const hostEmailMessage = renderEmail({
169
+ preheader: `${attendeeName}'s booking for ${title} was cancelled.`,
170
+ heading: "Booking cancelled",
171
+ paragraphs: [
172
+ `${emailStrong(attendeeName)}'s booking for ${emailStrong(title)} was cancelled.`,
173
+ `Original time: ${emailStrong(when)}.`,
174
+ `Guest: ${emailStrong(attendee)}.`,
175
+ ],
176
+ footer: "No further action is needed.",
177
+ });
178
+
259
179
  await sendBestEffort("host cancellation notification", {
260
180
  to: host,
261
- ...renderBookingCancelledHostEmail({
262
- title,
263
- when,
264
- attendeeName,
265
- attendee,
266
- }),
181
+ subject: `Cancelled booking: ${title}`,
182
+ html: hostEmailMessage.html,
183
+ text: hostEmailMessage.text,
267
184
  replyTo: attendee,
268
- templateId: CALENDAR_BOOKING_CANCELLED_HOST_EMAIL_ID,
269
185
  });
270
186
  }
@@ -6,10 +6,6 @@ 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";
13
9
 
14
10
  export interface GuestNotificationResult {
15
11
  requested: boolean;
@@ -100,53 +96,6 @@ function scopeLabel(scope: DeleteEventScope | undefined): string | undefined {
100
96
  return "this and following events";
101
97
  }
102
98
 
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
-
150
99
  export async function sendEventGuestNotificationNote({
151
100
  event,
152
101
  organizerEmail,
@@ -189,14 +138,33 @@ export async function sendEventGuestNotificationNote({
189
138
  };
190
139
  }
191
140
 
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,
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.",
200
168
  });
201
169
 
202
170
  let sentCount = 0;
@@ -206,14 +174,10 @@ export async function sendEventGuestNotificationNote({
206
174
  try {
207
175
  await sendEmail({
208
176
  to,
209
- subject: rendered.subject,
177
+ subject: `${subjectPrefix}: ${title}`,
210
178
  html: rendered.html,
211
179
  text: rendered.text,
212
180
  replyTo: organizerEmail,
213
- templateId:
214
- kind === "cancellation"
215
- ? CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID
216
- : CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID,
217
181
  });
218
182
  sentCount += 1;
219
183
  } catch (error) {
@@ -326,6 +326,7 @@ export default defineAction({
326
326
  editsJson: rec.editsJson,
327
327
  videoUrl: resolvedVideoUrl,
328
328
  videoFormat: rec.videoFormat,
329
+ videoSizeBytes: rec.videoSizeBytes ?? null,
329
330
  width: rec.width,
330
331
  height: rec.height,
331
332
  hasAudio: Boolean(rec.hasAudio),
@@ -24,7 +24,6 @@ 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";
28
27
  import {
29
28
  getCurrentOwnerEmail,
30
29
  nanoid,
@@ -68,36 +67,6 @@ async function fetchOrgName(orgId: string): Promise<string> {
68
67
  return row?.name ?? "Organization";
69
68
  }
70
69
 
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; mail clients don't support custom properties.
96
- brandColor: "#18181B",
97
- }),
98
- };
99
- }
100
-
101
70
  export default defineAction({
102
71
  description:
103
72
  "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.",
@@ -154,17 +123,25 @@ export default defineAction({
154
123
  const orgName = await fetchOrgName(organizationId);
155
124
  const inviteUrl = `${baseUrl()}/invite/${token}`;
156
125
 
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
+ });
157
139
  try {
158
140
  await sendEmail({
159
- ...renderClipsInviteEmail({
160
- appName: getAppName(),
161
- orgName,
162
- inviter,
163
- role,
164
- inviteUrl,
165
- }),
166
141
  to: args.email,
167
- templateId: CLIPS_ORGANIZATION_INVITE_EMAIL_ID,
142
+ subject: `You're invited to ${orgName} on ${appName}`,
143
+ html,
144
+ text,
168
145
  });
169
146
  } catch (err) {
170
147
  console.warn("[invite-member] email send failed:", err);
@@ -31,6 +31,7 @@ import {
31
31
  import { Textarea } from "@/components/ui/textarea";
32
32
  import { cn } from "@/lib/utils";
33
33
 
34
+ import { REACTION_EMOJIS } from "./reaction-emojis";
34
35
  import { msToClock } from "./scrubber";
35
36
 
36
37
  function makeTempId() {
@@ -60,8 +61,6 @@ const defaultLens: CommentsLens = {
60
61
  data ? { ...(data as object), comments: next } : data,
61
62
  };
62
63
 
63
- const COMMENT_EMOJIS = ["👍", "❤️", "🔥", "👏", "🎉", "😂"];
64
-
65
64
  export interface Comment {
66
65
  id: string;
67
66
  threadId: string;
@@ -928,7 +927,7 @@ function CommentCard({
928
927
  </PopoverTrigger>
929
928
  <PopoverContent side="top" align="start" className="p-1 w-auto">
930
929
  <div className="flex gap-0.5">
931
- {COMMENT_EMOJIS.map((e) => (
930
+ {REACTION_EMOJIS.map((e) => (
932
931
  <button
933
932
  key={e}
934
933
  onClick={() => {
@@ -0,0 +1,10 @@
1
+ export const REACTION_EMOJIS = [
2
+ "👍",
3
+ "❤️",
4
+ "🔥",
5
+ "👏",
6
+ "🎉",
7
+ "😂",
8
+ "🤯",
9
+ "👀",
10
+ ] as const;
@@ -7,7 +7,7 @@ import {
7
7
  } from "@/components/ui/tooltip";
8
8
  import { cn } from "@/lib/utils";
9
9
 
10
- const EMOJIS = ["👍", "❤️", "🔥", "👏", "🎉", "😂", "🤯"] as const;
10
+ import { REACTION_EMOJIS } from "./reaction-emojis";
11
11
 
12
12
  export interface ReactionsTrayProps {
13
13
  onReact: (emoji: string) => void;
@@ -38,7 +38,7 @@ export function ReactionsTray({ onReact, disabled }: ReactionsTrayProps) {
38
38
 
39
39
  return (
40
40
  <div className="relative flex w-fit max-w-full items-center gap-0.5 rounded-full border border-border bg-card px-1.5 py-1 shadow-sm sm:gap-1 sm:px-2">
41
- {EMOJIS.map((emoji) => (
41
+ {REACTION_EMOJIS.map((emoji) => (
42
42
  <Tooltip key={emoji}>
43
43
  <TooltipTrigger asChild>
44
44
  <button
@@ -156,6 +156,8 @@ export interface VideoPlayerHandle {
156
156
  export interface VideoPlayerProps {
157
157
  recordingId: string;
158
158
  videoUrl: string | null | undefined;
159
+ /** Version of the stored media bytes, used when a stable URL is replaced. */
160
+ mediaVersion?: string | number | null;
159
161
  /**
160
162
  * Container format of `videoUrl`, when known. Used only to pick an accurate
161
163
  * `canPlayType` MIME check (e.g. Safari cannot play `video/webm`) — Clips
@@ -228,6 +230,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
228
230
  const t = useT();
229
231
  const {
230
232
  videoUrl,
233
+ mediaVersion,
231
234
  videoFormat,
232
235
  embedProvider,
233
236
  durationMs,
@@ -261,10 +264,21 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
261
264
  onCommentClick,
262
265
  } = props;
263
266
 
264
- const resolvedVideoSrc = useMemo(
265
- () => resolveLocalUrl(videoUrl),
266
- [videoUrl],
267
- );
267
+ const resolvedVideoSrc = useMemo(() => {
268
+ const localUrl = resolveLocalUrl(videoUrl);
269
+ if (
270
+ !localUrl ||
271
+ mediaVersion == null ||
272
+ embedProvider === "loom" ||
273
+ isLoomEmbedUrl(localUrl)
274
+ ) {
275
+ return localUrl;
276
+ }
277
+ // Some storage providers keep the public URL stable while replacing the
278
+ // object behind it. Keep the player source identity aligned with the
279
+ // recording row so a repaired asset cannot stay cached in the player.
280
+ return setUrlSearchParam(localUrl, "media", String(mediaVersion));
281
+ }, [embedProvider, mediaVersion, videoUrl]);
268
282
  const videoRef = useRef<HTMLVideoElement | null>(null);
269
283
  const [playbackVideoEl, setPlaybackVideoEl] =
270
284
  useState<HTMLVideoElement | null>(null);
@@ -569,11 +583,11 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
569
583
  return;
570
584
  }
571
585
 
572
- if (recoveringFromErrorRef.current) return;
573
-
574
586
  const v = videoRef.current;
575
587
  const sameResource =
576
588
  activeVideoSourceIdentity === incomingVideoSourceIdentity;
589
+ if (recoveringFromErrorRef.current && sameResource) return;
590
+
577
591
  const playbackActive =
578
592
  playAttemptPendingRef.current ||
579
593
  isPlayPending ||
@@ -20,6 +20,8 @@ export function meta() {
20
20
  }
21
21
 
22
22
  const STORAGE_KEY_PREFIX = "clips-share-pw-";
23
+ const READY_MEDIA_SETTLE_POLL_MS = 20 * 1000;
24
+ const READY_MEDIA_SETTLE_POLL_INTERVAL_MS = 1000;
23
25
 
24
26
  /**
25
27
  * Parse `t` URL param into ms (supports plain seconds or `1m20s` / `1h2m3s`).
@@ -78,6 +80,7 @@ export default function EmbedRoute() {
78
80
  }
79
81
  });
80
82
  const [pwError, setPwError] = useState<string | null>(null);
83
+ const readyMediaPollRef = useRef<{ key: string; until: number } | null>(null);
81
84
 
82
85
  useEffect(() => {
83
86
  if (typeof document === "undefined") return;
@@ -121,6 +124,32 @@ export default function EmbedRoute() {
121
124
  return { ok: res.ok, status: res.status, data };
122
125
  },
123
126
  enabled: !!shareId,
127
+ refetchInterval: (q) => {
128
+ const payload = (q.state.data as { data?: any } | undefined)?.data;
129
+ const rec = payload?.recording;
130
+ if (!rec) return false;
131
+ if (rec.status !== "ready" || !rec.videoUrl) {
132
+ readyMediaPollRef.current = null;
133
+ return 2000;
134
+ }
135
+ const mediaKey = [
136
+ rec.id,
137
+ rec.durationMs ?? "",
138
+ rec.videoSizeBytes ?? "",
139
+ rec.videoFormat ?? "",
140
+ ].join(":");
141
+ const now = Date.now();
142
+ if (readyMediaPollRef.current?.key !== mediaKey) {
143
+ readyMediaPollRef.current = {
144
+ key: mediaKey,
145
+ until: now + READY_MEDIA_SETTLE_POLL_MS,
146
+ };
147
+ }
148
+ return now < readyMediaPollRef.current.until
149
+ ? READY_MEDIA_SETTLE_POLL_INTERVAL_MS
150
+ : false;
151
+ },
152
+ refetchIntervalInBackground: false,
124
153
  });
125
154
 
126
155
  const recording = dataQ.data?.data?.recording;
@@ -196,6 +225,10 @@ export default function EmbedRoute() {
196
225
  onVideoElementChange={setTrackedVideoEl}
197
226
  recordingId={recording.id}
198
227
  videoUrl={recording.videoUrl}
228
+ mediaVersion={[
229
+ recording.videoSizeBytes ?? "",
230
+ recording.updatedAt ?? "",
231
+ ].join(":")}
199
232
  videoFormat={recording.videoFormat}
200
233
  embedProvider={isLoomEmbedBacked ? "loom" : null}
201
234
  durationMs={recording.durationMs}
@@ -348,9 +348,9 @@ export default function RecordingPage() {
348
348
  // without requiring a manual refresh.
349
349
  const mediaKey = [
350
350
  rec.id,
351
- rec.videoUrl,
352
351
  rec.durationMs ?? "",
353
352
  rec.videoSizeBytes ?? "",
353
+ rec.videoFormat ?? "",
354
354
  ].join(":");
355
355
  const now = Date.now();
356
356
  if (readyMediaPollRef.current?.key !== mediaKey) {
@@ -1601,6 +1601,10 @@ export default function RecordingPage() {
1601
1601
  onVideoElementChange={setTrackedVideoEl}
1602
1602
  recordingId={recording.id}
1603
1603
  videoUrl={recording.videoUrl}
1604
+ mediaVersion={[
1605
+ recording.videoSizeBytes ?? "",
1606
+ recording.updatedAt ?? "",
1607
+ ].join(":")}
1604
1608
  videoFormat={recording.videoFormat}
1605
1609
  embedProvider={isLoomEmbedBacked ? "loom" : null}
1606
1610
  durationMs={recording.durationMs}