@agent-native/core 0.128.1 → 0.128.3

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 (84) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +2 -1
  4. package/corpus/core/src/server/email-template.ts +33 -0
  5. package/corpus/templates/chat/_gitignore +2 -0
  6. package/corpus/templates/clips/AGENTS.md +6 -1
  7. package/corpus/templates/clips/actions/complete-transactional-email-summary.ts +56 -0
  8. package/corpus/templates/clips/actions/import-loom-recording.ts +258 -76
  9. package/corpus/templates/clips/actions/lib/direct-video.ts +96 -0
  10. package/corpus/templates/clips/actions/lib/loom-import-job.ts +53 -2
  11. package/corpus/templates/clips/actions/lib/loom-video.ts +2 -62
  12. package/corpus/templates/clips/actions/lib/video-download-limits.ts +62 -0
  13. package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +249 -0
  14. package/corpus/templates/clips/app/components/library/library-grid.tsx +65 -0
  15. package/corpus/templates/clips/app/components/library/library-layout.tsx +55 -0
  16. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +28 -126
  17. package/corpus/templates/clips/app/global.css +23 -0
  18. package/corpus/templates/clips/app/hooks/use-transactional-email-bridge.ts +98 -0
  19. package/corpus/templates/clips/app/i18n/en-US.ts +20 -0
  20. package/corpus/templates/clips/app/routes/_app.tsx +2 -0
  21. package/corpus/templates/clips/app/routes/import.tsx +396 -0
  22. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +8 -6
  23. package/corpus/templates/clips/app/routes/record.tsx +13 -71
  24. package/corpus/templates/clips/changelog/2026-07-25-two-clip-emails-now-include-a-concise-summary-generated-secu.md +6 -0
  25. package/corpus/templates/clips/data/clips-transactional-emails/config.json +3 -0
  26. package/corpus/templates/clips/server/jobs/transactional-emails.ts +1034 -0
  27. package/corpus/templates/clips/server/lib/transactional-email-store.ts +865 -0
  28. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +270 -0
  29. package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
  30. package/corpus/templates/clips/server/routes/api/view-event.post.ts +59 -2
  31. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
  32. package/corpus/templates/content/AGENTS.md +8 -0
  33. package/corpus/templates/content/actions/_content-database-personal-view.ts +28 -3
  34. package/corpus/templates/content/actions/_property-utils.ts +32 -2
  35. package/corpus/templates/content/actions/configure-document-property.ts +12 -2
  36. package/corpus/templates/content/actions/delete-document-property.ts +13 -3
  37. package/corpus/templates/content/actions/duplicate-document-property.ts +13 -3
  38. package/corpus/templates/content/actions/list-content-spaces.ts +5 -2
  39. package/corpus/templates/content/actions/list-document-properties.ts +12 -3
  40. package/corpus/templates/content/actions/move-database-item.ts +74 -55
  41. package/corpus/templates/content/actions/reorder-document-property.ts +20 -4
  42. package/corpus/templates/content/actions/set-document-property.ts +10 -1
  43. package/corpus/templates/content/actions/update-content-database-personal-view.ts +7 -0
  44. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +31 -21
  45. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +30 -3
  46. package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +8 -1
  47. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +72 -20
  48. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +79 -10
  49. package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +8 -1
  50. package/corpus/templates/content/app/components/editor/database/settings.tsx +6 -0
  51. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +246 -26
  52. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +491 -197
  53. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +3 -3
  54. package/corpus/templates/content/app/components/sidebar/sidebar-reorder.tsx +409 -0
  55. package/corpus/templates/content/app/hooks/use-content-database.ts +117 -3
  56. package/corpus/templates/content/app/hooks/use-content-spaces.ts +1 -0
  57. package/corpus/templates/content/app/hooks/use-document-properties.ts +62 -18
  58. package/corpus/templates/content/app/hooks/use-documents.ts +13 -5
  59. package/corpus/templates/content/app/i18n-data.ts +261 -3
  60. package/corpus/templates/content/app/routes/_app.page.$id.tsx +9 -2
  61. package/corpus/templates/content/changelog/2026-07-24-database-page-properties-keep-their-context.md +6 -0
  62. package/corpus/templates/content/changelog/2026-07-26-sidebar-references-can-now-be-personally-reordered.md +6 -0
  63. package/corpus/templates/content/docs/solutions/2026-07-24-database-property-context-switch-diagnosis.md +249 -0
  64. package/corpus/templates/content/docs/solutions/content-sidebar-order-shape.md +389 -0
  65. package/corpus/templates/content/shared/api.ts +28 -0
  66. package/corpus/toolkit/CHANGELOG.md +6 -0
  67. package/corpus/toolkit/package.json +1 -1
  68. package/corpus/toolkit/src/editor/DragHandle.ts +63 -9
  69. package/dist/collab/struct-routes.d.ts +1 -1
  70. package/dist/server/email-template.d.ts +7 -0
  71. package/dist/server/email-template.d.ts.map +1 -1
  72. package/dist/server/email-template.js +24 -0
  73. package/dist/server/email-template.js.map +1 -1
  74. package/dist/server/realtime-token.d.ts +1 -1
  75. package/dist/templates/chat/_gitignore +2 -0
  76. package/package.json +3 -2
  77. package/src/server/email-template.ts +33 -0
  78. package/src/templates/chat/_gitignore +2 -0
  79. package/corpus/core/src/cli/sync-builder-starter-manifest.ts +0 -631
  80. package/dist/cli/sync-builder-starter-manifest.d.ts +0 -79
  81. package/dist/cli/sync-builder-starter-manifest.d.ts.map +0 -1
  82. package/dist/cli/sync-builder-starter-manifest.js +0 -426
  83. package/dist/cli/sync-builder-starter-manifest.js.map +0 -1
  84. package/src/cli/sync-builder-starter-manifest.ts +0 -631
@@ -0,0 +1,396 @@
1
+ import {
2
+ agentNativePath,
3
+ appBasePath,
4
+ } from "@agent-native/core/client/api-path";
5
+ import { callAction } from "@agent-native/core/client/hooks";
6
+ import { useT } from "@agent-native/core/client/i18n";
7
+ import {
8
+ IconArrowLeft,
9
+ IconCheck,
10
+ IconLink,
11
+ IconSparkles,
12
+ IconUpload,
13
+ IconVideo,
14
+ } from "@tabler/icons-react";
15
+ import { useQueryClient } from "@tanstack/react-query";
16
+ import {
17
+ useCallback,
18
+ useEffect,
19
+ useMemo,
20
+ useRef,
21
+ useState,
22
+ type FormEvent,
23
+ } from "react";
24
+ import { Link, useLocation, useNavigate } from "react-router";
25
+ import { toast } from "sonner";
26
+
27
+ import { StorageSetupCard } from "@/components/recorder/storage-setup-card";
28
+ import { Button } from "@/components/ui/button";
29
+ import { Input } from "@/components/ui/input";
30
+ import { Skeleton } from "@/components/ui/skeleton";
31
+ import {
32
+ VIDEO_STORAGE_STATUS_KEY,
33
+ useVideoStorageStatus,
34
+ type VideoStorageStatus,
35
+ } from "@/hooks/use-video-storage-status";
36
+ import enMessages from "@/i18n/en-US";
37
+ import { cn } from "@/lib/utils";
38
+
39
+ type ImportPhase = "form" | "importing" | "done" | "leaving";
40
+
41
+ export function meta() {
42
+ return [{ title: enMessages.importRoute.pageTitle }];
43
+ }
44
+
45
+ function recordingLink(recordingId: string): string {
46
+ const path = `${appBasePath()}/r/${encodeURIComponent(recordingId)}`;
47
+ if (typeof window === "undefined") return path;
48
+ return new URL(path, window.location.origin).toString();
49
+ }
50
+
51
+ async function copyRecordingLink(recordingId: string): Promise<void> {
52
+ if (typeof navigator === "undefined") return;
53
+ if (!navigator.clipboard?.writeText) return;
54
+ await navigator.clipboard
55
+ .writeText(recordingLink(recordingId))
56
+ .catch(() => undefined);
57
+ }
58
+
59
+ async function writeNavigateAppState(recordingId: string): Promise<void> {
60
+ await fetch(agentNativePath("/_agent-native/application-state/navigate"), {
61
+ method: "PUT",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({ view: "recording", recordingId }),
64
+ }).catch(() => {});
65
+ }
66
+
67
+ function userFacingActionErrorMessage(error: string): string {
68
+ return error.replace(/^Action [a-z0-9-]+ failed:\s*/i, "").trim() || error;
69
+ }
70
+
71
+ function ImportPanelSkeleton() {
72
+ return (
73
+ <div className="mx-auto w-full max-w-lg overflow-hidden rounded-2xl border border-border bg-card p-6 shadow-lg">
74
+ <Skeleton className="h-12 w-full rounded-lg" />
75
+ <Skeleton className="mt-3 h-12 w-full rounded-lg" />
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export default function ImportRoute() {
81
+ const t = useT();
82
+ const navigate = useNavigate();
83
+ const location = useLocation();
84
+ const queryClient = useQueryClient();
85
+ const storageQuery = useVideoStorageStatus();
86
+ const storageConfigured: boolean | null = storageQuery.isLoading
87
+ ? null
88
+ : !!storageQuery.data?.configured;
89
+
90
+ const spaceIdFromUrl = useMemo(() => {
91
+ const params = new URLSearchParams(location.search);
92
+ return params.get("spaceId") || null;
93
+ }, [location.search]);
94
+ const folderIdFromUrl = useMemo(() => {
95
+ const params = new URLSearchParams(location.search);
96
+ return params.get("folderId") || null;
97
+ }, [location.search]);
98
+
99
+ const recordHref = useMemo(() => {
100
+ const params = new URLSearchParams();
101
+ if (spaceIdFromUrl) params.set("spaceId", spaceIdFromUrl);
102
+ if (folderIdFromUrl) params.set("folderId", folderIdFromUrl);
103
+ const qs = params.toString();
104
+ return qs ? `/record?${qs}` : "/record";
105
+ }, [spaceIdFromUrl, folderIdFromUrl]);
106
+
107
+ const uploadHref = useMemo(() => {
108
+ const params = new URLSearchParams();
109
+ if (spaceIdFromUrl) params.set("spaceId", spaceIdFromUrl);
110
+ if (folderIdFromUrl) params.set("folderId", folderIdFromUrl);
111
+ params.set("autoUpload", "1");
112
+ return `/record?${params.toString()}`;
113
+ }, [spaceIdFromUrl, folderIdFromUrl]);
114
+
115
+ const [loomUrl, setLoomUrl] = useState("");
116
+ const [phase, setPhase] = useState<ImportPhase>("form");
117
+ const [loomError, setLoomError] = useState<string | null>(null);
118
+ const [stageIndex, setStageIndex] = useState(0);
119
+ const [progress, setProgress] = useState(0);
120
+ const stageTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
121
+ const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
122
+ const timeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
123
+
124
+ const busy = phase !== "form";
125
+
126
+ const importStages = useMemo(
127
+ () => [
128
+ t("importRoute.stageFetching"),
129
+ t("importRoute.stageUploading"),
130
+ t("importRoute.stageTranscript"),
131
+ t("importRoute.stageFinalizing"),
132
+ ],
133
+ [t],
134
+ );
135
+
136
+ const benefits = useMemo(
137
+ () => [
138
+ t("importRoute.benefitTranscript"),
139
+ t("importRoute.benefitQueryable"),
140
+ t("importRoute.benefitSummaries"),
141
+ t("importRoute.benefitPrimitive"),
142
+ ],
143
+ [t],
144
+ );
145
+
146
+ const clearTimers = useCallback(() => {
147
+ if (stageTimerRef.current) clearInterval(stageTimerRef.current);
148
+ if (progressTimerRef.current) clearInterval(progressTimerRef.current);
149
+ stageTimerRef.current = null;
150
+ progressTimerRef.current = null;
151
+ }, []);
152
+
153
+ useEffect(() => {
154
+ return () => {
155
+ clearTimers();
156
+ timeoutsRef.current.forEach((id) => clearTimeout(id));
157
+ };
158
+ }, [clearTimers]);
159
+
160
+ const handleSubmit = useCallback(
161
+ async (event: FormEvent<HTMLFormElement>) => {
162
+ event.preventDefault();
163
+ const url = loomUrl.trim();
164
+ if (!url || busy) return;
165
+
166
+ setLoomError(null);
167
+ setPhase("importing");
168
+ setStageIndex(0);
169
+ setProgress(6);
170
+ stageTimerRef.current = setInterval(() => {
171
+ setStageIndex((prev) => Math.min(prev + 1, importStages.length - 1));
172
+ }, 1300);
173
+ // Ease the accent bar toward ~92% while the request is in flight; the
174
+ // real completion snaps it to 100%.
175
+ progressTimerRef.current = setInterval(() => {
176
+ setProgress((p) => (p >= 92 ? p : p + Math.max(0.6, (92 - p) * 0.07)));
177
+ }, 120);
178
+
179
+ try {
180
+ const result = (await callAction(
181
+ "import-loom-recording" as any,
182
+ {
183
+ url,
184
+ spaceIds: spaceIdFromUrl ? [spaceIdFromUrl] : undefined,
185
+ folderId: folderIdFromUrl ?? undefined,
186
+ } as any,
187
+ )) as {
188
+ recordingId?: string;
189
+ status?: string;
190
+ storageSetupRequired?: boolean;
191
+ };
192
+ const recordingId = result?.recordingId;
193
+ if (!recordingId) {
194
+ throw new Error("Loom import did not return a recording id.");
195
+ }
196
+
197
+ clearTimers();
198
+ setStageIndex(importStages.length - 1);
199
+ setProgress(100);
200
+
201
+ if (
202
+ result?.storageSetupRequired ||
203
+ result?.status === "waiting_storage"
204
+ ) {
205
+ toast.info(t("recordRoute.storageNeededToFinishLoomImport"), {
206
+ description: t("recordRoute.connectStorageToRetryLoom"),
207
+ duration: 12_000,
208
+ });
209
+ await writeNavigateAppState(recordingId);
210
+ navigate(`/r/${recordingId}`);
211
+ return;
212
+ }
213
+
214
+ await copyRecordingLink(recordingId);
215
+ await writeNavigateAppState(recordingId);
216
+
217
+ // Linger on the "done" reveal, then fade out into the clip.
218
+ setPhase("done");
219
+ timeoutsRef.current.push(
220
+ setTimeout(() => setPhase("leaving"), 1900),
221
+ setTimeout(() => navigate(`/r/${recordingId}`), 2300),
222
+ );
223
+ } catch (err) {
224
+ clearTimers();
225
+ setProgress(0);
226
+ setPhase("form");
227
+ setLoomError(
228
+ err instanceof Error
229
+ ? userFacingActionErrorMessage(err.message)
230
+ : t("recordRoute.couldNotImportLoom"),
231
+ );
232
+ }
233
+ },
234
+ [
235
+ busy,
236
+ clearTimers,
237
+ folderIdFromUrl,
238
+ importStages.length,
239
+ loomUrl,
240
+ navigate,
241
+ spaceIdFromUrl,
242
+ t,
243
+ ],
244
+ );
245
+
246
+ const markStorageConfigured = useCallback(() => {
247
+ queryClient.setQueryData<VideoStorageStatus>(
248
+ VIDEO_STORAGE_STATUS_KEY,
249
+ (prev) => ({
250
+ configured: true,
251
+ activeProvider: prev?.activeProvider ?? null,
252
+ builderConfigured: prev?.builderConfigured ?? false,
253
+ }),
254
+ );
255
+ }, [queryClient]);
256
+
257
+ return (
258
+ <div className="relative min-h-screen bg-background">
259
+ <button
260
+ type="button"
261
+ aria-label={t("recordRoute.backToLibrary")}
262
+ onClick={() => navigate("/library")}
263
+ className="fixed start-4 top-4 z-30 inline-flex h-9 w-9 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
264
+ >
265
+ <IconArrowLeft className="h-5 w-5 rtl:-scale-x-100" />
266
+ </button>
267
+
268
+ <div className="flex min-h-screen flex-col items-center justify-center px-4 py-10">
269
+ <div className="mb-3 flex items-center gap-2 text-primary">
270
+ <IconLink className="h-6 w-6" />
271
+ <span className="text-sm font-medium uppercase tracking-wide">
272
+ {t("importRoute.title")}
273
+ </span>
274
+ </div>
275
+ <p className="mb-6 max-w-md text-center text-sm text-muted-foreground">
276
+ {t("importRoute.helperText")}
277
+ </p>
278
+
279
+ <div className="mx-auto w-full max-w-lg">
280
+ {storageConfigured === null ? (
281
+ <ImportPanelSkeleton />
282
+ ) : storageConfigured ? (
283
+ <div
284
+ className={cn(
285
+ "overflow-hidden rounded-2xl border border-border bg-card shadow-lg transition-opacity duration-300",
286
+ phase === "leaving" && "opacity-0",
287
+ )}
288
+ >
289
+ {busy ? (
290
+ <div className="h-1 w-full overflow-hidden bg-muted">
291
+ <div
292
+ className="h-full bg-primary transition-[width] duration-200 ease-out"
293
+ style={{ width: `${Math.min(100, progress)}%` }}
294
+ />
295
+ </div>
296
+ ) : null}
297
+ <div className="p-6">
298
+ {phase === "importing" ? (
299
+ <div className="flex flex-col items-center gap-2 py-8 text-center">
300
+ <IconSparkles className="h-6 w-6 animate-pulse text-primary" />
301
+ <p
302
+ key={stageIndex}
303
+ className="text-base font-semibold text-foreground"
304
+ style={{ animation: "clips-benefit-in 260ms ease-out" }}
305
+ >
306
+ {importStages[stageIndex]}
307
+ </p>
308
+ <p className="max-w-xs text-xs text-muted-foreground">
309
+ {t("importRoute.importingSubtitle")}
310
+ </p>
311
+ </div>
312
+ ) : phase === "done" || phase === "leaving" ? (
313
+ <div className="flex flex-col items-center gap-4 py-6 text-center">
314
+ <div className="flex items-center gap-2 text-primary">
315
+ <IconSparkles className="h-6 w-6" />
316
+ <p className="text-base font-semibold text-foreground">
317
+ {t("importRoute.doneHeading")}
318
+ </p>
319
+ </div>
320
+ <ul className="flex flex-col items-start gap-2 text-start">
321
+ {benefits.map((benefit, index) => (
322
+ <li
323
+ key={benefit}
324
+ className="flex items-center gap-2 text-sm font-medium text-foreground"
325
+ style={{
326
+ animation: "clips-benefit-in 220ms ease-out both",
327
+ animationDelay: `${index * 110}ms`,
328
+ }}
329
+ >
330
+ <IconCheck className="h-4 w-4 shrink-0 text-primary" />
331
+ {benefit}
332
+ </li>
333
+ ))}
334
+ </ul>
335
+ </div>
336
+ ) : (
337
+ <form onSubmit={handleSubmit} className="flex flex-col gap-3">
338
+ <Input
339
+ autoFocus
340
+ value={loomUrl}
341
+ onChange={(event) => {
342
+ setLoomUrl(event.target.value);
343
+ setLoomError(null);
344
+ }}
345
+ placeholder={t("importRoute.urlPlaceholder")}
346
+ className="h-12 text-base"
347
+ inputMode="url"
348
+ />
349
+ <Button
350
+ type="submit"
351
+ className="h-12 w-full gap-2"
352
+ disabled={!loomUrl.trim()}
353
+ >
354
+ <IconLink className="h-4 w-4" />
355
+ {t("importRoute.cta")}
356
+ </Button>
357
+ {loomError ? (
358
+ <p className="text-xs leading-relaxed text-destructive">
359
+ {loomError}
360
+ </p>
361
+ ) : null}
362
+ </form>
363
+ )}
364
+ </div>
365
+
366
+ {!busy ? (
367
+ <div className="flex items-center justify-center gap-4 border-t border-border px-6 py-4">
368
+ <Link
369
+ to={uploadHref}
370
+ className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground"
371
+ >
372
+ <IconUpload className="h-3.5 w-3.5" />
373
+ {t("preRecord.uploadVideo")}
374
+ </Link>
375
+ <Link
376
+ to={recordHref}
377
+ className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground"
378
+ >
379
+ <IconVideo className="h-3.5 w-3.5" />
380
+ {t("preRecord.recordNew")}
381
+ </Link>
382
+ </div>
383
+ ) : null}
384
+ </div>
385
+ ) : (
386
+ <StorageSetupCard
387
+ onConfigured={markStorageConfigured}
388
+ connectSource="clips_import_storage_setup_card"
389
+ connectFlow="import"
390
+ />
391
+ )}
392
+ </div>
393
+ </div>
394
+ </div>
395
+ );
396
+ }
@@ -552,18 +552,20 @@ export default function RecordingPage() {
552
552
  setRetryingFinalize(true);
553
553
  setProcessingTimeout(false);
554
554
  try {
555
- const retryingLoomImport = isLoomRecording;
556
- const actionPath = retryingLoomImport
555
+ const isUrlImportRetry =
556
+ isLoomRecording ||
557
+ recording?.sourceAppName?.trim().toLowerCase() === "video link";
558
+ const actionPath = isUrlImportRetry
557
559
  ? "/_agent-native/actions/import-loom-recording"
558
560
  : "/_agent-native/actions/finalize-recording";
559
- if (retryingLoomImport && !recording?.sourceWindowTitle) {
561
+ if (isUrlImportRetry && !recording?.sourceWindowTitle) {
560
562
  throw new Error(t("recordingPage.loomMissingUrl"));
561
563
  }
562
564
  const res = await fetch(agentNativePath(actionPath), {
563
565
  method: "POST",
564
566
  headers: { "Content-Type": "application/json" },
565
567
  body: JSON.stringify(
566
- retryingLoomImport
568
+ isUrlImportRetry
567
569
  ? {
568
570
  recordingId,
569
571
  url: recording?.sourceWindowTitle,
@@ -593,14 +595,14 @@ export default function RecordingPage() {
593
595
  });
594
596
  return;
595
597
  }
596
- if (retryingLoomImport && result?.status === "processing") {
598
+ if (isUrlImportRetry && result?.status === "processing") {
597
599
  // Download + reupload now run as a background job; this request only
598
600
  // confirms the retry was accepted, not that the clip is ready yet.
599
601
  toast.info(t("recordingPage.importingLoom"));
600
602
  return;
601
603
  }
602
604
  toast.success(
603
- retryingLoomImport
605
+ isUrlImportRetry
604
606
  ? t("recordingPage.loomImportResumed")
605
607
  : t("recordingPage.clipUploadResumed"),
606
608
  );
@@ -522,10 +522,6 @@ function friendlyRecordingErrorMessage(error: string): string {
522
522
  return error;
523
523
  }
524
524
 
525
- function userFacingActionErrorMessage(error: string): string {
526
- return error.replace(/^Action [a-z0-9-]+ failed:\s*/i, "").trim() || error;
527
- }
528
-
529
525
  interface PendingRecording {
530
526
  id: string;
531
527
  uploadChunkUrl: string;
@@ -804,7 +800,6 @@ export default function RecordRoute() {
804
800
  // the live camera bubble is hidden during full-screen recording.
805
801
  const [resolvedDisplaySurface, setResolvedDisplaySurface] =
806
802
  useState<DisplaySurface | null>(null);
807
- const [loomImporting, setLoomImporting] = useState(false);
808
803
  const [recordingMode, setRecordingMode] =
809
804
  useState<RecordingMode>("screen+camera");
810
805
  // Surfaced during the post-stop compression pass so the spinner can show
@@ -834,6 +829,17 @@ export default function RecordRoute() {
834
829
  const params = new URLSearchParams(location.search);
835
830
  return params.get("folderId") || null;
836
831
  }, [location.search]);
832
+ const autoOpenUploadFromUrl = useMemo(() => {
833
+ const params = new URLSearchParams(location.search);
834
+ return params.get("autoUpload") === "1";
835
+ }, [location.search]);
836
+ const importLoomHref = useMemo(() => {
837
+ const params = new URLSearchParams();
838
+ if (spaceIdFromUrl) params.set("spaceId", spaceIdFromUrl);
839
+ if (folderIdFromUrl) params.set("folderId", folderIdFromUrl);
840
+ const qs = params.toString();
841
+ return qs ? `/import?${qs}` : "/import";
842
+ }, [spaceIdFromUrl, folderIdFromUrl]);
837
843
  const storageConfigured: boolean | null = storageQuery.isLoading
838
844
  ? null
839
845
  : !!storageQuery.data?.configured;
@@ -1816,70 +1822,6 @@ export default function RecordRoute() {
1816
1822
  [markStorageConfigured, navigate, probeVideoMetadata, showSavedToast],
1817
1823
  );
1818
1824
 
1819
- const importLoom = useCallback(
1820
- async (url: string) => {
1821
- startSessionRef.current += 1;
1822
- fileUploadAbortRef.current?.abort(makeAbortError("Upload cancelled"));
1823
- fileUploadAbortRef.current = null;
1824
- setError(null);
1825
- setLoomImporting(true);
1826
-
1827
- try {
1828
- const result = (await callAction(
1829
- "import-loom-recording" as any,
1830
- {
1831
- url,
1832
- spaceIds: spaceIdFromUrl ? [spaceIdFromUrl] : undefined,
1833
- folderId: folderIdFromUrl ?? undefined,
1834
- } as any,
1835
- )) as {
1836
- recordingId?: string;
1837
- status?: string;
1838
- storageSetupRequired?: boolean;
1839
- };
1840
- const recordingId = result?.recordingId;
1841
- if (!recordingId) {
1842
- throw new Error("Loom import did not return a recording id.");
1843
- }
1844
-
1845
- if (
1846
- result?.storageSetupRequired ||
1847
- result?.status === "waiting_storage"
1848
- ) {
1849
- toast.info(t("recordRoute.storageNeededToFinishLoomImport"), {
1850
- description: t("recordRoute.connectStorageToRetryLoom"),
1851
- duration: 12_000,
1852
- });
1853
- } else if (result?.status === "processing") {
1854
- // The video download + reupload run as a background job (see
1855
- // import-loom-recording.ts) so this request stays fast regardless of
1856
- // Loom video length; the recording page polls until it is ready.
1857
- toast.info(t("recordingPage.importingLoom"));
1858
- } else {
1859
- showSavedToast(
1860
- t("recordRoute.loomImported"),
1861
- await copyRecordingShareLink(recordingId),
1862
- recordingId,
1863
- );
1864
- }
1865
- await writeAppState("navigate", {
1866
- view: "recording",
1867
- recordingId,
1868
- }).catch(() => {});
1869
- navigate(`/r/${recordingId}`);
1870
- } catch (err) {
1871
- throw new Error(
1872
- err instanceof Error
1873
- ? userFacingActionErrorMessage(err.message)
1874
- : t("recordRoute.couldNotImportLoom"),
1875
- );
1876
- } finally {
1877
- setLoomImporting(false);
1878
- }
1879
- },
1880
- [folderIdFromUrl, navigate, spaceIdFromUrl, showSavedToast],
1881
- );
1882
-
1883
1825
  const saveBrowserDiagnostics = useCallback(
1884
1826
  async (recordingId: string) => {
1885
1827
  const capture = browserDiagnosticsRef.current;
@@ -2547,10 +2489,10 @@ export default function RecordRoute() {
2547
2489
  initialRecorderOptions.surface
2548
2490
  }
2549
2491
  onUpload={uploadFile}
2550
- onImportLoom={importLoom}
2551
- importingLoom={loomImporting}
2492
+ importLoomHref={importLoomHref}
2552
2493
  cameraSize={cameraSize}
2553
2494
  onCameraSizeChange={handleCameraSizeChange}
2495
+ autoOpenUpload={autoOpenUploadFromUrl}
2554
2496
  />
2555
2497
  ) : (
2556
2498
  <StorageSetupCard
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ Two-Clip emails now include a concise summary generated securely through your Agent Chat.
@@ -0,0 +1,3 @@
1
+ {
2
+ "enabledAt": "2026-07-25T17:29:40.915Z"
3
+ }