@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/actions/set-workspace-feature-flag.ts +2 -2
- package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +16 -1
- package/corpus/templates/analytics/changelog/2026-08-27-show-analytics-s-own-registered-flags-in-fleet-feature-flag-.md +6 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +204 -32
- package/corpus/templates/assets/changelog/2026-08-26-templates-across-brand-kits.md +1 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +61 -68
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +9 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +3 -3
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +8 -3
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +12 -3
- 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
- package/corpus/templates/calendar/changelog/2026-08-28-event-creators-can-choose-whether-a-meeting-shows-as-free-or.md +6 -0
- package/corpus/templates/clips/app/global.css +0 -27
- package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
- package/corpus/templates/clips/app/routes/download.tsx +131 -16
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +35 -20
- package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +77 -22
- package/corpus/templates/forms/actions/response-insights.ts +4 -4
- package/corpus/templates/forms/actions/retry-response-deliveries.ts +26 -0
- package/corpus/templates/forms/server/db/schema.ts +64 -17
- package/corpus/templates/forms/server/handlers/submissions.ts +1022 -94
- package/corpus/templates/forms/server/lib/integrations.ts +100 -33
- package/corpus/templates/forms/server/middleware/00-public-cors.ts +1 -1
- package/corpus/templates/forms/server/plugins/db.ts +40 -0
- package/corpus/templates/plan/agent-native.json +1 -4
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +6 -1
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -6
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +78 -12
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +4 -4
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +111 -27
- package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +20 -34
- package/corpus/templates/slides/app/components/editor/slide-style.ts +2 -0
- package/corpus/templates/slides/app/global.css +35 -0
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +133 -5
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +675 -0
- package/corpus/templates/slides/app/lib/validate-native-slide-html.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -44
- package/corpus/templates/slides/app/root.tsx +1 -22
- package/corpus/templates/slides/changelog/2026-08-27-image-fit-controls-apply-cover-crops-and-position.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-28-shared-slides-with-a-style-block-render-again.md +6 -0
- package/dist/action.d.ts +3 -1
- package/dist/client/FeedbackButton.d.ts +12 -0
- package/dist/client/FeedbackButton.js +62 -28
- package/dist/client/app-providers.js +13 -1
- package/dist/client/feedback-context.d.ts +1 -0
- package/dist/client/feedback-context.js +3 -2
- package/dist/client/host-bridge.d.ts +1 -0
- package/dist/client/observability/ThumbsFeedback.js +73 -10
- package/dist/client/webmcp.d.ts +4 -0
- package/dist/client/webmcp.js +51 -4
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/observability/routes.js +8 -2
- package/dist/observability/store.d.ts +1 -1
- package/dist/observability/store.js +18 -3
- package/dist/observability/types.d.ts +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-routes.d.ts +1 -6
- package/dist/server/action-routes.js +58 -17
- package/dist/server/agent-chat-plugin.js +8 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/webmcp.mdx +31 -12
- package/package.json +1 -1
|
@@ -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
|
-
):
|
|
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
|
|
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={
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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-
|
|
353
|
-
<div className="flex items-
|
|
354
|
-
<div className="flex
|
|
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
|
-
|
|
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
|
|
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="
|
|
1128
|
-
<header className="flex min-w-0 shrink-0 flex-wrap items-center gap-2
|
|
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="
|
|
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
|
|
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-
|
|
1244
|
-
<div className="relative aspect-video w-full
|
|
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="
|
|
1378
|
-
<div className="
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
|
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-
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* - HTTP
|
|
40
|
-
* CDNs + the browser
|
|
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
|
-
|
|
351
|
-
channel: ClipsReleaseChannel
|
|
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 (
|
|
364
|
-
|
|
365
|
-
|
|
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(
|
|
373
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
+
});
|