@agent-native/core 0.128.0 → 0.128.2
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/core/CHANGELOG.md +27 -0
- package/corpus/core/package.json +3 -1
- package/corpus/core/src/server/builder-design-systems.ts +299 -82
- package/corpus/core/src/server/email-template.ts +33 -0
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/templates/analytics/_gitignore +2 -0
- package/corpus/templates/assets/_gitignore +2 -0
- package/corpus/templates/calendar/_gitignore +2 -0
- package/corpus/templates/chat/_gitignore +2 -0
- package/corpus/templates/clips/AGENTS.md +6 -1
- package/corpus/templates/clips/_gitignore +2 -0
- package/corpus/templates/clips/actions/complete-transactional-email-summary.ts +56 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +258 -76
- package/corpus/templates/clips/actions/lib/direct-video.ts +96 -0
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +53 -2
- package/corpus/templates/clips/actions/lib/loom-video.ts +2 -62
- package/corpus/templates/clips/actions/lib/video-download-limits.ts +62 -0
- package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +249 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +65 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +55 -0
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +28 -126
- package/corpus/templates/clips/app/global.css +23 -0
- package/corpus/templates/clips/app/hooks/use-transactional-email-bridge.ts +98 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +20 -0
- package/corpus/templates/clips/app/routes/_app.tsx +2 -0
- package/corpus/templates/clips/app/routes/import.tsx +396 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +8 -6
- package/corpus/templates/clips/app/routes/record.tsx +13 -71
- package/corpus/templates/clips/changelog/2026-07-25-two-clip-emails-now-include-a-concise-summary-generated-secu.md +6 -0
- package/corpus/templates/clips/data/clips-transactional-emails/config.json +3 -0
- package/corpus/templates/clips/server/jobs/transactional-emails.ts +1034 -0
- package/corpus/templates/clips/server/lib/transactional-email-store.ts +865 -0
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +270 -0
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
- package/corpus/templates/clips/server/routes/api/view-event.post.ts +59 -2
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
- package/corpus/templates/content/AGENTS.md +8 -0
- package/corpus/templates/content/_gitignore +2 -0
- package/corpus/templates/content/actions/_content-database-personal-view.ts +28 -3
- package/corpus/templates/content/actions/_property-utils.ts +32 -2
- package/corpus/templates/content/actions/configure-document-property.ts +12 -2
- package/corpus/templates/content/actions/delete-document-property.ts +13 -3
- package/corpus/templates/content/actions/duplicate-document-property.ts +13 -3
- package/corpus/templates/content/actions/list-content-spaces.ts +5 -2
- package/corpus/templates/content/actions/list-document-properties.ts +12 -3
- package/corpus/templates/content/actions/move-database-item.ts +74 -55
- package/corpus/templates/content/actions/reorder-document-property.ts +20 -4
- package/corpus/templates/content/actions/set-document-property.ts +10 -1
- package/corpus/templates/content/actions/update-content-database-personal-view.ts +7 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +31 -21
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +30 -3
- package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +8 -1
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +72 -20
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +79 -10
- package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +8 -1
- package/corpus/templates/content/app/components/editor/database/settings.tsx +6 -0
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +246 -26
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +491 -197
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +3 -3
- package/corpus/templates/content/app/components/sidebar/sidebar-reorder.tsx +409 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +117 -3
- package/corpus/templates/content/app/hooks/use-content-spaces.ts +1 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +62 -18
- package/corpus/templates/content/app/hooks/use-documents.ts +13 -5
- package/corpus/templates/content/app/i18n-data.ts +261 -3
- package/corpus/templates/content/app/routes/_app.page.$id.tsx +9 -2
- package/corpus/templates/content/changelog/2026-07-24-database-page-properties-keep-their-context.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-sidebar-references-can-now-be-personally-reordered.md +6 -0
- package/corpus/templates/content/docs/solutions/2026-07-24-database-property-context-switch-diagnosis.md +249 -0
- package/corpus/templates/content/docs/solutions/content-sidebar-order-shape.md +389 -0
- package/corpus/templates/content/shared/api.ts +28 -0
- package/corpus/templates/design/_gitignore +2 -0
- package/corpus/templates/design/app/i18n-data.ts +10 -0
- package/corpus/templates/design/app/lib/builder-design-system-upload.ts +280 -0
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +90 -54
- package/corpus/templates/design/changelog/2026-07-28-figma-fig-uploads-now-show-indexing-progress-and-an-open-in-.md +6 -0
- package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +49 -0
- package/corpus/templates/design/server/handlers/design-system-upload-start.ts +87 -0
- package/corpus/templates/design/server/handlers/index-design-system-sources.ts +82 -0
- package/corpus/templates/design/server/routes/api/design-system-decode-job-status.get.ts +1 -0
- package/corpus/templates/design/server/routes/api/design-system-upload-start.post.ts +1 -0
- package/corpus/templates/design/server/routes/api/index-design-system-sources.post.ts +1 -0
- package/corpus/templates/dispatch/_gitignore +2 -0
- package/corpus/templates/forms/_gitignore +2 -0
- package/corpus/templates/macros/_gitignore +2 -0
- package/corpus/templates/mail/_gitignore +2 -0
- package/corpus/templates/plan/_gitignore +2 -0
- package/corpus/templates/slides/_gitignore +2 -0
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +126 -53
- package/corpus/templates/slides/app/components/design-system/builder-design-system-upload.ts +271 -0
- package/corpus/templates/slides/app/components/design-system/builder-index-response.ts +1 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +49 -0
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +87 -0
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +79 -0
- package/corpus/templates/slides/server/routes/api/design-system-decode-job-status.get.ts +1 -0
- package/corpus/templates/slides/server/routes/api/design-system-upload-start.post.ts +1 -0
- package/corpus/templates/slides/server/routes/api/index-design-system-sources.post.ts +1 -0
- package/corpus/templates/tasks/_gitignore +2 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/DragHandle.ts +63 -9
- package/dist/collab/routes.d.ts +1 -1
- 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/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/server/builder-design-systems.d.ts +49 -0
- package/dist/server/builder-design-systems.d.ts.map +1 -1
- package/dist/server/builder-design-systems.js +221 -69
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/email-template.d.ts +7 -0
- package/dist/server/email-template.d.ts.map +1 -1
- package/dist/server/email-template.js +24 -0
- package/dist/server/email-template.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/_gitignore +2 -0
- package/package.json +5 -3
- package/src/server/builder-design-systems.ts +299 -82
- package/src/server/email-template.ts +33 -0
- package/src/server/index.ts +8 -0
- package/src/templates/chat/_gitignore +2 -0
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +0 -151
- package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +0 -1
- package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +0 -119
- package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +0 -1
|
@@ -44,6 +44,11 @@ Read the matching skill before deeper work in that area:
|
|
|
44
44
|
activation. Navigate the user to the recording view instead of a server action.
|
|
45
45
|
- Native transcript first; cloud transcription is fallback-only. Never hide a
|
|
46
46
|
usable native transcript behind failed metadata work.
|
|
47
|
+
- Use `import-loom-recording` for Loom or direct MP4/WebM URLs. Loom media and
|
|
48
|
+
public transcripts import in the background; direct videos need
|
|
49
|
+
`request-transcript` afterward.
|
|
50
|
+
- Internal transactional-email actions claim bounded two-Clip summary work and
|
|
51
|
+
complete it with one plain-text sentence after reviewing both context packets.
|
|
47
52
|
- The `view-screen` transcript is a bounded preview: when `previewTruncated` is
|
|
48
53
|
true it may end mid-sentence and says nothing about where transcription
|
|
49
54
|
ended. Call `get-recording-player-data` before judging completeness or
|
|
@@ -82,7 +87,7 @@ Read the matching skill before deeper work in that area:
|
|
|
82
87
|
| `list-recordings`, `search-recordings` | Library, trash, `--view=shared` |
|
|
83
88
|
| `get-recording-player-data` | Full transcript, chapters, diagnostics |
|
|
84
89
|
| `create-recording`, `finalize-recording` | Create row; finish upload |
|
|
85
|
-
| `import-loom-recording` | Import
|
|
90
|
+
| `import-loom-recording` | Import Loom or direct MP4/WebM URL |
|
|
86
91
|
| `update-recording` | Title, password, expiry, visibility |
|
|
87
92
|
| `move-recording` | Move `id` or `ids` to a folder or root |
|
|
88
93
|
| `archive-`, `trash-`, `restore-recording` | Lifecycle |
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { getCurrentOwnerEmail } from "../server/lib/recordings.js";
|
|
5
|
+
import { transactionalEmailStore } from "../server/lib/transactional-email-store.js";
|
|
6
|
+
|
|
7
|
+
export const MAX_TRANSACTIONAL_EMAIL_SUMMARY_LENGTH = 320;
|
|
8
|
+
|
|
9
|
+
export function normalizeTransactionalEmailSummary(value: string): string {
|
|
10
|
+
return value.replace(/\s+/g, " ").trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateTransactionalEmailSummary(value: string): string {
|
|
14
|
+
const summary = normalizeTransactionalEmailSummary(value);
|
|
15
|
+
if (!summary) throw new Error("Summary must be one nonempty sentence.");
|
|
16
|
+
if (summary.length > MAX_TRANSACTIONAL_EMAIL_SUMMARY_LENGTH) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Summary must be at most ${MAX_TRANSACTIONAL_EMAIL_SUMMARY_LENGTH} characters.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (/[<>]/.test(summary)) {
|
|
22
|
+
throw new Error("Summary must be plain text without HTML.");
|
|
23
|
+
}
|
|
24
|
+
const sentenceEndings = summary.match(/[.!?](?=\s|$)/g) ?? [];
|
|
25
|
+
if (sentenceEndings.length !== 1 || !/[.!?]$/.test(summary)) {
|
|
26
|
+
throw new Error("Summary must be exactly one sentence.");
|
|
27
|
+
}
|
|
28
|
+
return summary;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default defineAction({
|
|
32
|
+
description:
|
|
33
|
+
"Complete only the claimed two-Clip transactional email summary workflow.",
|
|
34
|
+
schema: z.object({
|
|
35
|
+
jobId: z.string().trim().min(1),
|
|
36
|
+
summary: z.string().max(2_000),
|
|
37
|
+
}),
|
|
38
|
+
run: async ({ jobId, summary }) => {
|
|
39
|
+
const claimantEmail = getCurrentOwnerEmail().trim().toLowerCase();
|
|
40
|
+
const normalizedSummary = validateTransactionalEmailSummary(summary);
|
|
41
|
+
const completed = await transactionalEmailStore.completeClaimedAi(
|
|
42
|
+
jobId,
|
|
43
|
+
claimantEmail,
|
|
44
|
+
normalizedSummary,
|
|
45
|
+
);
|
|
46
|
+
if (!completed) {
|
|
47
|
+
throw new Error("This transactional email AI claim is unavailable.");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
jobId: completed.logicalKey,
|
|
51
|
+
logicalKey: completed.logicalKey,
|
|
52
|
+
state: completed.state,
|
|
53
|
+
generatedSummary: completed.generatedSummary,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
3
3
|
import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
|
|
4
|
+
import { uploadFile } from "@agent-native/core/file-upload";
|
|
4
5
|
import { buildDeepLink } from "@agent-native/core/server";
|
|
5
6
|
import { extractLoomVideoId, normalizeLoomShareUrl } from "@shared/loom.js";
|
|
6
7
|
import { and, eq } from "drizzle-orm";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
|
|
9
10
|
import { getDb, schema } from "../server/db/index.js";
|
|
11
|
+
import { queueBuilderMediaCompression } from "../server/lib/builder-media-compression.js";
|
|
10
12
|
import { dispatchPostFinalizeJob } from "../server/lib/post-finalize-dispatch.js";
|
|
11
13
|
import {
|
|
12
14
|
getCurrentOwnerEmail,
|
|
@@ -18,7 +20,16 @@ import {
|
|
|
18
20
|
stringifySpaceIds,
|
|
19
21
|
} from "../server/lib/recordings.js";
|
|
20
22
|
import { hasRequestVideoStorage } from "../server/lib/video-storage.js";
|
|
21
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
downloadDirectVideo,
|
|
25
|
+
isCandidateDirectVideoUrl,
|
|
26
|
+
} from "./lib/direct-video.js";
|
|
27
|
+
import {
|
|
28
|
+
enqueueFirstImportEmailIfEligible,
|
|
29
|
+
failLoomImport,
|
|
30
|
+
} from "./lib/loom-import-job.js";
|
|
31
|
+
|
|
32
|
+
export { enqueueFirstImportEmailIfEligible };
|
|
22
33
|
|
|
23
34
|
const LoomOembedSchema = z
|
|
24
35
|
.object({
|
|
@@ -42,14 +53,16 @@ const ImportLoomRecordingSchema = z.object({
|
|
|
42
53
|
.min(1)
|
|
43
54
|
.max(2048)
|
|
44
55
|
.describe(
|
|
45
|
-
"Loom share
|
|
56
|
+
"Loom share/embed URL, or a direct link to an MP4/WebM video file",
|
|
46
57
|
),
|
|
47
58
|
title: z
|
|
48
59
|
.string()
|
|
49
60
|
.trim()
|
|
50
61
|
.max(200)
|
|
51
62
|
.optional()
|
|
52
|
-
.describe(
|
|
63
|
+
.describe(
|
|
64
|
+
"Optional title override; defaults to Loom's oEmbed title when available",
|
|
65
|
+
),
|
|
53
66
|
folderId: z.string().nullish().describe("Optional folder ID"),
|
|
54
67
|
spaceIds: z
|
|
55
68
|
.array(z.string().min(1))
|
|
@@ -71,12 +84,14 @@ const ImportLoomRecordingSchema = z.object({
|
|
|
71
84
|
.string()
|
|
72
85
|
.optional()
|
|
73
86
|
.describe(
|
|
74
|
-
"Existing waiting
|
|
87
|
+
"Existing waiting recording ID to retry after storage is connected",
|
|
75
88
|
),
|
|
76
89
|
});
|
|
77
90
|
|
|
78
91
|
const LOOM_STORAGE_SETUP_REQUIRED_REASON =
|
|
79
92
|
"Video storage is not connected yet. Connect Builder.io or configure S3-compatible storage, then retry this Loom import.";
|
|
93
|
+
const DIRECT_VIDEO_STORAGE_SETUP_REQUIRED_REASON =
|
|
94
|
+
"Video storage is not connected yet. Connect Builder.io or configure S3-compatible storage, then retry this import.";
|
|
80
95
|
|
|
81
96
|
function recordingDeepLink(recordingId: string): string {
|
|
82
97
|
return buildDeepLink({
|
|
@@ -127,14 +142,26 @@ async function fetchLoomOembed(shareUrl: string) {
|
|
|
127
142
|
|
|
128
143
|
export default defineAction({
|
|
129
144
|
description:
|
|
130
|
-
"Import a public Loom share URL into Clips as a playable recording.
|
|
145
|
+
"Import a public Loom share URL, or a direct link to a video file, into Clips as a playable recording. Loom links create the recording immediately and download/reupload Loom's public MP4 plus import Loom's public transcript in the background, since Loom's CDN plus a reupload can take longer than a single request should block on. Other direct video links (e.g. an MP4/WebM/MOV hosted by another screen recorder) are downloaded and reuploaded synchronously without transcript metadata — use request-transcript afterward. If storage is not connected, creates a waiting recording that can be retried after storage setup.",
|
|
131
146
|
schema: ImportLoomRecordingSchema,
|
|
132
147
|
run: async (args) => {
|
|
133
|
-
const shareUrl = normalizeLoomShareUrl(args.url);
|
|
134
148
|
const loomId = extractLoomVideoId(args.url);
|
|
135
|
-
|
|
149
|
+
const isLoom = Boolean(loomId);
|
|
150
|
+
const loomShareUrl = isLoom ? normalizeLoomShareUrl(args.url) : null;
|
|
151
|
+
if (isLoom && !loomShareUrl) {
|
|
136
152
|
throw new Error("Paste a Loom share or embed URL.");
|
|
137
153
|
}
|
|
154
|
+
if (!isLoom && !isCandidateDirectVideoUrl(args.url)) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
"Paste a Loom share URL, or a direct link to a video file.",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const sourceUrl = isLoom ? loomShareUrl! : args.url.trim();
|
|
160
|
+
const sourceAppName = isLoom ? "Loom" : "Video link";
|
|
161
|
+
const storageSetupReason = isLoom
|
|
162
|
+
? LOOM_STORAGE_SETUP_REQUIRED_REASON
|
|
163
|
+
: DIRECT_VIDEO_STORAGE_SETUP_REQUIRED_REASON;
|
|
164
|
+
const providerId = isLoom ? ("loom" as const) : ("video-link" as const);
|
|
138
165
|
|
|
139
166
|
const db = getDb();
|
|
140
167
|
const ownerEmail = getCurrentOwnerEmail();
|
|
@@ -150,25 +177,36 @@ export default defineAction({
|
|
|
150
177
|
),
|
|
151
178
|
);
|
|
152
179
|
if (!existingRecording) {
|
|
153
|
-
throw new Error("Waiting
|
|
180
|
+
throw new Error("Waiting recording not found.");
|
|
154
181
|
}
|
|
155
|
-
if (
|
|
156
|
-
|
|
182
|
+
if (
|
|
183
|
+
existingRecording.sourceAppName?.trim().toLowerCase() !==
|
|
184
|
+
sourceAppName.toLowerCase()
|
|
185
|
+
) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
"Only a matching waiting import can be retried this way.",
|
|
188
|
+
);
|
|
157
189
|
}
|
|
158
|
-
const
|
|
159
|
-
existingRecording.
|
|
160
|
-
|
|
190
|
+
const isWaitingStorageRetry =
|
|
191
|
+
existingRecording.status === "uploading" &&
|
|
192
|
+
!existingRecording.videoUrl &&
|
|
193
|
+
existingRecording.failureReason === storageSetupReason &&
|
|
194
|
+
existingRecording.sourceWindowTitle === sourceUrl;
|
|
161
195
|
const isRetryableLoomImport =
|
|
196
|
+
isLoom &&
|
|
162
197
|
!existingRecording.videoUrl &&
|
|
163
|
-
|
|
198
|
+
existingRecording.sourceWindowTitle === sourceUrl &&
|
|
164
199
|
(existingRecording.status === "processing" ||
|
|
165
200
|
existingRecording.status === "failed" ||
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
201
|
+
isWaitingStorageRetry);
|
|
202
|
+
const isRetryable = isLoom
|
|
203
|
+
? isRetryableLoomImport
|
|
204
|
+
: isWaitingStorageRetry;
|
|
205
|
+
if (!isRetryable) {
|
|
170
206
|
throw new Error(
|
|
171
|
-
|
|
207
|
+
isLoom
|
|
208
|
+
? "Only an incomplete Loom import can be retried in place."
|
|
209
|
+
: "Only a waiting-storage import can be retried in place.",
|
|
172
210
|
);
|
|
173
211
|
}
|
|
174
212
|
}
|
|
@@ -181,7 +219,8 @@ export default defineAction({
|
|
|
181
219
|
|
|
182
220
|
const now = new Date().toISOString();
|
|
183
221
|
const id = existingRecording?.id ?? nanoid();
|
|
184
|
-
const
|
|
222
|
+
const createdAt = existingRecording?.createdAt ?? now;
|
|
223
|
+
const oembed = isLoom ? await fetchLoomOembed(loomShareUrl!) : null;
|
|
185
224
|
|
|
186
225
|
const spaceIds = (
|
|
187
226
|
args.spaceIds ?? parseSpaceIds(existingRecording?.spaceIds)
|
|
@@ -192,11 +231,13 @@ export default defineAction({
|
|
|
192
231
|
existingRecording.title !== "Untitled recording"
|
|
193
232
|
? existingRecording.title
|
|
194
233
|
: null) ||
|
|
195
|
-
oembed
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
234
|
+
oembed?.title?.trim() ||
|
|
235
|
+
(isLoom
|
|
236
|
+
? `Loom recording ${loomId!.slice(0, 8)}`
|
|
237
|
+
: `Imported video ${id.slice(0, 8)}`);
|
|
238
|
+
const durationMs = boundedDurationMs(oembed?.duration);
|
|
239
|
+
const width = boundedDimension(oembed?.width ?? oembed?.thumbnail_width);
|
|
240
|
+
const height = boundedDimension(oembed?.height ?? oembed?.thumbnail_height);
|
|
200
241
|
const folderId = args.folderId ?? existingRecording?.folderId ?? null;
|
|
201
242
|
const visibility =
|
|
202
243
|
args.visibility ?? existingRecording?.visibility ?? defaultVisibility;
|
|
@@ -204,20 +245,23 @@ export default defineAction({
|
|
|
204
245
|
? "manual"
|
|
205
246
|
: (existingRecording?.titleSource ?? "upload");
|
|
206
247
|
|
|
207
|
-
const buildRecordingValues = (
|
|
248
|
+
const buildRecordingValues = (
|
|
249
|
+
videoSizeBytes: number,
|
|
250
|
+
videoFormat: "mp4" | "webm" = "mp4",
|
|
251
|
+
) => ({
|
|
208
252
|
organizationId,
|
|
209
253
|
orgId: organizationId,
|
|
210
254
|
folderId,
|
|
211
255
|
spaceIds: stringifySpaceIds(spaceIds),
|
|
212
256
|
title,
|
|
213
257
|
titleSource,
|
|
214
|
-
sourceAppName
|
|
215
|
-
sourceWindowTitle:
|
|
258
|
+
sourceAppName,
|
|
259
|
+
sourceWindowTitle: sourceUrl,
|
|
216
260
|
description: existingRecording?.description ?? "",
|
|
217
261
|
thumbnailUrl:
|
|
218
|
-
oembed
|
|
262
|
+
oembed?.thumbnail_url ?? existingRecording?.thumbnailUrl ?? null,
|
|
219
263
|
durationMs,
|
|
220
|
-
videoFormat
|
|
264
|
+
videoFormat,
|
|
221
265
|
videoSizeBytes,
|
|
222
266
|
width,
|
|
223
267
|
height,
|
|
@@ -237,7 +281,7 @@ export default defineAction({
|
|
|
237
281
|
...recordingValues,
|
|
238
282
|
status: "uploading",
|
|
239
283
|
videoUrl: null,
|
|
240
|
-
failureReason:
|
|
284
|
+
failureReason: storageSetupReason,
|
|
241
285
|
loomImportClaimId: null,
|
|
242
286
|
loomImportClaimedAt: null,
|
|
243
287
|
})
|
|
@@ -248,19 +292,19 @@ export default defineAction({
|
|
|
248
292
|
...recordingValues,
|
|
249
293
|
videoUrl: null,
|
|
250
294
|
status: "uploading",
|
|
251
|
-
failureReason:
|
|
295
|
+
failureReason: storageSetupReason,
|
|
252
296
|
ownerEmail,
|
|
253
|
-
createdAt
|
|
297
|
+
createdAt,
|
|
254
298
|
});
|
|
255
299
|
}
|
|
256
300
|
|
|
257
301
|
await writeAppState(`recording-upload-${id}`, {
|
|
258
302
|
recordingId: id,
|
|
259
303
|
status: "waiting_storage",
|
|
260
|
-
failureReason:
|
|
304
|
+
failureReason: storageSetupReason,
|
|
261
305
|
progress: 100,
|
|
262
|
-
provider:
|
|
263
|
-
sourceUrl
|
|
306
|
+
provider: providerId,
|
|
307
|
+
sourceUrl,
|
|
264
308
|
durationMs,
|
|
265
309
|
width,
|
|
266
310
|
height,
|
|
@@ -276,13 +320,13 @@ export default defineAction({
|
|
|
276
320
|
title,
|
|
277
321
|
status: "waiting_storage" as const,
|
|
278
322
|
storageSetupRequired: true,
|
|
279
|
-
provider:
|
|
280
|
-
sourceUrl
|
|
281
|
-
thumbnailUrl: oembed
|
|
323
|
+
provider: providerId,
|
|
324
|
+
sourceUrl,
|
|
325
|
+
thumbnailUrl: oembed?.thumbnail_url ?? null,
|
|
282
326
|
durationMs,
|
|
283
327
|
importMode: "reuploaded" as const,
|
|
284
328
|
videoSizeBytes,
|
|
285
|
-
note:
|
|
329
|
+
note: storageSetupReason,
|
|
286
330
|
};
|
|
287
331
|
};
|
|
288
332
|
|
|
@@ -292,22 +336,115 @@ export default defineAction({
|
|
|
292
336
|
);
|
|
293
337
|
}
|
|
294
338
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
339
|
+
if (isLoom) {
|
|
340
|
+
// Storage is connected: create/refresh the row now and hand the slow
|
|
341
|
+
// Loom download + reupload + transcript off to a durable background
|
|
342
|
+
// job (post-finalize-worker.post.ts's "loom-import" kind /
|
|
343
|
+
// runLoomImportJob). Loom's CDN plus a reupload can outlast a single
|
|
344
|
+
// request; the worker claims the row (loomImportClaimId) and moves it
|
|
345
|
+
// to "ready" or "failed" once done.
|
|
346
|
+
const recordingValues = buildRecordingValues(
|
|
347
|
+
existingRecording?.videoSizeBytes ?? 0,
|
|
348
|
+
);
|
|
349
|
+
if (existingRecording) {
|
|
350
|
+
await db
|
|
351
|
+
.update(schema.recordings)
|
|
352
|
+
.set({
|
|
353
|
+
...recordingValues,
|
|
354
|
+
status: "processing",
|
|
355
|
+
videoUrl: null,
|
|
356
|
+
failureReason: null,
|
|
357
|
+
loomImportClaimId: null,
|
|
358
|
+
loomImportClaimedAt: null,
|
|
359
|
+
})
|
|
360
|
+
.where(eq(schema.recordings.id, id));
|
|
361
|
+
} else {
|
|
362
|
+
await db.insert(schema.recordings).values({
|
|
363
|
+
id,
|
|
364
|
+
...recordingValues,
|
|
365
|
+
videoUrl: null,
|
|
366
|
+
status: "processing",
|
|
367
|
+
failureReason: null,
|
|
368
|
+
ownerEmail,
|
|
369
|
+
createdAt,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await writeAppState(`recording-upload-${id}`, {
|
|
374
|
+
recordingId: id,
|
|
375
|
+
status: "processing",
|
|
376
|
+
progress: 100,
|
|
377
|
+
provider: providerId,
|
|
378
|
+
sourceUrl,
|
|
379
|
+
durationMs,
|
|
380
|
+
width,
|
|
381
|
+
height,
|
|
382
|
+
hasAudio: true,
|
|
383
|
+
hasCamera: false,
|
|
384
|
+
updatedAt: now,
|
|
385
|
+
});
|
|
386
|
+
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
387
|
+
await writeAppState("navigate", { view: "recording", recordingId: id });
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
await dispatchPostFinalizeJob({
|
|
391
|
+
recordingId: id,
|
|
392
|
+
kind: "loom-import",
|
|
393
|
+
requireAccepted: true,
|
|
394
|
+
});
|
|
395
|
+
} catch (err) {
|
|
396
|
+
const failureReason = `Could not start the Loom import: ${
|
|
397
|
+
err instanceof Error ? err.message : String(err)
|
|
398
|
+
}`;
|
|
399
|
+
await failLoomImport(id, failureReason);
|
|
400
|
+
throw new Error(failureReason);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
recordingId: id,
|
|
405
|
+
title,
|
|
406
|
+
status: "processing" as const,
|
|
407
|
+
provider: providerId,
|
|
408
|
+
sourceUrl,
|
|
409
|
+
thumbnailUrl: oembed?.thumbnail_url ?? null,
|
|
410
|
+
durationMs,
|
|
411
|
+
importMode: "reuploaded" as const,
|
|
412
|
+
note: "Downloading and importing this Loom recording in the background.",
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Direct video links stay synchronous: they typically download and
|
|
417
|
+
// reupload well within a single request, and this keeps
|
|
418
|
+
// request-transcript as the deliberate next step for a transcript.
|
|
419
|
+
const media = await downloadDirectVideo(sourceUrl);
|
|
420
|
+
const videoFormat = media.mimeType === "video/webm" ? "webm" : "mp4";
|
|
421
|
+
const upload = await uploadFile({
|
|
422
|
+
data: media.bytes,
|
|
423
|
+
filename: `${id}.${videoFormat}`,
|
|
424
|
+
mimeType: media.mimeType,
|
|
425
|
+
ownerEmail,
|
|
426
|
+
stableUrl: true,
|
|
427
|
+
recordAsset: false,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
if (upload === null) {
|
|
431
|
+
return await saveWaitingForStorage(media.sizeBytes);
|
|
432
|
+
}
|
|
433
|
+
if (!upload?.url) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
"File upload returned no URL. Check your storage provider configuration.",
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const videoUrl = upload.url;
|
|
440
|
+
const recordingValues = buildRecordingValues(media.sizeBytes, videoFormat);
|
|
304
441
|
if (existingRecording) {
|
|
305
442
|
await db
|
|
306
443
|
.update(schema.recordings)
|
|
307
444
|
.set({
|
|
308
445
|
...recordingValues,
|
|
309
|
-
status: "
|
|
310
|
-
videoUrl
|
|
446
|
+
status: "ready",
|
|
447
|
+
videoUrl,
|
|
311
448
|
failureReason: null,
|
|
312
449
|
loomImportClaimId: null,
|
|
313
450
|
loomImportClaimedAt: null,
|
|
@@ -317,20 +454,75 @@ export default defineAction({
|
|
|
317
454
|
await db.insert(schema.recordings).values({
|
|
318
455
|
id,
|
|
319
456
|
...recordingValues,
|
|
320
|
-
videoUrl
|
|
321
|
-
status: "
|
|
457
|
+
videoUrl,
|
|
458
|
+
status: "ready",
|
|
322
459
|
failureReason: null,
|
|
323
460
|
ownerEmail,
|
|
461
|
+
createdAt,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
void queueBuilderMediaCompression({
|
|
466
|
+
recordingId: id,
|
|
467
|
+
ownerEmail,
|
|
468
|
+
videoUrl,
|
|
469
|
+
mimeType: media.mimeType,
|
|
470
|
+
providerId: upload.provider,
|
|
471
|
+
assetDbId: upload.id,
|
|
472
|
+
sourceSizeBytes: media.sizeBytes,
|
|
473
|
+
}).catch((err) => {
|
|
474
|
+
console.warn("[clips] Video import media compression queue failed", {
|
|
475
|
+
recordingId: id,
|
|
476
|
+
error: err instanceof Error ? err.message : String(err),
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const transcriptValues = {
|
|
481
|
+
ownerEmail,
|
|
482
|
+
language: "en",
|
|
483
|
+
segmentsJson: "[]",
|
|
484
|
+
fullText: "",
|
|
485
|
+
status: "failed" as const,
|
|
486
|
+
failureReason:
|
|
487
|
+
"Transcript import isn't available for direct video links yet. Use request-transcript to transcribe the uploaded media.",
|
|
488
|
+
updatedAt: now,
|
|
489
|
+
};
|
|
490
|
+
const [existingTranscript] = await db
|
|
491
|
+
.select({ recordingId: schema.recordingTranscripts.recordingId })
|
|
492
|
+
.from(schema.recordingTranscripts)
|
|
493
|
+
.where(eq(schema.recordingTranscripts.recordingId, id));
|
|
494
|
+
if (existingTranscript) {
|
|
495
|
+
await db
|
|
496
|
+
.update(schema.recordingTranscripts)
|
|
497
|
+
.set(transcriptValues)
|
|
498
|
+
.where(eq(schema.recordingTranscripts.recordingId, id));
|
|
499
|
+
} else {
|
|
500
|
+
await db.insert(schema.recordingTranscripts).values({
|
|
501
|
+
recordingId: id,
|
|
502
|
+
...transcriptValues,
|
|
324
503
|
createdAt: now,
|
|
325
504
|
});
|
|
326
505
|
}
|
|
327
506
|
|
|
507
|
+
try {
|
|
508
|
+
await enqueueFirstImportEmailIfEligible(
|
|
509
|
+
{ recordingId: id, ownerEmail, createdAt },
|
|
510
|
+
db,
|
|
511
|
+
);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
console.warn("[clips] First-import email enqueue failed", {
|
|
514
|
+
recordingId: id,
|
|
515
|
+
error: err instanceof Error ? err.message : String(err),
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
328
519
|
await writeAppState(`recording-upload-${id}`, {
|
|
329
520
|
recordingId: id,
|
|
330
|
-
status: "
|
|
521
|
+
status: "ready",
|
|
331
522
|
progress: 100,
|
|
332
|
-
|
|
333
|
-
|
|
523
|
+
videoUrl,
|
|
524
|
+
provider: providerId,
|
|
525
|
+
sourceUrl,
|
|
334
526
|
durationMs,
|
|
335
527
|
width,
|
|
336
528
|
height,
|
|
@@ -341,30 +533,20 @@ export default defineAction({
|
|
|
341
533
|
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
342
534
|
await writeAppState("navigate", { view: "recording", recordingId: id });
|
|
343
535
|
|
|
344
|
-
try {
|
|
345
|
-
await dispatchPostFinalizeJob({
|
|
346
|
-
recordingId: id,
|
|
347
|
-
kind: "loom-import",
|
|
348
|
-
requireAccepted: true,
|
|
349
|
-
});
|
|
350
|
-
} catch (err) {
|
|
351
|
-
const failureReason = `Could not start the Loom import: ${
|
|
352
|
-
err instanceof Error ? err.message : String(err)
|
|
353
|
-
}`;
|
|
354
|
-
await failLoomImport(id, failureReason);
|
|
355
|
-
throw new Error(failureReason);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
536
|
return {
|
|
359
537
|
recordingId: id,
|
|
360
538
|
title,
|
|
361
|
-
status: "
|
|
362
|
-
provider:
|
|
363
|
-
sourceUrl
|
|
364
|
-
|
|
539
|
+
status: "ready" as const,
|
|
540
|
+
provider: providerId,
|
|
541
|
+
sourceUrl,
|
|
542
|
+
videoUrl,
|
|
543
|
+
embedUrl: videoUrl,
|
|
544
|
+
thumbnailUrl: oembed?.thumbnail_url ?? null,
|
|
365
545
|
durationMs,
|
|
366
546
|
importMode: "reuploaded" as const,
|
|
367
|
-
|
|
547
|
+
storageProvider: upload.provider,
|
|
548
|
+
videoSizeBytes: media.sizeBytes,
|
|
549
|
+
note: "Imported as a Clips-hosted video. Use request-transcript to transcribe the uploaded media.",
|
|
368
550
|
};
|
|
369
551
|
},
|
|
370
552
|
link: ({ result }) => {
|
|
@@ -373,7 +555,7 @@ export default defineAction({
|
|
|
373
555
|
if (typeof recordingId !== "string") return null;
|
|
374
556
|
return {
|
|
375
557
|
url: recordingDeepLink(recordingId),
|
|
376
|
-
label: "Open imported
|
|
558
|
+
label: "Open imported clip in Clips",
|
|
377
559
|
view: "recording",
|
|
378
560
|
};
|
|
379
561
|
},
|