@agent-native/core 0.122.0 → 0.122.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -12
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +9 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +57 -4
- 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/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/app/components/layout/Sidebar.tsx +1 -23
- package/corpus/templates/analytics/app/routes/_index.tsx +1 -1
- 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/plugins/auth.ts +1 -1
- 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/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-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/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/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-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/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/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-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/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/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/struct-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 +2 -2
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +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 +1 -1
- package/src/a2a/client.ts +57 -4
- 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/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
|
@@ -9,8 +9,7 @@ import { useEffect } from "react";
|
|
|
9
9
|
import { shouldEnableBrainProviderStatusChecks } from "@/lib/brain-chat-readiness";
|
|
10
10
|
import { TAB_ID } from "@/lib/tab-id";
|
|
11
11
|
|
|
12
|
-
const SEO_TITLE =
|
|
13
|
-
"Agent-Native Brain - Open Source company knowledge base for AI agents";
|
|
12
|
+
const SEO_TITLE = "Brain - Open Source company knowledge base for AI agents";
|
|
14
13
|
const SEO_DESCRIPTION =
|
|
15
14
|
"Open Source company knowledge base that turns Slack, meetings, transcripts, docs, and decisions into cited answers for AI agents.";
|
|
16
15
|
|
|
@@ -2,7 +2,7 @@ import { createAuthPlugin } from "@agent-native/core/server";
|
|
|
2
2
|
|
|
3
3
|
export default createAuthPlugin({
|
|
4
4
|
marketing: {
|
|
5
|
-
appName: "
|
|
5
|
+
appName: "Brain",
|
|
6
6
|
tagline:
|
|
7
7
|
"A company knowledge layer where raw conversations become reviewed, searchable institutional knowledge.",
|
|
8
8
|
features: [
|
|
@@ -6,7 +6,6 @@ import { OrgSwitcher } from "@agent-native/core/client/org";
|
|
|
6
6
|
import { FeedbackButton } from "@agent-native/core/client/ui";
|
|
7
7
|
import { SidebarFooterActions } from "@agent-native/toolkit/app-shell";
|
|
8
8
|
import {
|
|
9
|
-
IconHierarchy2,
|
|
10
9
|
IconCalendar,
|
|
11
10
|
IconSettings,
|
|
12
11
|
IconLink,
|
|
@@ -101,11 +100,6 @@ const navItems = [
|
|
|
101
100
|
];
|
|
102
101
|
|
|
103
102
|
const bottomNavItems = [
|
|
104
|
-
{
|
|
105
|
-
path: "/agent",
|
|
106
|
-
labelKey: "settings.agentTitle",
|
|
107
|
-
icon: IconHierarchy2,
|
|
108
|
-
},
|
|
109
103
|
{ path: "/settings", labelKey: "navigation.settings", icon: IconSettings },
|
|
110
104
|
];
|
|
111
105
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import CalendarView from "@/pages/CalendarView";
|
|
2
2
|
|
|
3
3
|
const SEO_TITLE =
|
|
4
|
-
"
|
|
4
|
+
"Calendar - Open Source AI scheduling and Google Calendar automation";
|
|
5
5
|
const SEO_DESCRIPTION =
|
|
6
6
|
"Open Source AI calendar app for Google Calendar scheduling, booking links, meeting coordination, and agent-managed event updates.";
|
|
7
7
|
|
|
@@ -8,7 +8,7 @@ export default createAuthPlugin({
|
|
|
8
8
|
googleOnly: true,
|
|
9
9
|
mountGoogleOAuthRoutes: false,
|
|
10
10
|
marketing: {
|
|
11
|
-
appName: "
|
|
11
|
+
appName: "Calendar",
|
|
12
12
|
tagline:
|
|
13
13
|
"Your AI agent schedules, reschedules, and manages your calendar so you never have to.",
|
|
14
14
|
features: [
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
type ChatHistoryItem,
|
|
15
15
|
} from "@agent-native/toolkit/chat-history";
|
|
16
16
|
import {
|
|
17
|
-
IconHierarchy2,
|
|
18
17
|
IconLayoutSidebarLeftCollapse,
|
|
19
18
|
IconLayoutSidebarLeftExpand,
|
|
20
19
|
IconMessageCircle,
|
|
@@ -43,12 +42,6 @@ const navItems = [
|
|
|
43
42
|
];
|
|
44
43
|
|
|
45
44
|
const bottomNavItems = [
|
|
46
|
-
{
|
|
47
|
-
icon: IconHierarchy2,
|
|
48
|
-
labelKey: "settings.agentTitle",
|
|
49
|
-
href: "/agent",
|
|
50
|
-
view: "agent",
|
|
51
|
-
},
|
|
52
45
|
{
|
|
53
46
|
icon: IconSettings,
|
|
54
47
|
labelKey: "navigation.settings",
|
|
@@ -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"),
|