@agent-native/core 0.178.1-nightly-20260910175201 → 0.178.1

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 (37) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +1 -0
  3. package/corpus/templates/calendar/CHANGELOG.md +7 -0
  4. package/corpus/templates/clips/CHANGELOG.md +22 -0
  5. package/corpus/templates/clips/actions/create-intake-agent-link.ts +114 -0
  6. package/corpus/templates/clips/actions/create-intake-recording.ts +208 -0
  7. package/corpus/templates/clips/actions/create-recording-intake-link.ts +85 -0
  8. package/corpus/templates/clips/app/components/bug-report/bug-report-form.tsx +7 -0
  9. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +22 -5
  10. package/corpus/templates/clips/app/hooks/use-video-storage-status.ts +2 -1
  11. package/corpus/templates/clips/app/routes/bug-report.done.tsx +12 -10
  12. package/corpus/templates/clips/app/routes/bug-report.tsx +6 -0
  13. package/corpus/templates/clips/app/routes/record.tsx +269 -100
  14. package/corpus/templates/clips/server/db/schema.ts +24 -0
  15. package/corpus/templates/clips/server/lib/clip-intake.ts +247 -0
  16. package/corpus/templates/clips/server/plugins/auth.ts +8 -0
  17. package/corpus/templates/clips/server/plugins/db.ts +16 -0
  18. package/corpus/templates/clips/server/routes/api/clip-intake.get.ts +61 -0
  19. package/corpus/templates/clips/server/routes/api/clip-intake.post.ts +90 -0
  20. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +19 -4
  21. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +25 -10
  22. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +21 -4
  23. package/corpus/templates/clips/shared/clip-intake.ts +41 -0
  24. package/corpus/templates/clips/shared/finalize-recovery.ts +27 -5
  25. package/corpus/templates/clips/shared/recording-core.ts +4 -4
  26. package/corpus/templates/content/CHANGELOG.md +1 -0
  27. package/corpus/templates/design/CHANGELOG.md +9 -4
  28. package/corpus/templates/factory/CHANGELOG.md +11 -0
  29. package/dist/collab/struct-routes.d.ts +1 -1
  30. package/dist/observability/routes.d.ts +3 -3
  31. package/dist/progress/routes.d.ts +1 -1
  32. package/dist/resources/handlers.d.ts +1 -1
  33. package/dist/secrets/routes.d.ts +3 -3
  34. package/docs/content/template-clips-anonymous-intake.mdx +148 -0
  35. package/docs/content/template-clips-developers.mdx +4 -3
  36. package/docs/content/template-clips.mdx +1 -0
  37. package/package.json +3 -3
@@ -115,6 +115,10 @@ import {
115
115
  parseBugReportContext,
116
116
  type BugReportContext,
117
117
  } from "@shared/bug-report";
118
+ import {
119
+ parseClipIntakeParams,
120
+ type ClipIntakeParams,
121
+ } from "@shared/clip-intake";
118
122
  import { toast } from "sonner";
119
123
 
120
124
  import { CaptureInstallButton } from "@/components/capture-install-options";
@@ -223,9 +227,17 @@ function openUrlFromUserGesture(url: string): void {
223
227
  }
224
228
  }
225
229
 
226
- function bugReportDonePath(recordingId: string, context: BugReportContext) {
230
+ function bugReportDonePath(
231
+ recordingId: string,
232
+ context: BugReportContext,
233
+ intake: ClipIntakeParams | null,
234
+ ) {
227
235
  const params = new URLSearchParams({ recordingId });
228
236
  if (context.returnUrl) params.set("returnUrl", context.returnUrl);
237
+ if (intake) {
238
+ params.set("clip_intake_id", intake.intakeId);
239
+ params.set("clip_intake", intake.token);
240
+ }
229
241
  return `/bug-report/done?${params.toString()}`;
230
242
  }
231
243
 
@@ -549,9 +561,72 @@ interface PendingRecording {
549
561
  id: string;
550
562
  uploadChunkUrl: string;
551
563
  abortUrl: string;
564
+ resetChunksUrl?: string;
552
565
  uploadMode?: UploadMode;
553
566
  }
554
567
 
568
+ const INTAKE_CREATE_RETRY_DELAYS_MS = [250, 500, 1_000, 2_000] as const;
569
+
570
+ function isRetryableIntakeCreateStatus(status: number): boolean {
571
+ return [408, 409, 425, 429, 500, 502, 503, 504].includes(status);
572
+ }
573
+
574
+ async function createRecordingRequest(
575
+ url: string,
576
+ body: Record<string, unknown>,
577
+ signal?: AbortSignal,
578
+ ): Promise<Response> {
579
+ const isIntakeRequest =
580
+ typeof body.intakeId === "string" && typeof body.intakeToken === "string";
581
+ const request = () =>
582
+ fetch(url, {
583
+ method: "POST",
584
+ headers: { "Content-Type": "application/json" },
585
+ body: JSON.stringify(body),
586
+ signal,
587
+ });
588
+
589
+ for (let attempt = 0; ; attempt += 1) {
590
+ let response: Response;
591
+ try {
592
+ response = await request();
593
+ } catch (error) {
594
+ if (
595
+ !isIntakeRequest ||
596
+ signal?.aborted ||
597
+ attempt >= INTAKE_CREATE_RETRY_DELAYS_MS.length
598
+ ) {
599
+ throw error;
600
+ }
601
+ await new Promise((resolve) =>
602
+ window.setTimeout(
603
+ resolve,
604
+ INTAKE_CREATE_RETRY_DELAYS_MS[attempt] ?? 2_000,
605
+ ),
606
+ );
607
+ continue;
608
+ }
609
+
610
+ if (
611
+ !isIntakeRequest ||
612
+ !isRetryableIntakeCreateStatus(response.status) ||
613
+ attempt >= INTAKE_CREATE_RETRY_DELAYS_MS.length
614
+ ) {
615
+ return response;
616
+ }
617
+
618
+ // A transient response can mean the server already claimed the one-use
619
+ // intake and is still attaching its recording. Retrying the same signed
620
+ // request lets the idempotent action recover the attached row.
621
+ await new Promise((resolve) =>
622
+ window.setTimeout(
623
+ resolve,
624
+ INTAKE_CREATE_RETRY_DELAYS_MS[attempt] ?? 2_000,
625
+ ),
626
+ );
627
+ }
628
+ }
629
+
555
630
  function PreRecordPanelSkeleton() {
556
631
  return (
557
632
  <div
@@ -908,7 +983,11 @@ export default function RecordRoute() {
908
983
 
909
984
  const queryClient = useQueryClient();
910
985
  const { isDesktopApp } = useDesktopPromo();
911
- const storageQuery = useVideoStorageStatus();
986
+ const clipIntake = useMemo(
987
+ () => parseClipIntakeParams(new URLSearchParams(location.search)),
988
+ [location.search],
989
+ );
990
+ const storageQuery = useVideoStorageStatus(!clipIntake);
912
991
 
913
992
  // When the user clicks "Record for this space/folder", the empty-state CTA
914
993
  // appends ?spaceId or ?folderId so the new recording lands there.
@@ -920,9 +999,6 @@ export default function RecordRoute() {
920
999
  const params = new URLSearchParams(location.search);
921
1000
  return params.get("folderId") || null;
922
1001
  }, [location.search]);
923
- const storageConfigured: boolean | null = storageQuery.isLoading
924
- ? null
925
- : !!storageQuery.data?.configured;
926
1002
  const initialRecorderOptions = useMemo(() => {
927
1003
  const params = new URLSearchParams(location.search);
928
1004
  const mode = params.get("mode");
@@ -949,6 +1025,15 @@ export default function RecordRoute() {
949
1025
  () => parseBugReportContext(new URLSearchParams(location.search)),
950
1026
  [location.search],
951
1027
  );
1028
+ const clipIntakeRef = useRef<ClipIntakeParams | null>(null);
1029
+ useEffect(() => {
1030
+ clipIntakeRef.current = clipIntake;
1031
+ }, [clipIntake]);
1032
+ const storageConfigured: boolean | null = clipIntake
1033
+ ? true
1034
+ : storageQuery.isLoading
1035
+ ? null
1036
+ : !!storageQuery.data?.configured;
952
1037
  const markStorageConfigured = useCallback(
953
1038
  (status?: VideoStorageStatus) => {
954
1039
  queryClient.setQueryData<VideoStorageStatus>(
@@ -1022,6 +1107,7 @@ export default function RecordRoute() {
1022
1107
  // that upload, so doCancel() can trash it directly — createdId otherwise
1023
1108
  // only lives in uploadFile's own closure and never reaches pendingRef.
1024
1109
  const fileUploadRecordingIdRef = useRef<string | null>(null);
1110
+ const fileUploadAbortUrlRef = useRef<string | null>(null);
1025
1111
  const browserDiagnosticsRef = useRef<BrowserDiagnosticsCapture | null>(null);
1026
1112
  // Bumped by doCancel() to invalidate any in-flight startFlow().
1027
1113
  const startSessionRef = useRef(0);
@@ -1245,17 +1331,25 @@ export default function RecordRoute() {
1245
1331
  liveTranscription.start();
1246
1332
  }
1247
1333
 
1248
- const status = await fetchVideoStorageStatus();
1249
- if (isStale()) {
1250
- await liveTranscription.stopAndWait().catch(() => "");
1251
- await engine.cancel().catch(() => {});
1252
- return;
1253
- }
1254
- markStorageConfigured(status);
1255
- if (!status.configured) {
1256
- throw new Error(
1257
- "No video storage configured. Connect storage: Builder.io (free tier storage + AI) or S3-compatible storage.",
1258
- );
1334
+ const intake = clipIntakeRef.current;
1335
+ if (!intake) {
1336
+ const status = await fetchVideoStorageStatus();
1337
+ if (isStale()) {
1338
+ try {
1339
+ await liveTranscription.stopAndWait();
1340
+ // coercion-ok: stale recording cleanup intentionally ignores stop failure.
1341
+ } catch {
1342
+ // The recording is already stale; cleanup failure cannot change the outcome.
1343
+ }
1344
+ await engine.cancel().catch(() => {});
1345
+ return;
1346
+ }
1347
+ markStorageConfigured(status);
1348
+ if (!status.configured) {
1349
+ throw new Error(
1350
+ "No video storage configured. Connect storage: Builder.io (free tier storage + AI) or S3-compatible storage.",
1351
+ );
1352
+ }
1259
1353
  }
1260
1354
 
1261
1355
  // 2. Create the recording row server-side once permissions are granted.
@@ -1263,25 +1357,33 @@ export default function RecordRoute() {
1263
1357
  const reportTitle = reportContext
1264
1358
  ? `Bug report: ${bugReportTitle(reportContext)}`
1265
1359
  : null;
1266
- const res = await fetch(
1267
- agentNativePath("/_agent-native/actions/create-recording"),
1268
- {
1269
- method: "POST",
1270
- headers: { "Content-Type": "application/json" },
1271
- body: JSON.stringify({
1272
- title: reportTitle ?? captureTitle.title,
1273
- titleSource: reportTitle ? "context" : captureTitle.titleSource,
1274
- sourceAppName: captureTitle.sourceAppName,
1275
- sourceWindowTitle: captureTitle.sourceWindowTitle,
1276
- hasCamera: opts.mode !== "screen",
1277
- hasAudio: wantsMic,
1278
- visibility: reportContext ? "org" : undefined,
1279
- spaceIds: spaceIdFromUrl ? [spaceIdFromUrl] : undefined,
1280
- folderId: folderIdFromUrl ?? undefined,
1281
- mimeType: pickMimeType() || undefined,
1282
- requestStreaming: canUseTimeslicedRecorderChunks(pickMimeType()),
1283
- }),
1284
- },
1360
+ const recordingPayload = {
1361
+ title: reportTitle ?? captureTitle.title,
1362
+ titleSource: reportTitle ? "context" : captureTitle.titleSource,
1363
+ sourceAppName: captureTitle.sourceAppName,
1364
+ sourceWindowTitle: captureTitle.sourceWindowTitle,
1365
+ hasCamera: opts.mode !== "screen",
1366
+ hasAudio: wantsMic,
1367
+ visibility: reportContext ? "org" : undefined,
1368
+ spaceIds: spaceIdFromUrl ? [spaceIdFromUrl] : undefined,
1369
+ folderId: folderIdFromUrl ?? undefined,
1370
+ mimeType: pickMimeType() || undefined,
1371
+ requestStreaming: canUseTimeslicedRecorderChunks(pickMimeType()),
1372
+ };
1373
+ const res = await createRecordingRequest(
1374
+ agentNativePath(
1375
+ intake
1376
+ ? "/_agent-native/actions/create-intake-recording"
1377
+ : "/_agent-native/actions/create-recording",
1378
+ ),
1379
+ intake
1380
+ ? {
1381
+ ...recordingPayload,
1382
+ intakeId: intake.intakeId,
1383
+ intakeToken: intake.token,
1384
+ bugReport: reportContext ?? undefined,
1385
+ }
1386
+ : recordingPayload,
1285
1387
  );
1286
1388
  if (!res.ok) {
1287
1389
  if (res.status === 401 || res.status === 403) {
@@ -1299,11 +1401,13 @@ export default function RecordRoute() {
1299
1401
  id: string;
1300
1402
  uploadChunkUrl: string;
1301
1403
  abortUrl: string;
1404
+ resetChunksUrl?: string;
1302
1405
  uploadMode?: UploadMode;
1303
1406
  };
1304
1407
  id?: string;
1305
1408
  uploadChunkUrl?: string;
1306
1409
  abortUrl?: string;
1410
+ resetChunksUrl?: string;
1307
1411
  uploadMode?: UploadMode;
1308
1412
  };
1309
1413
  const info = created.result ?? (created as PendingRecording);
@@ -1313,11 +1417,18 @@ export default function RecordRoute() {
1313
1417
  // Cancelled mid-POST: pendingRef is still null, so trash directly.
1314
1418
  if (isStale()) {
1315
1419
  await liveTranscription.stopAndWait().catch(() => "");
1316
- fetch(agentNativePath("/_agent-native/actions/trash-recording"), {
1317
- method: "POST",
1318
- headers: { "Content-Type": "application/json" },
1319
- body: JSON.stringify({ id: info.id }),
1320
- }).catch(() => {});
1420
+ if (intake) {
1421
+ fetch(`${appBasePath()}${info.abortUrl}`, {
1422
+ method: "POST",
1423
+ headers: { "Content-Type": "application/json" },
1424
+ }).catch(() => {});
1425
+ } else {
1426
+ fetch(agentNativePath("/_agent-native/actions/trash-recording"), {
1427
+ method: "POST",
1428
+ headers: { "Content-Type": "application/json" },
1429
+ body: JSON.stringify({ id: info.id }),
1430
+ }).catch(() => {});
1431
+ }
1321
1432
  await engine.cancel().catch(() => {});
1322
1433
  return;
1323
1434
  }
@@ -1332,9 +1443,12 @@ export default function RecordRoute() {
1332
1443
  recordingId: info.id,
1333
1444
  uploadUrl: uploadChunkUrl,
1334
1445
  abortUrl,
1446
+ resetUrl: info.resetChunksUrl
1447
+ ? `${appBasePath()}${info.resetChunksUrl}`
1448
+ : undefined,
1335
1449
  uploadMode: info.uploadMode,
1336
1450
  });
1337
- await saveBugReportContextRef.current(info.id);
1451
+ if (!intake) await saveBugReportContextRef.current(info.id);
1338
1452
 
1339
1453
  setPreviewStream(ps);
1340
1454
  setCameraStream(cs);
@@ -1354,11 +1468,19 @@ export default function RecordRoute() {
1354
1468
  // record attempts.
1355
1469
  const orphan = pendingRef.current;
1356
1470
  if (orphan?.id) {
1357
- fetch(agentNativePath("/_agent-native/actions/trash-recording"), {
1358
- method: "POST",
1359
- headers: { "Content-Type": "application/json" },
1360
- body: JSON.stringify({ id: orphan.id }),
1361
- }).catch(() => {});
1471
+ const intake = clipIntakeRef.current;
1472
+ if (intake) {
1473
+ fetch(orphan.abortUrl, {
1474
+ method: "POST",
1475
+ headers: { "Content-Type": "application/json" },
1476
+ }).catch(() => {});
1477
+ } else {
1478
+ fetch(agentNativePath("/_agent-native/actions/trash-recording"), {
1479
+ method: "POST",
1480
+ headers: { "Content-Type": "application/json" },
1481
+ body: JSON.stringify({ id: orphan.id }),
1482
+ }).catch(() => {});
1483
+ }
1362
1484
  }
1363
1485
  // Release any tracks the engine grabbed before failing.
1364
1486
  try {
@@ -1499,13 +1621,16 @@ export default function RecordRoute() {
1499
1621
 
1500
1622
  let createdId: string | null = null;
1501
1623
  try {
1502
- const status = await fetchVideoStorageStatus();
1503
- if (isStale()) return;
1504
- markStorageConfigured(status);
1505
- if (!status.configured) {
1506
- throw new Error(
1507
- "No video storage configured. Connect storage: Builder.io (free tier storage + AI) or S3-compatible storage.",
1508
- );
1624
+ const intake = clipIntakeRef.current;
1625
+ if (!intake) {
1626
+ const status = await fetchVideoStorageStatus();
1627
+ if (isStale()) return;
1628
+ markStorageConfigured(status);
1629
+ if (!status.configured) {
1630
+ throw new Error(
1631
+ "No video storage configured. Connect storage: Builder.io (free tier storage + AI) or S3-compatible storage.",
1632
+ );
1633
+ }
1509
1634
  }
1510
1635
 
1511
1636
  const meta = await probeVideoMetadata(file);
@@ -1584,29 +1709,37 @@ export default function RecordRoute() {
1584
1709
  const reportTitle = reportContext
1585
1710
  ? `Bug report: ${bugReportTitle(reportContext)}`
1586
1711
  : null;
1712
+ const recordingPayload = {
1713
+ title:
1714
+ reportTitle ??
1715
+ (file.name.replace(/\.[^/.]+$/, "") || defaultRecordingTitle()),
1716
+ titleSource: reportTitle ? "context" : "upload",
1717
+ hasCamera: false,
1718
+ hasAudio: true,
1719
+ width: meta.width,
1720
+ height: meta.height,
1721
+ visibility: reportContext ? "org" : undefined,
1722
+ spaceIds: spaceIdFromUrl ? [spaceIdFromUrl] : undefined,
1723
+ folderId: folderIdFromUrl ?? undefined,
1724
+ mimeType: uploadMimeType,
1725
+ requestStreaming: true,
1726
+ };
1587
1727
 
1588
- const res = await fetch(
1589
- agentNativePath("/_agent-native/actions/create-recording"),
1590
- {
1591
- method: "POST",
1592
- headers: { "Content-Type": "application/json" },
1593
- signal: abort.signal,
1594
- body: JSON.stringify({
1595
- title:
1596
- reportTitle ??
1597
- (file.name.replace(/\.[^/.]+$/, "") || defaultRecordingTitle()),
1598
- titleSource: reportTitle ? "context" : "upload",
1599
- hasCamera: false,
1600
- hasAudio: true,
1601
- width: meta.width,
1602
- height: meta.height,
1603
- visibility: reportContext ? "org" : undefined,
1604
- spaceIds: spaceIdFromUrl ? [spaceIdFromUrl] : undefined,
1605
- folderId: folderIdFromUrl ?? undefined,
1606
- mimeType: uploadMimeType,
1607
- requestStreaming: true,
1608
- }),
1609
- },
1728
+ const res = await createRecordingRequest(
1729
+ agentNativePath(
1730
+ intake
1731
+ ? "/_agent-native/actions/create-intake-recording"
1732
+ : "/_agent-native/actions/create-recording",
1733
+ ),
1734
+ intake
1735
+ ? {
1736
+ ...recordingPayload,
1737
+ intakeId: intake.intakeId,
1738
+ intakeToken: intake.token,
1739
+ bugReport: reportContext ?? undefined,
1740
+ }
1741
+ : recordingPayload,
1742
+ abort.signal,
1610
1743
  );
1611
1744
  if (!res.ok) {
1612
1745
  if (res.status === 401 || res.status === 403) {
@@ -1624,11 +1757,13 @@ export default function RecordRoute() {
1624
1757
  id: string;
1625
1758
  uploadChunkUrl: string;
1626
1759
  abortUrl?: string;
1760
+ resetChunksUrl?: string;
1627
1761
  uploadMode?: UploadMode;
1628
1762
  };
1629
1763
  id?: string;
1630
1764
  uploadChunkUrl?: string;
1631
1765
  abortUrl?: string;
1766
+ resetChunksUrl?: string;
1632
1767
  uploadMode?: UploadMode;
1633
1768
  };
1634
1769
  const info =
@@ -1644,16 +1779,20 @@ export default function RecordRoute() {
1644
1779
  }
1645
1780
  createdId = info.id;
1646
1781
  fileUploadRecordingIdRef.current = createdId;
1647
- await saveBugReportContextRef.current(info.id);
1782
+ fileUploadAbortUrlRef.current =
1783
+ intake && info.abortUrl ? `${appBasePath()}${info.abortUrl}` : null;
1784
+ if (!intake) await saveBugReportContextRef.current(info.id);
1648
1785
  if (isStale()) throw makeAbortError("Upload cancelled");
1649
- void uploadVideoBlobThumbnail(createdId, uploadBlob, {
1650
- signal: abort.signal,
1651
- }).catch((err) => {
1652
- console.warn("[recorder] local-file thumbnail upload skipped", {
1653
- recordingId: createdId,
1654
- error: err instanceof Error ? err.message : String(err),
1786
+ if (!intake) {
1787
+ void uploadVideoBlobThumbnail(createdId, uploadBlob, {
1788
+ signal: abort.signal,
1789
+ }).catch((err) => {
1790
+ console.warn("[recorder] local-file thumbnail upload skipped", {
1791
+ recordingId: createdId,
1792
+ error: err instanceof Error ? err.message : String(err),
1793
+ });
1655
1794
  });
1656
- });
1795
+ }
1657
1796
  if (isStale()) throw makeAbortError("Upload cancelled");
1658
1797
  const uploadBase = `${appBasePath()}${info.uploadChunkUrl}`;
1659
1798
 
@@ -1855,7 +1994,11 @@ export default function RecordRoute() {
1855
1994
  completeUploadToast(t("recordRoute.videoUploaded"));
1856
1995
  }
1857
1996
  if (reportContext && createdId) {
1858
- const path = bugReportDonePath(createdId, reportContext);
1997
+ const path = bugReportDonePath(
1998
+ createdId,
1999
+ reportContext,
2000
+ clipIntakeRef.current,
2001
+ );
1859
2002
  await writeAppState(`navigate:${getBrowserTabId()}`, {
1860
2003
  view: "bug-report-done",
1861
2004
  recordingId: createdId,
@@ -1886,11 +2029,15 @@ export default function RecordRoute() {
1886
2029
  const preserveBufferedChunks =
1887
2030
  isStoredButUnservableFinalizeError(message);
1888
2031
  if (createdId && !serverRejectedTooLarge && !preserveBufferedChunks) {
1889
- fetch(`${appBasePath()}/api/uploads/${createdId}/abort`, {
1890
- method: "POST",
1891
- headers: { "Content-Type": "application/json" },
1892
- body: JSON.stringify({ reason: message }),
1893
- }).catch(() => {});
2032
+ fetch(
2033
+ fileUploadAbortUrlRef.current ??
2034
+ `${appBasePath()}/api/uploads/${createdId}/abort`,
2035
+ {
2036
+ method: "POST",
2037
+ headers: { "Content-Type": "application/json" },
2038
+ body: JSON.stringify({ reason: message }),
2039
+ },
2040
+ ).catch(() => {});
1894
2041
  }
1895
2042
  if (aborted || isStale()) return;
1896
2043
  setError(message);
@@ -1914,6 +2061,7 @@ export default function RecordRoute() {
1914
2061
  }
1915
2062
  if (fileUploadRecordingIdRef.current === createdId) {
1916
2063
  fileUploadRecordingIdRef.current = null;
2064
+ fileUploadAbortUrlRef.current = null;
1917
2065
  }
1918
2066
  setCompressionProgress(null);
1919
2067
  setUploadProgress(null);
@@ -2086,7 +2234,11 @@ export default function RecordRoute() {
2086
2234
  }
2087
2235
 
2088
2236
  if (reportContext) {
2089
- const path = bugReportDonePath(recordingId, reportContext);
2237
+ const path = bugReportDonePath(
2238
+ recordingId,
2239
+ reportContext,
2240
+ clipIntakeRef.current,
2241
+ );
2090
2242
  await writeAppState(`navigate:${getBrowserTabId()}`, {
2091
2243
  view: "bug-report-done",
2092
2244
  recordingId,
@@ -2310,13 +2462,16 @@ export default function RecordRoute() {
2310
2462
  countdownAudioCueRef.current?.cleanup();
2311
2463
  countdownAudioCueRef.current = null;
2312
2464
  const uploadRecordingId = fileUploadRecordingIdRef.current;
2465
+ const uploadAbortUrl = fileUploadAbortUrlRef.current;
2313
2466
  if (fileUploadAbortRef.current) {
2314
2467
  fileUploadAbortRef.current.abort(makeAbortError("Upload cancelled"));
2315
2468
  fileUploadAbortRef.current = null;
2316
2469
  }
2317
2470
  fileUploadRecordingIdRef.current = null;
2471
+ fileUploadAbortUrlRef.current = null;
2318
2472
  const engine = engineRef.current;
2319
2473
  const pendingId = pendingRef.current?.id;
2474
+ const pendingAbortUrl = pendingRef.current?.abortUrl;
2320
2475
  engineRef.current = null;
2321
2476
  pendingRef.current = null;
2322
2477
  liveTranscription.stop();
@@ -2342,11 +2497,18 @@ export default function RecordRoute() {
2342
2497
  // atomically instead: `skipIfReady` makes the trash a conditional
2343
2498
  // no-op if the row is already "ready" by the time the UPDATE runs, so a
2344
2499
  // fully saved video is never silently discarded.
2345
- fetch(agentNativePath("/_agent-native/actions/trash-recording"), {
2346
- method: "POST",
2347
- headers: { "Content-Type": "application/json" },
2348
- body: JSON.stringify({ id: pendingId, skipIfReady: true }),
2349
- }).catch(() => {});
2500
+ if (pendingAbortUrl && clipIntakeRef.current) {
2501
+ fetch(pendingAbortUrl, {
2502
+ method: "POST",
2503
+ headers: { "Content-Type": "application/json" },
2504
+ }).catch(() => {});
2505
+ } else {
2506
+ fetch(agentNativePath("/_agent-native/actions/trash-recording"), {
2507
+ method: "POST",
2508
+ headers: { "Content-Type": "application/json" },
2509
+ body: JSON.stringify({ id: pendingId, skipIfReady: true }),
2510
+ }).catch(() => {});
2511
+ }
2350
2512
  }
2351
2513
  if (uploadRecordingId) {
2352
2514
  // A local file import (as opposed to a live recording) never
@@ -2354,11 +2516,18 @@ export default function RecordRoute() {
2354
2516
  // closure. Without this, discarding mid-upload aborts the transfer
2355
2517
  // but leaves the row merely marked "failed" instead of trashed, which
2356
2518
  // contradicts the confirmation dialog's "permanently deleted" copy.
2357
- fetch(agentNativePath("/_agent-native/actions/trash-recording"), {
2358
- method: "POST",
2359
- headers: { "Content-Type": "application/json" },
2360
- body: JSON.stringify({ id: uploadRecordingId, skipIfReady: true }),
2361
- }).catch(() => {});
2519
+ if (uploadAbortUrl) {
2520
+ fetch(uploadAbortUrl, {
2521
+ method: "POST",
2522
+ headers: { "Content-Type": "application/json" },
2523
+ }).catch(() => {});
2524
+ } else {
2525
+ fetch(agentNativePath("/_agent-native/actions/trash-recording"), {
2526
+ method: "POST",
2527
+ headers: { "Content-Type": "application/json" },
2528
+ body: JSON.stringify({ id: uploadRecordingId, skipIfReady: true }),
2529
+ }).catch(() => {});
2530
+ }
2362
2531
  }
2363
2532
  setCameraStream(null);
2364
2533
  setPreviewStream(null);
@@ -220,6 +220,30 @@ export const recordings = table("recordings", {
220
220
  ...ownableColumns(),
221
221
  });
222
222
 
223
+ export const clipIntakeSessions = table(
224
+ "clips_intake_sessions",
225
+ {
226
+ id: text("id").primaryKey(),
227
+ ownerEmail: text("owner_email").notNull(),
228
+ organizationId: text("organization_id").notNull(),
229
+ recordingId: text("recording_id"),
230
+ status: text("status", {
231
+ enum: ["open", "creating", "recording", "completed", "aborted"],
232
+ })
233
+ .notNull()
234
+ .default("open"),
235
+ expiresAt: text("expires_at").notNull(),
236
+ createdAt: text("created_at").notNull().default(now()),
237
+ updatedAt: text("updated_at").notNull().default(now()),
238
+ },
239
+ (session) => ({
240
+ expiresIndex: index("clips_intake_sessions_expires_idx").on(
241
+ session.status,
242
+ session.expiresAt,
243
+ ),
244
+ }),
245
+ );
246
+
223
247
  export const recordingShares = createSharesTable("recording_shares");
224
248
 
225
249
  // -----------------------------------------------------------------------------