@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +1 -0
- package/corpus/templates/calendar/CHANGELOG.md +7 -0
- package/corpus/templates/clips/CHANGELOG.md +22 -0
- package/corpus/templates/clips/actions/create-intake-agent-link.ts +114 -0
- package/corpus/templates/clips/actions/create-intake-recording.ts +208 -0
- package/corpus/templates/clips/actions/create-recording-intake-link.ts +85 -0
- package/corpus/templates/clips/app/components/bug-report/bug-report-form.tsx +7 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +22 -5
- package/corpus/templates/clips/app/hooks/use-video-storage-status.ts +2 -1
- package/corpus/templates/clips/app/routes/bug-report.done.tsx +12 -10
- package/corpus/templates/clips/app/routes/bug-report.tsx +6 -0
- package/corpus/templates/clips/app/routes/record.tsx +269 -100
- package/corpus/templates/clips/server/db/schema.ts +24 -0
- package/corpus/templates/clips/server/lib/clip-intake.ts +247 -0
- package/corpus/templates/clips/server/plugins/auth.ts +8 -0
- package/corpus/templates/clips/server/plugins/db.ts +16 -0
- package/corpus/templates/clips/server/routes/api/clip-intake.get.ts +61 -0
- package/corpus/templates/clips/server/routes/api/clip-intake.post.ts +90 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +19 -4
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +25 -10
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +21 -4
- package/corpus/templates/clips/shared/clip-intake.ts +41 -0
- package/corpus/templates/clips/shared/finalize-recovery.ts +27 -5
- package/corpus/templates/clips/shared/recording-core.ts +4 -4
- package/corpus/templates/content/CHANGELOG.md +1 -0
- package/corpus/templates/design/CHANGELOG.md +9 -4
- package/corpus/templates/factory/CHANGELOG.md +11 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/docs/content/template-clips-anonymous-intake.mdx +148 -0
- package/docs/content/template-clips-developers.mdx +4 -3
- package/docs/content/template-clips.mdx +1 -0
- 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(
|
|
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
|
|
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
|
|
1249
|
-
if (
|
|
1250
|
-
await
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
|
1503
|
-
if (
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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
|
|
1589
|
-
agentNativePath(
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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(
|
|
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(
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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(
|
|
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
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
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
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
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
|
// -----------------------------------------------------------------------------
|