@agent-native/core 0.122.0 → 0.122.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/README.md +5 -25
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +57 -4
- package/corpus/core/src/agent/production-agent.ts +65 -1
- package/corpus/core/src/agent/run-store.ts +61 -1
- package/corpus/core/src/client/settings/SettingsPanel.tsx +9 -9
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +12 -2
- package/corpus/core/src/extensions/url-safety.ts +67 -4
- package/corpus/core/src/server/builder-design-systems.ts +1 -1
- package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/corpus/core/src/templates/default/app/root.tsx +1 -1
- package/corpus/core/src/templates/default/public/manifest.json +2 -2
- package/corpus/core/src/vite/client.ts +64 -3
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +15 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -23
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +3 -3
- package/corpus/templates/analytics/app/routes/_index.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-07-25-analytics-dashboards-and-scheduled-reports-are-faster-and-mo.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/analytics/package.json +1 -1
- package/corpus/templates/analytics/public/manifest.json +1 -1
- package/corpus/templates/analytics/server/db/schema.ts +12 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +8 -4
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +136 -5
- package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +250 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +167 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
- package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +105 -23
- package/corpus/templates/analytics/server/lib/first-party-unbounded-panel-repair.ts +169 -0
- package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
- package/corpus/templates/analytics/server/plugins/db.ts +41 -1
- package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +6 -3
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/assets/app/routes/_index.tsx +1 -1
- package/corpus/templates/assets/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/assets/server/plugins/auth.ts +1 -1
- package/corpus/templates/brain/app/lib/brain.ts +0 -7
- package/corpus/templates/brain/app/routes/_index.tsx +1 -2
- package/corpus/templates/brain/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/brain/package.json +1 -1
- package/corpus/templates/brain/public/manifest.json +1 -1
- package/corpus/templates/brain/server/plugins/auth.ts +1 -1
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -6
- package/corpus/templates/calendar/app/routes/_app._index.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/calendar/package.json +1 -1
- package/corpus/templates/calendar/public/manifest.json +1 -1
- package/corpus/templates/calendar/server/plugins/auth.ts +1 -1
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/corpus/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +53 -98
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +260 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +0 -7
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -1
- package/corpus/templates/clips/app/routes/_index.tsx +1 -1
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +6 -0
- package/corpus/templates/clips/app/routes/record.tsx +5 -0
- package/corpus/templates/clips/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/clips/server/db/schema.ts +2 -0
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +2 -1
- package/corpus/templates/clips/server/plugins/auth.ts +1 -1
- package/corpus/templates/clips/server/plugins/db.ts +8 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +56 -3
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -39
- package/corpus/templates/content/app/routes/_app._index.tsx +1 -2
- package/corpus/templates/content/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/content/package.json +1 -1
- package/corpus/templates/content/public/manifest.json +1 -1
- package/corpus/templates/content/server/plugins/auth.ts +1 -1
- package/corpus/templates/design/actions/edit-design.ts +66 -27
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/design/app/routes/_index.tsx +1 -1
- package/corpus/templates/design/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/design/changelog/2026-07-25-design-edits-retry-concurrent-changes.md +6 -0
- package/corpus/templates/design/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/design/server/plugins/auth.ts +1 -1
- package/corpus/templates/design/server/source-workspace.ts +5 -1
- package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -10
- package/corpus/templates/dispatch/app/routes/overview.tsx +1 -1
- package/corpus/templates/dispatch/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/dispatch/package.json +1 -1
- package/corpus/templates/dispatch/public/manifest.json +1 -1
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +6 -4
- package/corpus/templates/forms/actions/create-form.ts +5 -1
- package/corpus/templates/forms/actions/export-responses.ts +22 -11
- package/corpus/templates/forms/actions/update-form.ts +5 -1
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +0 -35
- package/corpus/templates/forms/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-fields-can-be-created-without-ids.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-response-exports-use-file-storage.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/forms/package.json +1 -1
- package/corpus/templates/forms/public/manifest.json +1 -1
- package/corpus/templates/forms/server/lib/validate-fields.ts +43 -0
- package/corpus/templates/forms/server/plugins/auth.ts +1 -1
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +0 -2
- package/corpus/templates/macros/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/macros/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/macros/package.json +1 -1
- package/corpus/templates/macros/server/plugins/auth.ts +2 -2
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -39
- package/corpus/templates/mail/app/routes/$view.$threadId.tsx +1 -1
- package/corpus/templates/mail/app/routes/$view.tsx +1 -1
- package/corpus/templates/mail/app/routes/_index.tsx +1 -1
- package/corpus/templates/mail/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/mail/package.json +1 -1
- package/corpus/templates/mail/public/manifest.json +1 -1
- package/corpus/templates/mail/server/plugins/auth.ts +1 -1
- package/corpus/templates/plan/agent-native.app-skill.json +1 -1
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +4 -9
- package/corpus/templates/plan/app/lib/app-config.ts +1 -1
- package/corpus/templates/plan/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/plan/package.json +1 -1
- package/corpus/templates/plan/public/manifest.json +1 -1
- package/corpus/templates/plan/server/plugins/auth.ts +1 -1
- package/corpus/templates/slides/actions/extract-pdf.ts +4 -4
- package/corpus/templates/slides/actions/import-file.ts +3 -4
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/slides/app/routes/_index.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-gemini-image-models-updated.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-pdf-imports-tolerate-missing-canvas.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/slides/package.json +1 -1
- package/corpus/templates/slides/public/manifest.json +1 -1
- package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +5 -2
- package/corpus/templates/slides/server/lib/pdf-parse-setup.ts +150 -0
- package/corpus/templates/slides/server/plugins/auth.ts +1 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +58 -4
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +63 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +49 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +9 -9
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/client/settings/SettingsTabsPage.js +7 -4
- package/dist/client/settings/SettingsTabsPage.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/url-safety.d.ts +9 -16
- package/dist/extensions/url-safety.d.ts.map +1 -1
- package/dist/extensions/url-safety.js +48 -4
- package/dist/extensions/url-safety.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/builder-design-systems.js +1 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/dist/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/dist/templates/default/app/root.tsx +1 -1
- package/dist/templates/default/public/manifest.json +2 -2
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +64 -1
- package/dist/vite/client.js.map +1 -1
- package/package.json +3 -3
- package/src/a2a/client.ts +57 -4
- package/src/agent/production-agent.ts +65 -1
- package/src/agent/run-store.ts +61 -1
- package/src/client/settings/SettingsPanel.tsx +9 -9
- package/src/client/settings/SettingsTabsPage.tsx +12 -2
- package/src/extensions/url-safety.ts +67 -4
- package/src/server/builder-design-systems.ts +1 -1
- package/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/src/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/src/templates/default/app/root.tsx +1 -1
- package/src/templates/default/public/manifest.json +2 -2
- package/src/vite/client.ts +64 -3
|
@@ -1,14 +1,13 @@
|
|
|
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";
|
|
5
4
|
import { buildDeepLink } from "@agent-native/core/server";
|
|
6
5
|
import { extractLoomVideoId, normalizeLoomShareUrl } from "@shared/loom.js";
|
|
7
6
|
import { and, eq } from "drizzle-orm";
|
|
8
7
|
import { z } from "zod";
|
|
9
8
|
|
|
10
9
|
import { getDb, schema } from "../server/db/index.js";
|
|
11
|
-
import {
|
|
10
|
+
import { dispatchPostFinalizeJob } from "../server/lib/post-finalize-dispatch.js";
|
|
12
11
|
import {
|
|
13
12
|
getCurrentOwnerEmail,
|
|
14
13
|
getOrganizationDefaultVisibility,
|
|
@@ -19,11 +18,7 @@ import {
|
|
|
19
18
|
stringifySpaceIds,
|
|
20
19
|
} from "../server/lib/recordings.js";
|
|
21
20
|
import { hasRequestVideoStorage } from "../server/lib/video-storage.js";
|
|
22
|
-
import {
|
|
23
|
-
fetchLoomTranscript,
|
|
24
|
-
loomTranscriptUnavailableMessage,
|
|
25
|
-
} from "./lib/loom-transcript.js";
|
|
26
|
-
import { downloadLoomVideo } from "./lib/loom-video.js";
|
|
21
|
+
import { failLoomImport } from "./lib/loom-import-job.js";
|
|
27
22
|
|
|
28
23
|
const LoomOembedSchema = z
|
|
29
24
|
.object({
|
|
@@ -132,7 +127,7 @@ async function fetchLoomOembed(shareUrl: string) {
|
|
|
132
127
|
|
|
133
128
|
export default defineAction({
|
|
134
129
|
description:
|
|
135
|
-
"Import a public Loom share URL into Clips as a playable recording.
|
|
130
|
+
"Import a public Loom share URL into Clips as a playable recording. Creates the recording immediately and downloads Loom's public MP4, reuploads it to the configured Clips storage provider, and imports Loom's public transcript in the background (Loom's CDN plus a reupload can take longer than a single request should block on). If storage is not connected, creates a waiting recording that can be retried after storage setup.",
|
|
136
131
|
schema: ImportLoomRecordingSchema,
|
|
137
132
|
run: async (args) => {
|
|
138
133
|
const shareUrl = normalizeLoomShareUrl(args.url);
|
|
@@ -163,15 +158,17 @@ export default defineAction({
|
|
|
163
158
|
const existingSourceUrl = normalizeLoomShareUrl(
|
|
164
159
|
existingRecording.sourceWindowTitle ?? "",
|
|
165
160
|
);
|
|
166
|
-
const
|
|
167
|
-
existingRecording.status === "uploading" &&
|
|
161
|
+
const isRetryableLoomImport =
|
|
168
162
|
!existingRecording.videoUrl &&
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
existingSourceUrl === shareUrl &&
|
|
164
|
+
(existingRecording.status === "processing" ||
|
|
165
|
+
existingRecording.status === "failed" ||
|
|
166
|
+
(existingRecording.status === "uploading" &&
|
|
167
|
+
existingRecording.failureReason ===
|
|
168
|
+
LOOM_STORAGE_SETUP_REQUIRED_REASON));
|
|
169
|
+
if (!isRetryableLoomImport) {
|
|
173
170
|
throw new Error(
|
|
174
|
-
"Only
|
|
171
|
+
"Only an incomplete Loom import can be retried in place.",
|
|
175
172
|
);
|
|
176
173
|
}
|
|
177
174
|
}
|
|
@@ -241,6 +238,8 @@ export default defineAction({
|
|
|
241
238
|
status: "uploading",
|
|
242
239
|
videoUrl: null,
|
|
243
240
|
failureReason: LOOM_STORAGE_SETUP_REQUIRED_REASON,
|
|
241
|
+
loomImportClaimId: null,
|
|
242
|
+
loomImportClaimedAt: null,
|
|
244
243
|
})
|
|
245
244
|
.where(eq(schema.recordings.id, id));
|
|
246
245
|
} else {
|
|
@@ -293,123 +292,79 @@ export default defineAction({
|
|
|
293
292
|
);
|
|
294
293
|
}
|
|
295
294
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const recordingValues = buildRecordingValues(media.sizeBytes);
|
|
307
|
-
if (upload === null) {
|
|
308
|
-
return await saveWaitingForStorage(media.sizeBytes);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (!upload?.url) {
|
|
312
|
-
throw new Error(
|
|
313
|
-
"File upload returned no URL. Check your storage provider configuration.",
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const videoUrl = upload.url;
|
|
295
|
+
// Storage is connected: create the row now and hand the slow Loom
|
|
296
|
+
// download + reupload + transcript off to a durable background job.
|
|
297
|
+
// Loom's CDN plus a reupload can outlast the platform's request timeout;
|
|
298
|
+
// doing it inline previously crashed the function mid-download instead of
|
|
299
|
+
// returning a clean error (see post-finalize-worker.post.ts's "loom-import"
|
|
300
|
+
// kind / runLoomImportJob).
|
|
301
|
+
const recordingValues = buildRecordingValues(
|
|
302
|
+
existingRecording?.videoSizeBytes ?? 0,
|
|
303
|
+
);
|
|
318
304
|
if (existingRecording) {
|
|
319
305
|
await db
|
|
320
306
|
.update(schema.recordings)
|
|
321
307
|
.set({
|
|
322
308
|
...recordingValues,
|
|
323
|
-
|
|
324
|
-
|
|
309
|
+
status: "processing",
|
|
310
|
+
videoUrl: null,
|
|
325
311
|
failureReason: null,
|
|
312
|
+
loomImportClaimId: null,
|
|
313
|
+
loomImportClaimedAt: null,
|
|
326
314
|
})
|
|
327
315
|
.where(eq(schema.recordings.id, id));
|
|
328
316
|
} else {
|
|
329
317
|
await db.insert(schema.recordings).values({
|
|
330
318
|
id,
|
|
331
319
|
...recordingValues,
|
|
332
|
-
videoUrl,
|
|
333
|
-
status: "
|
|
320
|
+
videoUrl: null,
|
|
321
|
+
status: "processing",
|
|
334
322
|
failureReason: null,
|
|
335
323
|
ownerEmail,
|
|
336
324
|
createdAt: now,
|
|
337
325
|
});
|
|
338
326
|
}
|
|
339
327
|
|
|
340
|
-
|
|
328
|
+
await writeAppState(`recording-upload-${id}`, {
|
|
341
329
|
recordingId: id,
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
});
|
|
330
|
+
status: "processing",
|
|
331
|
+
progress: 100,
|
|
332
|
+
provider: "loom",
|
|
333
|
+
sourceUrl: shareUrl,
|
|
334
|
+
durationMs,
|
|
335
|
+
width,
|
|
336
|
+
height,
|
|
337
|
+
hasAudio: true,
|
|
338
|
+
hasCamera: false,
|
|
339
|
+
updatedAt: now,
|
|
353
340
|
});
|
|
341
|
+
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
342
|
+
await writeAppState("navigate", { view: "recording", recordingId: id });
|
|
354
343
|
|
|
355
|
-
let transcript: Awaited<ReturnType<typeof fetchLoomTranscript>> = null;
|
|
356
344
|
try {
|
|
357
|
-
|
|
358
|
-
} catch (err) {
|
|
359
|
-
console.warn(
|
|
360
|
-
`[clips] Loom transcript import skipped for ${loomId}:`,
|
|
361
|
-
(err as Error)?.message ?? String(err),
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const transcriptValues = {
|
|
366
|
-
ownerEmail,
|
|
367
|
-
language: transcript?.language ?? "en",
|
|
368
|
-
segmentsJson: transcript ? JSON.stringify(transcript.segments) : "[]",
|
|
369
|
-
fullText: transcript?.fullText ?? "",
|
|
370
|
-
status: transcript ? ("ready" as const) : ("failed" as const),
|
|
371
|
-
failureReason: transcript ? null : loomTranscriptUnavailableMessage(),
|
|
372
|
-
updatedAt: now,
|
|
373
|
-
};
|
|
374
|
-
const [existingTranscript] = await db
|
|
375
|
-
.select({ recordingId: schema.recordingTranscripts.recordingId })
|
|
376
|
-
.from(schema.recordingTranscripts)
|
|
377
|
-
.where(eq(schema.recordingTranscripts.recordingId, id));
|
|
378
|
-
if (existingTranscript) {
|
|
379
|
-
await db
|
|
380
|
-
.update(schema.recordingTranscripts)
|
|
381
|
-
.set(transcriptValues)
|
|
382
|
-
.where(eq(schema.recordingTranscripts.recordingId, id));
|
|
383
|
-
} else {
|
|
384
|
-
await db.insert(schema.recordingTranscripts).values({
|
|
345
|
+
await dispatchPostFinalizeJob({
|
|
385
346
|
recordingId: id,
|
|
386
|
-
|
|
387
|
-
|
|
347
|
+
kind: "loom-import",
|
|
348
|
+
requireAccepted: true,
|
|
388
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);
|
|
389
356
|
}
|
|
390
357
|
|
|
391
|
-
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
392
|
-
await writeAppState("navigate", { view: "recording", recordingId: id });
|
|
393
|
-
|
|
394
358
|
return {
|
|
395
359
|
recordingId: id,
|
|
396
360
|
title,
|
|
397
|
-
status: "
|
|
361
|
+
status: "processing" as const,
|
|
398
362
|
provider: "loom" as const,
|
|
399
363
|
sourceUrl: shareUrl,
|
|
400
|
-
videoUrl,
|
|
401
|
-
embedUrl: videoUrl,
|
|
402
364
|
thumbnailUrl: oembed.thumbnail_url ?? null,
|
|
403
365
|
durationMs,
|
|
404
|
-
transcriptStatus: transcript
|
|
405
|
-
? ("ready" as const)
|
|
406
|
-
: ("unavailable" as const),
|
|
407
366
|
importMode: "reuploaded" as const,
|
|
408
|
-
|
|
409
|
-
videoSizeBytes: media.sizeBytes,
|
|
410
|
-
note: transcript
|
|
411
|
-
? "Imported as a Clips-hosted MP4 with Loom's public transcript."
|
|
412
|
-
: "Imported as a Clips-hosted MP4. Loom did not expose an importable transcript; use request-transcript to transcribe the uploaded media.",
|
|
367
|
+
note: "Downloading and importing this Loom recording in the background.",
|
|
413
368
|
};
|
|
414
369
|
},
|
|
415
370
|
link: ({ result }) => {
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { writeAppState } from "@agent-native/core/application-state";
|
|
2
|
+
import { uploadFile } from "@agent-native/core/file-upload";
|
|
3
|
+
import { and, eq } from "drizzle-orm";
|
|
4
|
+
|
|
5
|
+
import { getDb, schema } from "../../server/db/index.js";
|
|
6
|
+
import { queueBuilderMediaCompression } from "../../server/lib/builder-media-compression.js";
|
|
7
|
+
import {
|
|
8
|
+
extractLoomVideoId,
|
|
9
|
+
normalizeLoomShareUrl,
|
|
10
|
+
} from "../../shared/loom.js";
|
|
11
|
+
import {
|
|
12
|
+
fetchLoomTranscript,
|
|
13
|
+
loomTranscriptUnavailableMessage,
|
|
14
|
+
} from "./loom-transcript.js";
|
|
15
|
+
import { downloadLoomVideo } from "./loom-video.js";
|
|
16
|
+
|
|
17
|
+
export type LoomImportJobResult = {
|
|
18
|
+
status: "ready" | "failed";
|
|
19
|
+
failureReason?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function failLoomImport(
|
|
23
|
+
recordingId: string,
|
|
24
|
+
failureReason: string,
|
|
25
|
+
claimId?: string,
|
|
26
|
+
): Promise<LoomImportJobResult> {
|
|
27
|
+
const now = new Date().toISOString();
|
|
28
|
+
const [updated] = await getDb()
|
|
29
|
+
.update(schema.recordings)
|
|
30
|
+
.set({
|
|
31
|
+
status: "failed",
|
|
32
|
+
failureReason,
|
|
33
|
+
loomImportClaimId: null,
|
|
34
|
+
loomImportClaimedAt: null,
|
|
35
|
+
updatedAt: now,
|
|
36
|
+
})
|
|
37
|
+
.where(
|
|
38
|
+
claimId
|
|
39
|
+
? and(
|
|
40
|
+
eq(schema.recordings.id, recordingId),
|
|
41
|
+
eq(schema.recordings.loomImportClaimId, claimId),
|
|
42
|
+
)
|
|
43
|
+
: eq(schema.recordings.id, recordingId),
|
|
44
|
+
)
|
|
45
|
+
.returning({ id: schema.recordings.id });
|
|
46
|
+
if (!updated) return { status: "failed", failureReason };
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await writeAppState(`recording-upload-${recordingId}`, {
|
|
50
|
+
recordingId,
|
|
51
|
+
status: "failed",
|
|
52
|
+
failureReason,
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
});
|
|
55
|
+
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.warn("[clips] Loom failure state update failed", {
|
|
58
|
+
recordingId,
|
|
59
|
+
error: err instanceof Error ? err.message : String(err),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return { status: "failed", failureReason };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Downloads a Loom video and re-uploads it to Clips storage, off the request
|
|
67
|
+
* that created the "processing" row. Loom's CDN plus a reupload can outlast a
|
|
68
|
+
* synchronous serverless function's execution ceiling; running it here keeps
|
|
69
|
+
* import-loom-recording's own request fast regardless of Loom video length.
|
|
70
|
+
*/
|
|
71
|
+
export async function runLoomImportJob({
|
|
72
|
+
recordingId,
|
|
73
|
+
ownerEmail,
|
|
74
|
+
claimId,
|
|
75
|
+
}: {
|
|
76
|
+
recordingId: string;
|
|
77
|
+
ownerEmail: string;
|
|
78
|
+
claimId: string;
|
|
79
|
+
}): Promise<LoomImportJobResult> {
|
|
80
|
+
const db = getDb();
|
|
81
|
+
const [recording] = await db
|
|
82
|
+
.select({
|
|
83
|
+
id: schema.recordings.id,
|
|
84
|
+
durationMs: schema.recordings.durationMs,
|
|
85
|
+
sourceWindowTitle: schema.recordings.sourceWindowTitle,
|
|
86
|
+
loomImportClaimId: schema.recordings.loomImportClaimId,
|
|
87
|
+
})
|
|
88
|
+
.from(schema.recordings)
|
|
89
|
+
.where(eq(schema.recordings.id, recordingId));
|
|
90
|
+
|
|
91
|
+
const shareUrl = normalizeLoomShareUrl(recording?.sourceWindowTitle ?? "");
|
|
92
|
+
const loomId = shareUrl ? extractLoomVideoId(shareUrl) : null;
|
|
93
|
+
if (
|
|
94
|
+
!recording ||
|
|
95
|
+
recording.loomImportClaimId !== claimId ||
|
|
96
|
+
!shareUrl ||
|
|
97
|
+
!loomId
|
|
98
|
+
) {
|
|
99
|
+
return failLoomImport(
|
|
100
|
+
recordingId,
|
|
101
|
+
"This Loom recording is missing its source URL.",
|
|
102
|
+
claimId,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let media: Awaited<ReturnType<typeof downloadLoomVideo>>;
|
|
107
|
+
try {
|
|
108
|
+
media = await downloadLoomVideo({ loomId, shareUrl });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return failLoomImport(
|
|
111
|
+
recordingId,
|
|
112
|
+
err instanceof Error ? err.message : String(err),
|
|
113
|
+
claimId,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const upload = await uploadFile({
|
|
119
|
+
data: media.bytes,
|
|
120
|
+
filename: `${recordingId}.mp4`,
|
|
121
|
+
mimeType: media.mimeType,
|
|
122
|
+
ownerEmail,
|
|
123
|
+
stableUrl: true,
|
|
124
|
+
recordAsset: false,
|
|
125
|
+
});
|
|
126
|
+
if (!upload?.url) {
|
|
127
|
+
return failLoomImport(
|
|
128
|
+
recordingId,
|
|
129
|
+
"File upload returned no URL. Check your storage provider configuration.",
|
|
130
|
+
claimId,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const now = new Date().toISOString();
|
|
135
|
+
const [mediaReady] = await db
|
|
136
|
+
.update(schema.recordings)
|
|
137
|
+
.set({
|
|
138
|
+
videoUrl: upload.url,
|
|
139
|
+
videoSizeBytes: media.sizeBytes,
|
|
140
|
+
status: "ready",
|
|
141
|
+
failureReason: null,
|
|
142
|
+
updatedAt: now,
|
|
143
|
+
})
|
|
144
|
+
.where(
|
|
145
|
+
and(
|
|
146
|
+
eq(schema.recordings.id, recordingId),
|
|
147
|
+
eq(schema.recordings.loomImportClaimId, claimId),
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
.returning({ id: schema.recordings.id });
|
|
151
|
+
if (!mediaReady) {
|
|
152
|
+
return {
|
|
153
|
+
status: "failed",
|
|
154
|
+
failureReason: "The Loom import lease was lost before media was saved.",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
void queueBuilderMediaCompression({
|
|
159
|
+
recordingId,
|
|
160
|
+
ownerEmail,
|
|
161
|
+
videoUrl: upload.url,
|
|
162
|
+
mimeType: media.mimeType,
|
|
163
|
+
providerId: upload.provider,
|
|
164
|
+
assetDbId: upload.id,
|
|
165
|
+
sourceSizeBytes: media.sizeBytes,
|
|
166
|
+
}).catch((err) => {
|
|
167
|
+
console.warn("[clips] Loom media compression queue failed", {
|
|
168
|
+
recordingId,
|
|
169
|
+
error: err instanceof Error ? err.message : String(err),
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
let transcript: Awaited<ReturnType<typeof fetchLoomTranscript>> = null;
|
|
175
|
+
try {
|
|
176
|
+
transcript = await fetchLoomTranscript({
|
|
177
|
+
shareUrl,
|
|
178
|
+
durationMs: recording.durationMs,
|
|
179
|
+
});
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.warn(
|
|
182
|
+
`[clips] Loom transcript import skipped for ${loomId}:`,
|
|
183
|
+
err instanceof Error ? err.message : String(err),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const transcriptValues = {
|
|
188
|
+
ownerEmail,
|
|
189
|
+
language: transcript?.language ?? "en",
|
|
190
|
+
segmentsJson: transcript ? JSON.stringify(transcript.segments) : "[]",
|
|
191
|
+
fullText: transcript?.fullText ?? "",
|
|
192
|
+
status: transcript ? ("ready" as const) : ("failed" as const),
|
|
193
|
+
failureReason: transcript ? null : loomTranscriptUnavailableMessage(),
|
|
194
|
+
updatedAt: now,
|
|
195
|
+
};
|
|
196
|
+
const [existingTranscript] = await db
|
|
197
|
+
.select({ recordingId: schema.recordingTranscripts.recordingId })
|
|
198
|
+
.from(schema.recordingTranscripts)
|
|
199
|
+
.where(eq(schema.recordingTranscripts.recordingId, recordingId));
|
|
200
|
+
if (existingTranscript) {
|
|
201
|
+
await db
|
|
202
|
+
.update(schema.recordingTranscripts)
|
|
203
|
+
.set(transcriptValues)
|
|
204
|
+
.where(eq(schema.recordingTranscripts.recordingId, recordingId));
|
|
205
|
+
} else {
|
|
206
|
+
await db.insert(schema.recordingTranscripts).values({
|
|
207
|
+
recordingId,
|
|
208
|
+
...transcriptValues,
|
|
209
|
+
createdAt: now,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.warn("[clips] Loom transcript persistence skipped", {
|
|
214
|
+
recordingId,
|
|
215
|
+
error: err instanceof Error ? err.message : String(err),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
await writeAppState(`recording-upload-${recordingId}`, {
|
|
221
|
+
recordingId,
|
|
222
|
+
status: "ready",
|
|
223
|
+
progress: 100,
|
|
224
|
+
videoUrl: upload.url,
|
|
225
|
+
updatedAt: now,
|
|
226
|
+
});
|
|
227
|
+
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.warn("[clips] Loom ready state update skipped", {
|
|
230
|
+
recordingId,
|
|
231
|
+
error: err instanceof Error ? err.message : String(err),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await db
|
|
237
|
+
.update(schema.recordings)
|
|
238
|
+
.set({ loomImportClaimId: null, loomImportClaimedAt: null })
|
|
239
|
+
.where(
|
|
240
|
+
and(
|
|
241
|
+
eq(schema.recordings.id, recordingId),
|
|
242
|
+
eq(schema.recordings.loomImportClaimId, claimId),
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.warn("[clips] Loom import lease release failed", {
|
|
247
|
+
recordingId,
|
|
248
|
+
error: err instanceof Error ? err.message : String(err),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { status: "ready" };
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return failLoomImport(
|
|
255
|
+
recordingId,
|
|
256
|
+
err instanceof Error ? err.message : String(err),
|
|
257
|
+
claimId,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
IconLayoutSidebarLeftExpand,
|
|
31
31
|
IconPlus,
|
|
32
32
|
IconShare,
|
|
33
|
-
IconHierarchy2,
|
|
34
33
|
IconSettings,
|
|
35
34
|
IconSearch,
|
|
36
35
|
} from "@tabler/icons-react";
|
|
@@ -304,12 +303,6 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
304
303
|
icon: React.ComponentType<{ className?: string }>;
|
|
305
304
|
match: (path: string) => boolean;
|
|
306
305
|
}[] = [
|
|
307
|
-
{
|
|
308
|
-
to: "/agent",
|
|
309
|
-
label: t("settings.agentTitle"),
|
|
310
|
-
icon: IconHierarchy2,
|
|
311
|
-
match: (p) => p.startsWith("/agent"),
|
|
312
|
-
},
|
|
313
306
|
{
|
|
314
307
|
to: "/settings",
|
|
315
308
|
label: t("navigation.settings"),
|
|
@@ -6,7 +6,7 @@ import { LibraryGrid } from "@/components/library/library-grid";
|
|
|
6
6
|
import { usePageHeaderLayout } from "@/components/library/page-header";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
8
|
|
|
9
|
-
const SEO_TITLE = "
|
|
9
|
+
const SEO_TITLE = "Clips - Open Source screen recorder";
|
|
10
10
|
const SEO_DESCRIPTION =
|
|
11
11
|
"Open Source screen recorder and meeting-notes app with AI transcripts, summaries, search, dictation, and agent-readable share links.";
|
|
12
12
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DefaultSpinner } from "@agent-native/core/client/ui";
|
|
2
2
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
3
3
|
|
|
4
|
-
const SEO_TITLE = "
|
|
4
|
+
const SEO_TITLE = "Clips - Open Source screen recorder";
|
|
5
5
|
const SEO_DESCRIPTION =
|
|
6
6
|
"Open Source screen recorder and meeting-notes app with AI transcripts, summaries, search, dictation, and agent-readable share links.";
|
|
7
7
|
|
|
@@ -596,6 +596,12 @@ export default function RecordingPage() {
|
|
|
596
596
|
});
|
|
597
597
|
return;
|
|
598
598
|
}
|
|
599
|
+
if (retryingLoomImport && result?.status === "processing") {
|
|
600
|
+
// Download + reupload now run as a background job; this request only
|
|
601
|
+
// confirms the retry was accepted, not that the clip is ready yet.
|
|
602
|
+
toast.info(t("recordingPage.importingLoom"));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
599
605
|
toast.success(
|
|
600
606
|
retryingLoomImport
|
|
601
607
|
? t("recordingPage.loomImportResumed")
|
|
@@ -1850,6 +1850,11 @@ export default function RecordRoute() {
|
|
|
1850
1850
|
description: t("recordRoute.connectStorageToRetryLoom"),
|
|
1851
1851
|
duration: 12_000,
|
|
1852
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"));
|
|
1853
1858
|
} else {
|
|
1854
1859
|
showSavedToast(
|
|
1855
1860
|
t("recordRoute.loomImported"),
|
|
@@ -166,6 +166,8 @@ export const recordings = table("recordings", {
|
|
|
166
166
|
.default("uploading"),
|
|
167
167
|
uploadProgress: integer("upload_progress").notNull().default(0),
|
|
168
168
|
failureReason: text("failure_reason"),
|
|
169
|
+
loomImportClaimId: text("loom_import_claim_id"),
|
|
170
|
+
loomImportClaimedAt: text("loom_import_claimed_at"),
|
|
169
171
|
|
|
170
172
|
// Non-destructive edits: JSON `{ trims: [{startMs,endMs,excluded}], blurs: [...], speed: [...] }`
|
|
171
173
|
editsJson: text("edits_json").notNull().default("{}"),
|
|
@@ -7,7 +7,7 @@ export default createAuthPlugin({
|
|
|
7
7
|
// callback can handle both normal sign-in and the Calendar connect flow.
|
|
8
8
|
mountGoogleOAuthRoutes: false,
|
|
9
9
|
marketing: {
|
|
10
|
-
appName: "
|
|
10
|
+
appName: "Clips",
|
|
11
11
|
tagline:
|
|
12
12
|
"Your AI agent transcribes, summarizes, and searches everything you record alongside you.",
|
|
13
13
|
features: [
|
|
@@ -873,6 +873,14 @@ const migrations = runMigrations(
|
|
|
873
873
|
`CREATE INDEX IF NOT EXISTS recording_agent_views_recording_idx ON recording_agent_views (recording_id, last_seen_at)`,
|
|
874
874
|
].join("; "),
|
|
875
875
|
},
|
|
876
|
+
{
|
|
877
|
+
version: 52,
|
|
878
|
+
name: "recording-loom-import-claim-lease",
|
|
879
|
+
sql: [
|
|
880
|
+
`ALTER TABLE recordings ADD COLUMN IF NOT EXISTS loom_import_claim_id TEXT`,
|
|
881
|
+
`ALTER TABLE recordings ADD COLUMN IF NOT EXISTS loom_import_claimed_at TEXT`,
|
|
882
|
+
].join("; "),
|
|
883
|
+
},
|
|
876
884
|
],
|
|
877
885
|
{ table: "clips_migrations" },
|
|
878
886
|
);
|