@agent-native/core 0.176.1 → 0.177.0-nightly-20260828155142

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 (71) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/set-workspace-feature-flag.ts +2 -2
  3. package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +16 -1
  4. package/corpus/templates/analytics/changelog/2026-08-27-show-analytics-s-own-registered-flags-in-fleet-feature-flag-.md +6 -0
  5. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +204 -32
  6. package/corpus/templates/assets/changelog/2026-08-26-templates-across-brand-kits.md +1 -0
  7. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +61 -68
  8. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +9 -3
  9. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +3 -3
  10. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +8 -3
  11. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +12 -3
  12. package/corpus/templates/calendar/changelog/2026-08-28-all-day-events-stay-in-the-compact-top-bar-in-day-and-week-v.md +6 -0
  13. package/corpus/templates/calendar/changelog/2026-08-28-event-creators-can-choose-whether-a-meeting-shows-as-free-or.md +6 -0
  14. package/corpus/templates/clips/app/global.css +0 -27
  15. package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
  16. package/corpus/templates/clips/app/routes/download.tsx +131 -16
  17. package/corpus/templates/clips/app/routes/share.$shareId.tsx +35 -20
  18. package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +77 -22
  19. package/corpus/templates/forms/actions/response-insights.ts +4 -4
  20. package/corpus/templates/forms/actions/retry-response-deliveries.ts +26 -0
  21. package/corpus/templates/forms/server/db/schema.ts +64 -17
  22. package/corpus/templates/forms/server/handlers/submissions.ts +1022 -94
  23. package/corpus/templates/forms/server/lib/integrations.ts +100 -33
  24. package/corpus/templates/forms/server/middleware/00-public-cors.ts +1 -1
  25. package/corpus/templates/forms/server/plugins/db.ts +40 -0
  26. package/corpus/templates/plan/agent-native.json +1 -4
  27. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +6 -1
  28. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +2 -2
  29. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -6
  30. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +78 -12
  31. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +4 -4
  32. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +111 -27
  33. package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +20 -34
  34. package/corpus/templates/slides/app/components/editor/slide-style.ts +2 -0
  35. package/corpus/templates/slides/app/global.css +35 -0
  36. package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +133 -5
  37. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +675 -0
  38. package/corpus/templates/slides/app/lib/validate-native-slide-html.ts +1 -0
  39. package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -44
  40. package/corpus/templates/slides/app/root.tsx +1 -22
  41. package/corpus/templates/slides/changelog/2026-08-27-image-fit-controls-apply-cover-crops-and-position.md +6 -0
  42. package/corpus/templates/slides/changelog/2026-08-28-shared-slides-with-a-style-block-render-again.md +6 -0
  43. package/dist/action.d.ts +3 -1
  44. package/dist/client/FeedbackButton.d.ts +12 -0
  45. package/dist/client/FeedbackButton.js +62 -28
  46. package/dist/client/app-providers.js +13 -1
  47. package/dist/client/feedback-context.d.ts +1 -0
  48. package/dist/client/feedback-context.js +3 -2
  49. package/dist/client/host-bridge.d.ts +1 -0
  50. package/dist/client/observability/ThumbsFeedback.js +73 -10
  51. package/dist/client/webmcp.d.ts +4 -0
  52. package/dist/client/webmcp.js +51 -4
  53. package/dist/collab/routes.d.ts +1 -1
  54. package/dist/collab/struct-routes.d.ts +1 -1
  55. package/dist/index.browser.d.ts +1 -1
  56. package/dist/index.browser.js +1 -1
  57. package/dist/observability/routes.d.ts +5 -5
  58. package/dist/observability/routes.js +8 -2
  59. package/dist/observability/store.d.ts +1 -1
  60. package/dist/observability/store.js +18 -3
  61. package/dist/observability/types.d.ts +1 -0
  62. package/dist/progress/routes.d.ts +1 -1
  63. package/dist/resources/handlers.d.ts +2 -2
  64. package/dist/secrets/routes.d.ts +9 -9
  65. package/dist/server/action-routes.d.ts +1 -6
  66. package/dist/server/action-routes.js +58 -17
  67. package/dist/server/agent-chat-plugin.js +8 -1
  68. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  69. package/dist/server/transcribe-voice.d.ts +1 -1
  70. package/docs/content/webmcp.mdx +31 -12
  71. package/package.json +1 -1
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-28
4
+ ---
5
+
6
+ Event creators can choose whether a meeting shows as Free or Busy.
@@ -155,33 +155,6 @@
155
155
  animation: clips-collapsible-up 120ms ease-out;
156
156
  }
157
157
 
158
- .clips-share-content {
159
- container: clips-share-content / inline-size;
160
- }
161
-
162
- .clips-share-metadata-description {
163
- order: 2;
164
- }
165
-
166
- .clips-share-metadata-actions {
167
- order: 1;
168
- }
169
-
170
- @container clips-share-content (min-width: 860px) {
171
- .clips-share-metadata {
172
- align-items: flex-start;
173
- flex-direction: row;
174
- }
175
-
176
- .clips-share-metadata-description {
177
- order: 1;
178
- }
179
-
180
- .clips-share-metadata-actions {
181
- order: 2;
182
- }
183
- }
184
-
185
158
  @media (prefers-reduced-motion: reduce) {
186
159
  .clips-collapsible-content[data-state] {
187
160
  animation-duration: 1ms;
@@ -631,6 +631,8 @@ const messages = {
631
631
  macSublabel: "Universal (Apple Silicon + Intel)",
632
632
  windowsSublabel: "64-bit MSI installer",
633
633
  downloadFor: "Download for {{platform}}",
634
+ downloadStarted: "Download started",
635
+ downloadAgain: "Didn't work? Try downloading again",
634
636
  alsoFor: "Also available for {{platform}}",
635
637
  backToLibrary: "Back to library",
636
638
  clipsDesktop: "Clips Desktop",
@@ -4,6 +4,7 @@ import {
4
4
  IconBrandChrome,
5
5
  IconBrandApple,
6
6
  IconBrandWindows,
7
+ IconCheck,
7
8
  IconExternalLink,
8
9
  IconTerminal2,
9
10
  } from "@tabler/icons-react";
@@ -50,6 +51,7 @@ interface PlatformVariant {
50
51
  }
51
52
 
52
53
  const LATEST_JSON_URL = `${appBasePath()}/api/clips-latest.json`;
54
+ const MANIFEST_STORAGE_KEY = "clips-download-manifest-v1";
53
55
 
54
56
  const VARIANTS: PlatformVariant[] = [
55
57
  {
@@ -85,6 +87,65 @@ interface Manifest {
85
87
  }[];
86
88
  }
87
89
 
90
+ interface ConfirmedDownload {
91
+ asset: Manifest["assets"][number];
92
+ label: string;
93
+ }
94
+
95
+ function manifestStorageKey(channel: DownloadReleaseChannel): string {
96
+ return `${MANIFEST_STORAGE_KEY}-${channel}`;
97
+ }
98
+
99
+ function isManifest(value: unknown): value is Manifest {
100
+ if (!value || typeof value !== "object") return false;
101
+ const manifest = value as Partial<Manifest>;
102
+ return (
103
+ typeof manifest.version === "string" &&
104
+ typeof manifest.tag === "string" &&
105
+ (typeof manifest.pub_date === "string" || manifest.pub_date === null) &&
106
+ Array.isArray(manifest.assets) &&
107
+ manifest.assets.every((asset) => {
108
+ if (!asset || typeof asset !== "object") return false;
109
+ const candidate = asset as Partial<Manifest["assets"][number]>;
110
+ return (
111
+ typeof candidate.name === "string" &&
112
+ typeof candidate.url === "string" &&
113
+ typeof candidate.size === "number" &&
114
+ typeof candidate.kind === "string"
115
+ );
116
+ })
117
+ );
118
+ }
119
+
120
+ function readCachedManifest(channel: DownloadReleaseChannel): Manifest | null {
121
+ if (typeof window === "undefined") return null;
122
+ try {
123
+ const raw = window.localStorage.getItem(manifestStorageKey(channel));
124
+ if (!raw) return null;
125
+ const parsed: unknown = JSON.parse(raw);
126
+ return isManifest(parsed) ? parsed : null;
127
+ // coercion-ok: an unreadable browser cache is absent; the network fetch remains authoritative.
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function writeCachedManifest(
134
+ channel: DownloadReleaseChannel,
135
+ manifest: Manifest,
136
+ ): void {
137
+ if (typeof window === "undefined") return;
138
+ try {
139
+ window.localStorage.setItem(
140
+ manifestStorageKey(channel),
141
+ JSON.stringify(manifest),
142
+ );
143
+ // coercion-ok: browser storage is optional and must not block the download action.
144
+ } catch {
145
+ // Storage can be unavailable in private browsing or locked-down contexts.
146
+ }
147
+ }
148
+
88
149
  function detectPlatform(): PlatformId | null {
89
150
  if (typeof navigator === "undefined") return null;
90
151
  const ua = navigator.userAgent;
@@ -97,11 +158,11 @@ function detectPlatform(): PlatformId | null {
97
158
  function pickAsset(
98
159
  manifest: Manifest | null,
99
160
  variant: PlatformVariant,
100
- ): { url: string; name: string } | null {
161
+ ): Manifest["assets"][number] | null {
101
162
  if (!manifest) return null;
102
163
  for (const kind of variant.assetKinds) {
103
164
  const asset = manifest.assets.find((a) => a.kind === kind);
104
- if (asset) return { url: asset.url, name: asset.name };
165
+ if (asset) return asset;
105
166
  }
106
167
  return null;
107
168
  }
@@ -113,6 +174,9 @@ function primaryDownloadButton(
113
174
  downloadLabel: string,
114
175
  retryLabel: string,
115
176
  onRetry: () => void,
177
+ downloadStarted: boolean,
178
+ downloadStartedLabel: string,
179
+ onDownload: (asset: Manifest["assets"][number]) => void,
116
180
  ) {
117
181
  const asset = pickAsset(manifest, variant);
118
182
  const Icon = variant.icon;
@@ -123,9 +187,13 @@ function primaryDownloadButton(
123
187
  size="lg"
124
188
  className="h-12 min-w-[252px] gap-2 px-6 text-base"
125
189
  >
126
- <a href={asset.url} download onClick={markDesktopAppDownloaded}>
127
- <Icon className="h-5 w-5" />
128
- {downloadLabel}
190
+ <a href={asset.url} download onClick={() => onDownload(asset)}>
191
+ {downloadStarted ? (
192
+ <IconCheck className="h-5 w-5" />
193
+ ) : (
194
+ <Icon className="h-5 w-5" />
195
+ )}
196
+ {downloadStarted ? downloadStartedLabel : downloadLabel}
129
197
  </a>
130
198
  </Button>
131
199
  );
@@ -168,6 +236,8 @@ export default function DownloadPage() {
168
236
  const [manifestError, setManifestError] = useState(false);
169
237
  const [detected, setDetected] = useState<PlatformId | null>(null);
170
238
  const [manifestRequest, setManifestRequest] = useState(0);
239
+ const [confirmedDownload, setConfirmedDownload] =
240
+ useState<ConfirmedDownload | null>(null);
171
241
 
172
242
  useEffect(() => {
173
243
  setDetected(detectPlatform());
@@ -182,7 +252,8 @@ export default function DownloadPage() {
182
252
  if (!hostResolved) return;
183
253
 
184
254
  let cancelled = false;
185
- setManifest(null);
255
+ const cachedManifest = readCachedManifest(channel);
256
+ setManifest(cachedManifest);
186
257
  setManifestError(false);
187
258
  const manifestUrl =
188
259
  channel === "nightly"
@@ -191,10 +262,15 @@ export default function DownloadPage() {
191
262
  fetch(manifestUrl)
192
263
  .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))))
193
264
  .then((json) => {
194
- if (!cancelled) setManifest(json as Manifest);
265
+ if (!isManifest(json)) throw new Error("Invalid manifest");
266
+ if (!cancelled) {
267
+ setManifest(json);
268
+ setManifestError(false);
269
+ writeCachedManifest(channel, json);
270
+ }
195
271
  })
196
272
  .catch(() => {
197
- if (!cancelled) setManifestError(true);
273
+ if (!cancelled && !cachedManifest) setManifestError(true);
198
274
  });
199
275
  return () => {
200
276
  cancelled = true;
@@ -202,6 +278,7 @@ export default function DownloadPage() {
202
278
  }, [channel, hostResolved, manifestRequest]);
203
279
 
204
280
  const retryManifest = () => {
281
+ setConfirmedDownload(null);
205
282
  setManifestRequest((request) => request + 1);
206
283
  };
207
284
 
@@ -210,13 +287,27 @@ export default function DownloadPage() {
210
287
 
211
288
  setManifest(null);
212
289
  setManifestError(false);
290
+ setConfirmedDownload(null);
213
291
  setChannel(nextChannel);
214
292
  };
215
293
 
216
294
  const primary = VARIANTS.find((v) => v.id === detected) ?? VARIANTS[0];
295
+ const primaryAsset = pickAsset(manifest, primary);
296
+ const downloadLabel = t("downloadRoute.downloadFor", {
297
+ platform: primary.label,
298
+ });
299
+ const downloadStartedLabel = t("downloadRoute.downloadStarted");
300
+ const primaryDownloadStarted =
301
+ confirmedDownload?.asset.url === primaryAsset?.url;
302
+
303
+ const handleDownload = (asset: Manifest["assets"][number], label: string) => {
304
+ markDesktopAppDownloaded();
305
+ setConfirmedDownload({ asset, label });
306
+ };
217
307
 
218
308
  const handlePlatformChange = (nextPlatform: PlatformId) => {
219
309
  if (nextPlatform === detected) return;
310
+ setConfirmedDownload(null);
220
311
  setDetected(nextPlatform);
221
312
  };
222
313
 
@@ -297,9 +388,35 @@ export default function DownloadPage() {
297
388
  primary,
298
389
  manifest,
299
390
  manifestError,
300
- t("downloadRoute.downloadFor", { platform: primary.label }),
391
+ downloadLabel,
301
392
  t("downloadRoute.retry"),
302
393
  retryManifest,
394
+ primaryDownloadStarted,
395
+ downloadStartedLabel,
396
+ (asset) => handleDownload(asset, downloadLabel),
397
+ )}
398
+
399
+ {primaryDownloadStarted && confirmedDownload && (
400
+ <p
401
+ aria-live="polite"
402
+ className="mt-3 text-xs text-muted-foreground"
403
+ >
404
+ <span className="sr-only">{downloadStartedLabel}</span>
405
+ <a
406
+ href={confirmedDownload.asset.url}
407
+ target="_blank"
408
+ rel="noreferrer"
409
+ onClick={() =>
410
+ handleDownload(
411
+ confirmedDownload.asset,
412
+ confirmedDownload.label,
413
+ )
414
+ }
415
+ className="text-muted-foreground underline underline-offset-2 hover:text-foreground"
416
+ >
417
+ {t("downloadRoute.downloadAgain")}
418
+ </a>
419
+ </p>
303
420
  )}
304
421
 
305
422
  <div className="mt-5 flex justify-center">
@@ -349,25 +466,23 @@ export default function DownloadPage() {
349
466
  </div>
350
467
 
351
468
  {chromeExtensionEnabled && (
352
- <section className="mt-10 w-full max-w-xl rounded-2xl border border-border bg-card p-4 text-start shadow-sm">
353
- <div className="flex items-start gap-3">
354
- <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
469
+ <section className="mt-16 w-full max-w-md text-start">
470
+ <div className="flex items-center gap-2">
471
+ <div className="flex size-7 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
355
472
  <IconBrandChrome className="h-4 w-4" />
356
473
  </div>
357
474
  <div className="min-w-0 flex-1">
358
475
  <h2 className="text-sm font-semibold text-foreground">
359
476
  {t("downloadRoute.chromeTitle")}
360
477
  </h2>
361
- <p className="mt-1 text-sm leading-relaxed text-muted-foreground">
362
- {t("downloadRoute.chromeDescription")}
363
- </p>
364
478
  </div>
365
479
  </div>
366
480
  <Button
367
481
  asChild={Boolean(clipsChromeExtensionUrl)}
368
482
  disabled={!clipsChromeExtensionUrl}
369
483
  variant="outline"
370
- className="mt-4 w-full gap-2"
484
+ size="sm"
485
+ className="mt-3 w-full gap-2"
371
486
  >
372
487
  {clipsChromeExtensionUrl ? (
373
488
  <a
@@ -1122,10 +1122,10 @@ export default function ShareRoute() {
1122
1122
  canDownloadRecording || isLoomEmbedBacked ? recording.videoUrl : null;
1123
1123
 
1124
1124
  return (
1125
- <div className="flex min-h-screen max-w-full flex-col overflow-x-hidden bg-background text-foreground lg:h-screen lg:flex-row lg:overflow-hidden">
1125
+ <div className="clips-recording-view flex min-h-screen w-full max-w-full flex-col overflow-x-hidden bg-background xl:h-screen xl:flex-row xl:overflow-hidden [&_.agent-composer-root]:!bg-background [&_.agent-composer-root]:!border-0">
1126
1126
  {agentDiscovery}
1127
- <div className="clips-share-content flex w-full min-w-0 flex-col lg:flex-1">
1128
- <header className="flex min-w-0 shrink-0 flex-wrap items-center gap-2 border-b border-border px-3 py-2 sm:px-4 sm:py-3 lg:flex-nowrap">
1127
+ <div className="flex w-full min-w-0 flex-col xl:flex-1">
1128
+ <header className="flex min-w-0 shrink-0 flex-wrap items-center gap-2 px-3 py-2 sm:px-4 sm:py-3 xl:flex-nowrap">
1129
1129
  {session ? (
1130
1130
  <Button
1131
1131
  asChild
@@ -1138,7 +1138,7 @@ export default function ShareRoute() {
1138
1138
  </Link>
1139
1139
  </Button>
1140
1140
  ) : null}
1141
- <div className="flex min-w-0 flex-1 items-center gap-2">
1141
+ <div className="hidden min-w-0 flex-1 items-center gap-2 sm:flex">
1142
1142
  {recording.visibility === "private" ? (
1143
1143
  <span
1144
1144
  className="inline-flex shrink-0 text-muted-foreground"
@@ -1168,6 +1168,7 @@ export default function ShareRoute() {
1168
1168
  agentViewCount={agentViewCount}
1169
1169
  canViewDetails={viewerCanEdit}
1170
1170
  onOpenInsights={() => setPanel("insights")}
1171
+ className="shrink-0 border-0 shadow-none"
1171
1172
  />
1172
1173
  {session ? null : (
1173
1174
  <SignedOutShareActions
@@ -1189,7 +1190,10 @@ export default function ShareRoute() {
1189
1190
  hasPassword={Boolean(recording.hasPassword)}
1190
1191
  viewerReshareOnly={viewerReshareOnly}
1191
1192
  >
1192
- <ClipsShareTrigger label={t("sharePage.share")} />
1193
+ <ClipsShareTrigger
1194
+ label={t("sharePage.share")}
1195
+ className="border-0 shadow-none"
1196
+ />
1193
1197
  </ShareRecordingPopover>
1194
1198
  ) : null}
1195
1199
  {!viewerCanEdit && canDownloadRecording ? (
@@ -1240,8 +1244,8 @@ export default function ShareRoute() {
1240
1244
  </div>
1241
1245
  </header>
1242
1246
 
1243
- <div className="flex flex-col gap-4 overflow-hidden p-0 sm:p-4 lg:min-h-0 lg:flex-1">
1244
- <div className="relative aspect-video w-full lg:min-h-0 lg:flex-1 lg:aspect-auto">
1247
+ <div className="flex flex-col gap-0 overflow-hidden p-0 sm:gap-4 sm:p-4 xl:min-h-0 xl:flex-1 xl:overflow-hidden">
1248
+ <div className="relative aspect-video w-full xl:min-h-0 xl:flex-1 xl:aspect-auto">
1245
1249
  <VideoPlayer
1246
1250
  ref={playerRef}
1247
1251
  onVideoElementChange={setTrackedVideoEl}
@@ -1374,21 +1378,26 @@ export default function ShareRoute() {
1374
1378
  : null}
1375
1379
  </div>
1376
1380
 
1377
- <div className="clips-share-metadata flex shrink-0 flex-col gap-3 px-4 pb-4 sm:px-0 sm:pb-0">
1378
- <div className="clips-share-metadata-description min-w-0 flex-1">
1379
- {recording.description ? (
1380
- <p className="whitespace-pre-wrap break-words text-sm text-muted-foreground">
1381
- {recording.description}
1382
- </p>
1383
- ) : null}
1384
- </div>
1385
- <div className="clips-share-metadata-actions flex max-w-full flex-col items-stretch gap-2 sm:items-end">
1386
- <div className="flex flex-wrap items-center gap-2 sm:justify-end">
1381
+ <div className="flex shrink-0 flex-col gap-3 px-4 py-4 sm:flex-row sm:items-start sm:px-0 sm:py-0">
1382
+ <div className="min-w-0 flex-1">
1383
+ <div className="mb-3 sm:hidden">
1384
+ {showTitleSkeleton ? (
1385
+ <Skeleton
1386
+ aria-label={t("sharePage.generatingTitle")}
1387
+ className="h-7 w-72 max-w-full"
1388
+ />
1389
+ ) : (
1390
+ <h1 className="truncate text-2xl font-semibold leading-tight">
1391
+ {visibleTitle}
1392
+ </h1>
1393
+ )}
1394
+ </div>
1395
+ <div className="flex flex-wrap items-center gap-2">
1387
1396
  {recording.enableComments ? (
1388
1397
  <TimestampedCommentButton
1389
1398
  enableComments={recording.enableComments}
1390
1399
  canComment={!session || viewerCanComment}
1391
- className="shrink-0"
1400
+ className="shrink-0 border-0 shadow-none"
1392
1401
  onOpen={() => {
1393
1402
  if (!session) {
1394
1403
  requireSignIn("comment");
@@ -1405,6 +1414,7 @@ export default function ShareRoute() {
1405
1414
  <ReactionsTray
1406
1415
  reactions={reactions}
1407
1416
  disabled={Boolean(session) && !viewerCanComment}
1417
+ className="border-0 shadow-none"
1408
1418
  onReact={(emoji) => {
1409
1419
  if (!session) {
1410
1420
  requireSignIn("react");
@@ -1441,13 +1451,18 @@ export default function ShareRoute() {
1441
1451
  />
1442
1452
  ) : null}
1443
1453
  </div>
1454
+ {recording.description ? (
1455
+ <p className="mt-3 whitespace-pre-wrap break-words text-sm text-muted-foreground">
1456
+ {recording.description}
1457
+ </p>
1458
+ ) : null}
1444
1459
  {viewerCanEdit && canDownloadRecording ? (
1445
1460
  <Button
1446
1461
  variant="outline"
1447
1462
  size="sm"
1448
1463
  onClick={downloadRecording}
1449
1464
  disabled={downloading}
1450
- className="gap-1.5"
1465
+ className="mt-3 gap-1.5 border-0 shadow-none"
1451
1466
  >
1452
1467
  <IconDownload className="h-4 w-4" />
1453
1468
  {downloading
@@ -1460,7 +1475,7 @@ export default function ShareRoute() {
1460
1475
  </div>
1461
1476
  </div>
1462
1477
 
1463
- <aside className="flex min-h-[420px] w-full min-w-0 shrink-0 flex-col border-t border-border bg-background lg:min-h-0 lg:w-[380px] lg:border-s lg:border-t-0">
1478
+ <aside className="flex min-h-[420px] w-full min-w-0 shrink-0 flex-col bg-muted xl:min-h-0 xl:w-[380px] xl:flex">
1464
1479
  <Tabs
1465
1480
  value={panel}
1466
1481
  onValueChange={setPanel}
@@ -32,12 +32,12 @@ import {
32
32
  *
33
33
  * - A 5-minute process-wide memoization (`cached`) — every request
34
34
  * for 5 min shares one upstream fetch.
35
- * - A stale-while-error fallback — if GitHub ever errors out AND we
36
- * have a previously-successful payload (even expired), we return
37
- * it. Avoids a download outage during a transient GitHub hiccup or
38
- * a rate-limit burst.
39
- * - HTTP `cache-control: max-age=60` on the response so downstream
40
- * CDNs + the browser cache this aggressively.
35
+ * - A stale-while-revalidate refresh — once the cache expires, we return the
36
+ * previous payload immediately and refresh it in the background.
37
+ * - A stale-while-error fallback — if GitHub ever errors out AND we have a
38
+ * previously-successful payload (even expired), we keep serving it.
39
+ * - Durable HTTP cache headers with short freshness and a long stale window
40
+ * for downstream CDNs + the browser.
41
41
  */
42
42
 
43
43
  const RELEASES_URL_BASE =
@@ -47,6 +47,16 @@ const PER_PAGE = 100;
47
47
  // by then, something else is wrong and the 404 is correct.
48
48
  const MAX_PAGES = 10;
49
49
  const CACHE_TTL_MS = 5 * 60_000;
50
+ const CACHE_RETRY_BACKOFF_MS = 60_000;
51
+
52
+ export const CLIPS_RELEASE_CACHE_HEADERS = {
53
+ "cache-control":
54
+ "public, max-age=300, stale-while-revalidate=86400, stale-if-error=86400",
55
+ "cdn-cache-control":
56
+ "public, max-age=300, stale-while-revalidate=86400, stale-if-error=86400",
57
+ "netlify-cdn-cache-control":
58
+ "public, durable, s-maxage=300, stale-while-revalidate=86400, stale-if-error=86400",
59
+ } as const;
50
60
 
51
61
  export type ClipsReleaseChannel = "production" | "nightly";
52
62
 
@@ -201,6 +211,9 @@ const cache = new Map<
201
211
  { data: DownloadManifest; ts: number }
202
212
  >();
203
213
  const inFlight = new Map<ClipsReleaseChannel, Promise<DownloadManifest>>();
214
+ const retryAfter = new Map<ClipsReleaseChannel, number>();
215
+
216
+ type WaitUntil = (promise: Promise<unknown>) => void;
204
217
 
205
218
  class UpstreamError extends Error {
206
219
  statusCode: number;
@@ -347,32 +360,70 @@ async function buildManifest(
347
360
  };
348
361
  }
349
362
 
350
- async function getManifest(
351
- channel: ClipsReleaseChannel = "production",
363
+ function refreshManifest(
364
+ channel: ClipsReleaseChannel,
352
365
  ): Promise<DownloadManifest> {
353
- const now = Date.now();
354
- const cached = cache.get(channel);
355
- if (cached && now - cached.ts < CACHE_TTL_MS) return cached.data;
356
366
  const pending = inFlight.get(channel);
357
367
  if (pending) return pending;
358
368
  const request = (async () => {
359
369
  try {
360
370
  const data = await buildManifest(channel);
361
371
  cache.set(channel, { data, ts: Date.now() });
372
+ retryAfter.delete(channel);
362
373
  return data;
363
- } catch (err) {
364
- // Stale-while-error: if we have an older payload, serve it. Only
365
- // bubble the error if the cache is empty.
366
- if (cached) return cached.data;
367
- throw err;
368
- } finally {
369
- inFlight.delete(channel);
374
+ } catch (error) {
375
+ retryAfter.set(channel, Date.now() + CACHE_RETRY_BACKOFF_MS);
376
+ throw error;
370
377
  }
371
378
  })();
372
- inFlight.set(channel, request);
373
- return request;
379
+ inFlight.set(
380
+ channel,
381
+ request.finally(() => {
382
+ inFlight.delete(channel);
383
+ }),
384
+ );
385
+ return inFlight.get(channel)!;
386
+ }
387
+
388
+ function refreshInBackground(
389
+ channel: ClipsReleaseChannel,
390
+ waitUntil?: WaitUntil,
391
+ ): void {
392
+ const refresh = refreshManifest(channel).catch(() => undefined);
393
+ if (waitUntil) {
394
+ waitUntil(refresh);
395
+ } else {
396
+ void refresh;
397
+ }
374
398
  }
375
399
 
400
+ async function getManifest(
401
+ channel: ClipsReleaseChannel = "production",
402
+ waitUntil?: WaitUntil,
403
+ ): Promise<DownloadManifest> {
404
+ const now = Date.now();
405
+ const cached = cache.get(channel);
406
+ if (cached) {
407
+ if (
408
+ now - cached.ts >= CACHE_TTL_MS &&
409
+ now >= (retryAfter.get(channel) ?? 0)
410
+ ) {
411
+ refreshInBackground(channel, waitUntil);
412
+ }
413
+ return cached.data;
414
+ }
415
+ return refreshManifest(channel);
416
+ }
417
+
418
+ export const __clipsLatestTest = {
419
+ getManifest,
420
+ reset() {
421
+ cache.clear();
422
+ inFlight.clear();
423
+ retryAfter.clear();
424
+ },
425
+ };
426
+
376
427
  export function normalizeClipsReleaseChannel(
377
428
  value: unknown,
378
429
  ): ClipsReleaseChannel {
@@ -383,7 +434,11 @@ export default defineEventHandler(async (event) => {
383
434
  const channel = normalizeClipsReleaseChannel(getQuery(event).channel);
384
435
  let manifest: DownloadManifest;
385
436
  try {
386
- manifest = await getManifest(channel);
437
+ const waitUntil =
438
+ typeof event.waitUntil === "function"
439
+ ? (promise: Promise<unknown>) => event.waitUntil(promise)
440
+ : undefined;
441
+ manifest = await getManifest(channel, waitUntil);
387
442
  } catch (err) {
388
443
  const e = err as {
389
444
  statusCode?: number;
@@ -397,7 +452,7 @@ export default defineEventHandler(async (event) => {
397
452
  }
398
453
  setResponseHeaders(event, {
399
454
  "content-type": "application/json; charset=utf-8",
400
- "cache-control": "public, max-age=60",
455
+ ...CLIPS_RELEASE_CACHE_HEADERS,
401
456
  });
402
457
  return manifest;
403
458
  });
@@ -77,7 +77,10 @@ const responseInsightsSchema = z.object({
77
77
  type ResponseInsightsArgs = z.infer<typeof responseInsightsSchema>;
78
78
 
79
79
  type FormRow = typeof schema.forms.$inferSelect;
80
- type ResponseRow = typeof schema.responses.$inferSelect;
80
+ type ResponseRow = Pick<
81
+ typeof schema.responses.$inferSelect,
82
+ "id" | "formId" | "data" | "submittedAt" | "submitterEmail"
83
+ >;
81
84
 
82
85
  function safeJson<T>(value: string, fallback: T): T {
83
86
  try {
@@ -331,10 +334,7 @@ export default defineAction({
331
334
  formId: schema.responses.formId,
332
335
  data: schema.responses.data,
333
336
  submittedAt: schema.responses.submittedAt,
334
- ip: schema.responses.ip,
335
337
  submitterEmail: schema.responses.submitterEmail,
336
- pageUrl: schema.responses.pageUrl,
337
- clientSurface: schema.responses.clientSurface,
338
338
  })
339
339
  .from(schema.responses)
340
340
  .where(responseFilter)
@@ -0,0 +1,26 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { assertAccess } from "@agent-native/core/sharing";
3
+ import { eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import { reconcileResponseDeliveries } from "../server/handlers/submissions.js";
8
+
9
+ export default defineAction({
10
+ description:
11
+ "Retry failed or pending deliveries for a saved response using its immutable delivery snapshot. The form may be unpublished or archived; pass the response ID.",
12
+ schema: z.object({
13
+ responseId: z.string().describe("Saved response ID to reconcile"),
14
+ }),
15
+ run: async ({ responseId }) => {
16
+ const db = getDb();
17
+ const [response] = await db
18
+ .select({ formId: schema.responses.formId })
19
+ .from(schema.responses)
20
+ .where(eq(schema.responses.id, responseId));
21
+ if (!response) throw new Error(`Response ${responseId} not found`);
22
+
23
+ await assertAccess("form", response.formId, "editor");
24
+ return reconcileResponseDeliveries(responseId);
25
+ },
26
+ });