@agent-native/core 0.84.20 → 0.84.22
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 +16 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/caller-auth.ts +37 -8
- package/corpus/core/src/a2a/client.ts +178 -67
- package/corpus/core/src/agent/production-agent.ts +19 -3
- package/corpus/core/src/agent/types.ts +1 -1
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/cli/recap.ts +43 -34
- package/corpus/core/src/client/AssistantChat.tsx +27 -4
- package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
- package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
- package/corpus/core/src/client/guided-questions.tsx +11 -6
- package/corpus/core/src/client/i18n.tsx +10 -5
- package/corpus/core/src/client/sse-event-processor.ts +147 -5
- package/corpus/core/src/client/tool-display.ts +8 -0
- package/corpus/core/src/file-upload/builder.ts +40 -14
- package/corpus/core/src/file-upload/types.ts +3 -0
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
- package/corpus/core/src/mcp/builtin-tools.ts +3 -0
- package/corpus/core/src/mcp/org-directory.ts +66 -20
- package/corpus/core/src/notifications/channels.ts +235 -38
- package/corpus/core/src/scripts/call-agent.ts +7 -2
- package/corpus/core/src/server/analytics.ts +32 -6
- package/corpus/core/src/server/ssr-handler.ts +31 -12
- package/corpus/templates/analytics/AGENTS.md +21 -0
- package/corpus/templates/analytics/README.md +35 -0
- package/corpus/templates/analytics/actions/delete-analytics-alert-rule.ts +23 -0
- package/corpus/templates/analytics/actions/list-analytics-alert-rules.ts +20 -0
- package/corpus/templates/analytics/actions/run-analytics-alerts.ts +23 -0
- package/corpus/templates/analytics/actions/save-analytics-alert-rule.ts +69 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
- package/corpus/templates/analytics/changelog/2026-07-01-analytics-can-alert-when-first-party-events-spike.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-play-back-in-production-instead-of-showing.md +6 -0
- package/corpus/templates/analytics/netlify.toml +3 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
- package/corpus/templates/analytics/server/db/schema.ts +55 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +5 -2
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +71 -0
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +734 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +10 -3
- package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +54 -0
- package/corpus/templates/analytics/server/plugins/db.ts +100 -0
- package/corpus/templates/analytics/server/routes/api/analytics-alerts/run.post.ts +60 -0
- package/corpus/templates/assets/README.md +30 -8
- package/corpus/templates/brain/README.md +20 -552
- package/corpus/templates/calendar/README.md +22 -28
- package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
- package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
- package/corpus/templates/chat/README.md +32 -0
- package/corpus/templates/clips/README.md +37 -0
- package/corpus/templates/clips/actions/create-recording.ts +7 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
- package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
- package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
- package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +18 -7
- package/corpus/templates/clips/app/components/library/recording-card.tsx +42 -5
- package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +106 -2
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
- package/corpus/templates/clips/app/routes/record.tsx +48 -8
- package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-move-to-folder-now-opens-folder-choices-directly-instead-of.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +64 -6
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
- package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +51 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
- package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
- package/corpus/templates/content/AGENTS.md +7 -3
- package/corpus/templates/content/README.md +22 -34
- package/corpus/templates/content/actions/list-trashed-content-databases.ts +4 -0
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +58 -6
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +257 -24
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +41 -15
- package/corpus/templates/content/app/i18n/zh-TW.ts +25 -7
- package/corpus/templates/content/app/i18n-data.ts +278 -40
- package/corpus/templates/content/changelog/2026-06-30-database-once-again-creates-an-inline-database-in-the-curren.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/design/AGENTS.md +7 -6
- package/corpus/templates/design/README.md +33 -0
- package/corpus/templates/design/actions/delete-file.ts +86 -10
- package/corpus/templates/design/actions/get-design-snapshot.ts +13 -0
- package/corpus/templates/design/actions/present-design-variants.ts +11 -13
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
- package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
- package/corpus/templates/dispatch/README.md +34 -0
- package/corpus/templates/forms/README.md +34 -0
- package/corpus/templates/macros/README.md +28 -0
- package/corpus/templates/mail/README.md +35 -0
- package/corpus/templates/plan/README.md +30 -118
- package/corpus/templates/plan/app/global.css +1 -1
- package/corpus/templates/slides/README.md +34 -0
- package/corpus/templates/videos/README.md +22 -201
- package/dist/a2a/caller-auth.d.ts +1 -0
- package/dist/a2a/caller-auth.d.ts.map +1 -1
- package/dist/a2a/caller-auth.js +34 -8
- package/dist/a2a/caller-auth.js.map +1 -1
- package/dist/a2a/client.d.ts +4 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +148 -58
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +19 -4
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/types.d.ts +1 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +3 -3
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +41 -34
- package/dist/cli/recap.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +28 -4
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/PoweredByBadge.d.ts +9 -3
- package/dist/client/PoweredByBadge.d.ts.map +1 -1
- package/dist/client/PoweredByBadge.js +47 -15
- package/dist/client/PoweredByBadge.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +5 -1
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +10 -6
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/client/i18n.d.ts +1 -1
- package/dist/client/i18n.d.ts.map +1 -1
- package/dist/client/i18n.js +6 -2
- package/dist/client/i18n.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +2 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +121 -5
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/tool-display.d.ts.map +1 -1
- package/dist/client/tool-display.js +8 -0
- package/dist/client/tool-display.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/file-upload/builder.d.ts.map +1 -1
- package/dist/file-upload/builder.js +26 -8
- package/dist/file-upload/builder.js.map +1 -1
- package/dist/file-upload/types.d.ts +5 -1
- package/dist/file-upload/types.d.ts.map +1 -1
- package/dist/file-upload/types.js.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +43 -18
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +3 -0
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/mcp/org-directory.d.ts.map +1 -1
- package/dist/mcp/org-directory.js +58 -20
- package/dist/mcp/org-directory.js.map +1 -1
- package/dist/notifications/channels.d.ts +10 -0
- package/dist/notifications/channels.d.ts.map +1 -1
- package/dist/notifications/channels.js +172 -20
- package/dist/notifications/channels.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +2 -2
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/analytics.d.ts +16 -0
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +30 -7
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +28 -12
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
OpenSourceBadge,
|
|
3
3
|
PoweredByBadge,
|
|
4
|
+
LanguagePicker,
|
|
4
5
|
StarfieldBackground,
|
|
5
6
|
useT,
|
|
6
7
|
} from "@agent-native/core/client";
|
|
@@ -53,13 +54,23 @@ function BookingPageShell({
|
|
|
53
54
|
return (
|
|
54
55
|
<div
|
|
55
56
|
className={cn(
|
|
56
|
-
"relative min-h-screen
|
|
57
|
+
"relative min-h-screen bg-background dark:bg-black",
|
|
57
58
|
className,
|
|
58
59
|
)}
|
|
59
60
|
>
|
|
60
61
|
<StarfieldBackground className="fixed inset-0 opacity-25 dark:opacity-60" />
|
|
61
|
-
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(ellipse_at_center,hsl(var(--background)/0.35)_0%,hsl(var(--background)/0.88)_72%)]" />
|
|
62
|
-
<div className="
|
|
62
|
+
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(ellipse_at_center,hsl(var(--background)/0.35)_0%,hsl(var(--background)/0.88)_72%)] dark:bg-[radial-gradient(ellipse_at_center,hsl(var(--background)/0.35)_0%,#000000_100%)]" />
|
|
63
|
+
<div className="fixed top-4 right-4 z-50 flex items-center gap-1">
|
|
64
|
+
<LanguagePicker variant="ghost-icon" />
|
|
65
|
+
<ThemeToggle />
|
|
66
|
+
</div>
|
|
67
|
+
<div className="fixed bottom-[21px] left-4 z-50 flex flex-col items-start gap-2 max-sm:static max-sm:mx-auto max-sm:mt-8">
|
|
68
|
+
<PoweredByBadge variant="plain" embedded />
|
|
69
|
+
<OpenSourceBadge embedded />
|
|
70
|
+
</div>
|
|
71
|
+
<div className="relative z-10 min-h-screen overflow-x-hidden p-4">
|
|
72
|
+
{children}
|
|
73
|
+
</div>
|
|
63
74
|
</div>
|
|
64
75
|
);
|
|
65
76
|
}
|
|
@@ -281,9 +292,6 @@ export default function BookingPage() {
|
|
|
281
292
|
|
|
282
293
|
return (
|
|
283
294
|
<BookingPageShell className="pb-20">
|
|
284
|
-
<div className="absolute top-4 right-4 z-20">
|
|
285
|
-
<ThemeToggle />
|
|
286
|
-
</div>
|
|
287
295
|
<div className="mx-auto mt-[7.5vh] w-full max-w-lg">
|
|
288
296
|
{/* Header */}
|
|
289
297
|
<div className="mb-8 text-center">
|
|
@@ -509,8 +517,6 @@ export default function BookingPage() {
|
|
|
509
517
|
)}
|
|
510
518
|
</div>
|
|
511
519
|
</div>
|
|
512
|
-
<OpenSourceBadge />
|
|
513
|
-
<PoweredByBadge />
|
|
514
520
|
</BookingPageShell>
|
|
515
521
|
);
|
|
516
522
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Chat
|
|
2
|
+
|
|
3
|
+
The minimal agent-native starter app — a clean, ChatGPT-style shell with chat at
|
|
4
|
+
the center, durable threads, standard app navigation, auth, live sync, and
|
|
5
|
+
actions. Start here when you want a real browser app to build on without
|
|
6
|
+
committing to a domain template.
|
|
7
|
+
|
|
8
|
+
**Live app: [chat.agent-native.com](https://chat.agent-native.com)**
|
|
9
|
+
|
|
10
|
+
Chat is the basic agent-native app starting point. It gives you the app-agent
|
|
11
|
+
loop wired end to end and one example action, so you can add your own UI, data,
|
|
12
|
+
and actions on top.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- ChatGPT-style shell with a threads list and durable chat history.
|
|
17
|
+
- Auth, live sync, and application state wired out of the box.
|
|
18
|
+
- The action surface the agent and UI share, plus one example action to copy.
|
|
19
|
+
- A minimal, brandable base for any domain app.
|
|
20
|
+
|
|
21
|
+
## Develop locally
|
|
22
|
+
|
|
23
|
+
Scaffold your own copy and run it:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @agent-native/core@latest create my-app --standalone --template chat
|
|
27
|
+
cd my-app
|
|
28
|
+
pnpm install
|
|
29
|
+
pnpm dev
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Full docs: [agent-native.com/docs/template-chat](https://agent-native.com/docs/template-chat).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Clips
|
|
2
|
+
|
|
3
|
+
An open-source, agent-native alternative to Loom, Granola, and Wispr Flow rolled
|
|
4
|
+
into one app. Screen recording, calendar-synced meeting notes, and push-to-talk
|
|
5
|
+
voice dictation — all transcribed, searchable, and yours to own.
|
|
6
|
+
|
|
7
|
+
**Live app: [clips.agent-native.com](https://clips.agent-native.com)**
|
|
8
|
+
|
|
9
|
+
The agent transcribes, titles, summarizes, and indexes everything you capture,
|
|
10
|
+
then lets you ask "find the clip where we discussed the rollout plan" across every
|
|
11
|
+
transcript. Shared clips are agent-readable: paste a share link into an agent and
|
|
12
|
+
it can read the transcript and see timestamped frames without the raw video.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Screen recording with webcam overlay, audio capture, and pause/trim.
|
|
17
|
+
- Calendar-synced meeting recordings with AI summary, notes, and action items.
|
|
18
|
+
- Push-to-talk (Fn-hold) dictation with searchable history and vocabulary biasing.
|
|
19
|
+
- Auto-generated titles, summaries, and chapter markers for every recording.
|
|
20
|
+
- Full-text search across recordings, meetings, and dictations in one library.
|
|
21
|
+
- Sharing with per-clip permissions, agent-readable links, and Slack unfurls.
|
|
22
|
+
- Chrome extension for capturing browser logs with a recording.
|
|
23
|
+
|
|
24
|
+
## Develop locally
|
|
25
|
+
|
|
26
|
+
Scaffold your own copy and run it:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @agent-native/core@latest create my-clips --standalone --template clips
|
|
30
|
+
cd my-clips
|
|
31
|
+
pnpm install
|
|
32
|
+
pnpm dev
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Clips needs a video storage backend (Builder.io or an S3-compatible bucket)
|
|
36
|
+
before recordings can upload — see the docs for setup.
|
|
37
|
+
Full docs: [agent-native.com/docs/template-clips](https://agent-native.com/docs/template-clips).
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
stringifySpaceIds,
|
|
24
24
|
} from "../server/lib/recordings.js";
|
|
25
25
|
import { setResumableSession } from "../server/lib/resumable-session.js";
|
|
26
|
+
import { isStreamingUploadDisabled } from "../server/lib/streaming-upload-mode.js";
|
|
26
27
|
import { createRecordingSchema } from "./lib/create-recording-schema.js";
|
|
27
28
|
import { DEFAULT_RECORDING_TITLE } from "./lib/title-source.js";
|
|
28
29
|
|
|
@@ -86,7 +87,11 @@ export default defineAction({
|
|
|
86
87
|
// init fails.
|
|
87
88
|
let uploadMode: UploadMode = "buffered";
|
|
88
89
|
const uploadProvider = await getActiveFileUploadProviderForRequest();
|
|
89
|
-
if (
|
|
90
|
+
if (
|
|
91
|
+
args.requestStreaming &&
|
|
92
|
+
!isStreamingUploadDisabled() &&
|
|
93
|
+
uploadProvider?.resumable
|
|
94
|
+
) {
|
|
90
95
|
try {
|
|
91
96
|
const recordingMimeType =
|
|
92
97
|
args.mimeType?.split(";")[0]?.trim() || "video/webm";
|
|
@@ -103,7 +108,7 @@ export default defineAction({
|
|
|
103
108
|
await setResumableSession(id, {
|
|
104
109
|
providerId: uploadProvider.id,
|
|
105
110
|
sessionId: session.sessionId,
|
|
106
|
-
meta: session.meta,
|
|
111
|
+
meta: { ...session.meta, skipCompressionWait: true },
|
|
107
112
|
bytesUploaded: 0,
|
|
108
113
|
lastCommittedIndex: -1,
|
|
109
114
|
});
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
deleteResumableSession,
|
|
40
40
|
getResumableSession,
|
|
41
41
|
} from "../server/lib/resumable-session.js";
|
|
42
|
+
import { isStreamingUploadDisabled } from "../server/lib/streaming-upload-mode.js";
|
|
42
43
|
import {
|
|
43
44
|
requiresConfiguredVideoStorage,
|
|
44
45
|
STORAGE_SETUP_REQUIRED_REASON,
|
|
@@ -341,6 +342,11 @@ export default defineAction({
|
|
|
341
342
|
// Resumable path: create-recording initialized a session and chunk.post.ts
|
|
342
343
|
// forwarded all chunks to the provider. Complete the session to get the CDN URL.
|
|
343
344
|
const resumableSession = await getResumableSession(id);
|
|
345
|
+
if (resumableSession && isStreamingUploadDisabled()) {
|
|
346
|
+
console.warn(
|
|
347
|
+
`[finalize] streaming uploads are disabled, but completing existing resumable session for in-flight recording: ${id}`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
344
350
|
if (resumableSession) {
|
|
345
351
|
debugLog("[finalize] resumable session found, completing upload", {
|
|
346
352
|
id,
|
|
@@ -351,6 +357,9 @@ export default defineAction({
|
|
|
351
357
|
if (!uploadProvider?.resumable) {
|
|
352
358
|
throw new Error("No resumable upload provider configured");
|
|
353
359
|
}
|
|
360
|
+
if (resumableSession.bytesUploaded <= 0) {
|
|
361
|
+
throw new Error("Recording upload contained no video bytes");
|
|
362
|
+
}
|
|
354
363
|
const videoUrl = await uploadProvider.resumable.completeSession(
|
|
355
364
|
{
|
|
356
365
|
sessionId: resumableSession.sessionId,
|
|
@@ -359,6 +368,7 @@ export default defineAction({
|
|
|
359
368
|
typeof resumableSession.meta.filename === "string"
|
|
360
369
|
? resumableSession.meta.filename
|
|
361
370
|
: "",
|
|
371
|
+
{ skipCompressionWait: true },
|
|
362
372
|
);
|
|
363
373
|
debugLog("[finalize] resumable upload completed", { id, videoUrl });
|
|
364
374
|
const result = await markRecordingReady({
|
|
@@ -560,6 +570,7 @@ export default defineAction({
|
|
|
560
570
|
filename: `${id}.${videoFormat}`,
|
|
561
571
|
mimeType,
|
|
562
572
|
ownerEmail,
|
|
573
|
+
skipCompressionWait: true,
|
|
563
574
|
});
|
|
564
575
|
} catch (err) {
|
|
565
576
|
// Capture structured context so a "Builder.io upload failed (500)" can
|
|
@@ -233,10 +233,12 @@ export default defineAction({
|
|
|
233
233
|
// `?password=<pw>` (legacy fallback) so old share pages keep
|
|
234
234
|
// working during rollout. (audit 11 F-07)
|
|
235
235
|
// Owners are skipped — the blob route bypasses the password gate
|
|
236
|
-
// for them, so they don't need the token.
|
|
237
|
-
//
|
|
236
|
+
// for them, so they don't need the token. Remote provider URLs are
|
|
237
|
+
// still proxied through same-origin media serving so CORS, range
|
|
238
|
+
// requests, and signed URL quirks match public share playback.
|
|
238
239
|
const resolvedVideoUrl = resolvePlayerVideoUrl(rec, {
|
|
239
240
|
addPasswordToken: access.role !== "owner",
|
|
241
|
+
proxyRemoteMedia: true,
|
|
240
242
|
});
|
|
241
243
|
|
|
242
244
|
return {
|
|
@@ -17,6 +17,18 @@ export type LoomVideoDownload = {
|
|
|
17
17
|
sourceUrl: string;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
const LOOM_VIDEO_UNAVAILABLE_MESSAGE =
|
|
21
|
+
"Loom did not provide a downloadable MP4 for this video. Download the original from Loom and use Upload video in Clips.";
|
|
22
|
+
|
|
23
|
+
export class LoomVideoUnavailableError extends Error {
|
|
24
|
+
statusCode = 422;
|
|
25
|
+
|
|
26
|
+
constructor(message = LOOM_VIDEO_UNAVAILABLE_MESSAGE) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "LoomVideoUnavailableError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
function formatBytes(bytes: number): string {
|
|
21
33
|
if (bytes >= 1024 * 1024) {
|
|
22
34
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -129,6 +141,10 @@ async function fetchTranscodedVideoUrl({
|
|
|
129
141
|
{ maxRedirects: 2 },
|
|
130
142
|
);
|
|
131
143
|
|
|
144
|
+
if (response.status === 204) {
|
|
145
|
+
throw new LoomVideoUnavailableError();
|
|
146
|
+
}
|
|
147
|
+
|
|
132
148
|
if (!response.ok) {
|
|
133
149
|
throw new Error(
|
|
134
150
|
`Loom did not provide a downloadable MP4 (${response.status} ${response.statusText}). Make sure the link is public and allows playback.`,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getBrowserTabId,
|
|
3
|
-
sendToAgentChat,
|
|
4
3
|
setClientAppState,
|
|
5
4
|
useSession,
|
|
6
5
|
useT,
|
|
@@ -225,6 +224,21 @@ export function LibraryGrid({
|
|
|
225
224
|
}
|
|
226
225
|
};
|
|
227
226
|
|
|
227
|
+
const moveSingle = async (
|
|
228
|
+
rec: RecordingSummary,
|
|
229
|
+
targetFolderId: string | null,
|
|
230
|
+
) => {
|
|
231
|
+
try {
|
|
232
|
+
await moveRecording.mutateAsync({
|
|
233
|
+
id: rec.id,
|
|
234
|
+
folderId: targetFolderId,
|
|
235
|
+
});
|
|
236
|
+
toast.success(t("libraryGrid.clipsMoved", { count: 1 }));
|
|
237
|
+
} catch (err: any) {
|
|
238
|
+
toast.error(err?.message ?? t("libraryGrid.moveFailed"));
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
228
242
|
const openRenameDialog = (rec: RecordingSummary) => {
|
|
229
243
|
setRenamingRec(rec);
|
|
230
244
|
setRenameValue(isDefaultTitle(rec.title) ? "" : (rec.title ?? ""));
|
|
@@ -385,12 +399,9 @@ export function LibraryGrid({
|
|
|
385
399
|
? openRenameDialog
|
|
386
400
|
: undefined
|
|
387
401
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
background: false,
|
|
392
|
-
});
|
|
393
|
-
}}
|
|
402
|
+
moveTargets={moveTargets}
|
|
403
|
+
onMove={canMoveSelection ? moveSingle : undefined}
|
|
404
|
+
isMovePending={moveRecording.isPending}
|
|
394
405
|
onTrash={(rec) => {
|
|
395
406
|
trashRecording.mutate(
|
|
396
407
|
{ id: rec.id },
|
|
@@ -24,6 +24,9 @@ import {
|
|
|
24
24
|
DropdownMenuContent,
|
|
25
25
|
DropdownMenuItem,
|
|
26
26
|
DropdownMenuSeparator,
|
|
27
|
+
DropdownMenuSub,
|
|
28
|
+
DropdownMenuSubContent,
|
|
29
|
+
DropdownMenuSubTrigger,
|
|
27
30
|
DropdownMenuTrigger,
|
|
28
31
|
} from "@/components/ui/dropdown-menu";
|
|
29
32
|
import { isDefaultTitle } from "@/hooks/use-auto-title";
|
|
@@ -32,6 +35,8 @@ import { isStaleRecordingUpload } from "@/lib/recording-status";
|
|
|
32
35
|
import { isStorageSetupFailureReason } from "@/lib/storage-failures";
|
|
33
36
|
import { cn } from "@/lib/utils";
|
|
34
37
|
|
|
38
|
+
import type { BulkMoveTarget } from "./bulk-action-toolbar";
|
|
39
|
+
|
|
35
40
|
function formatDuration(ms: number): string {
|
|
36
41
|
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
37
42
|
const m = Math.floor(totalSeconds / 60);
|
|
@@ -59,7 +64,9 @@ interface RecordingCardProps {
|
|
|
59
64
|
selectionMode?: boolean;
|
|
60
65
|
onToggleSelect?: (id: string) => void;
|
|
61
66
|
onShare?: (rec: RecordingSummary) => void;
|
|
62
|
-
onMove?: (rec: RecordingSummary) => void;
|
|
67
|
+
onMove?: (rec: RecordingSummary, folderId: string | null) => void;
|
|
68
|
+
moveTargets?: BulkMoveTarget[];
|
|
69
|
+
isMovePending?: boolean;
|
|
63
70
|
onRename?: (rec: RecordingSummary) => void;
|
|
64
71
|
onArchive?: (rec: RecordingSummary) => void;
|
|
65
72
|
onTrash?: (rec: RecordingSummary) => void;
|
|
@@ -73,6 +80,8 @@ export function RecordingCard({
|
|
|
73
80
|
onToggleSelect,
|
|
74
81
|
onShare,
|
|
75
82
|
onMove,
|
|
83
|
+
moveTargets = [],
|
|
84
|
+
isMovePending = false,
|
|
76
85
|
onRename,
|
|
77
86
|
onArchive,
|
|
78
87
|
onTrash,
|
|
@@ -112,6 +121,7 @@ export function RecordingCard({
|
|
|
112
121
|
/native recording|native fullscreen|screencapture|avconvert/i.test(
|
|
113
122
|
recording.failureReason ?? "",
|
|
114
123
|
);
|
|
124
|
+
const canMove = Boolean(onMove && moveTargets.length > 0);
|
|
115
125
|
|
|
116
126
|
const displayThumbnail = useMemo(() => {
|
|
117
127
|
if (hovered && recording.animatedThumbnailUrl)
|
|
@@ -332,10 +342,37 @@ export function RecordingCard({
|
|
|
332
342
|
<IconShare className="h-4 w-4 me-2" />{" "}
|
|
333
343
|
{t("recordingPage.share")}
|
|
334
344
|
</DropdownMenuItem>
|
|
335
|
-
|
|
336
|
-
<
|
|
337
|
-
|
|
338
|
-
|
|
345
|
+
{canMove ? (
|
|
346
|
+
<DropdownMenuSub>
|
|
347
|
+
<DropdownMenuSubTrigger>
|
|
348
|
+
<IconFolder className="h-4 w-4 me-2" />{" "}
|
|
349
|
+
{t("clipsFinalRaw.moveToFolder")}
|
|
350
|
+
</DropdownMenuSubTrigger>
|
|
351
|
+
<DropdownMenuSubContent className="w-64">
|
|
352
|
+
{moveTargets.map((target, index) => (
|
|
353
|
+
<DropdownMenuItem
|
|
354
|
+
key={target.id ?? `root-${index}`}
|
|
355
|
+
disabled={target.disabled || isMovePending}
|
|
356
|
+
onSelect={() => onMove?.(recording, target.id)}
|
|
357
|
+
>
|
|
358
|
+
<span
|
|
359
|
+
className="truncate"
|
|
360
|
+
style={{
|
|
361
|
+
paddingInlineStart: (target.depth ?? 0) * 12,
|
|
362
|
+
}}
|
|
363
|
+
>
|
|
364
|
+
{target.name}
|
|
365
|
+
</span>
|
|
366
|
+
{target.disabled && (
|
|
367
|
+
<span className="ms-auto text-xs text-muted-foreground">
|
|
368
|
+
{t("clipsFinalRaw.current")}
|
|
369
|
+
</span>
|
|
370
|
+
)}
|
|
371
|
+
</DropdownMenuItem>
|
|
372
|
+
))}
|
|
373
|
+
</DropdownMenuSubContent>
|
|
374
|
+
</DropdownMenuSub>
|
|
375
|
+
) : null}
|
|
339
376
|
{onRename ? (
|
|
340
377
|
<>
|
|
341
378
|
<DropdownMenuSeparator />
|
|
@@ -153,11 +153,11 @@ export function Scrubber(props: ScrubberProps) {
|
|
|
153
153
|
<div
|
|
154
154
|
ref={barRef}
|
|
155
155
|
data-player-scrubber-bar
|
|
156
|
-
className="relative w-full h-1.5 bg-white/
|
|
156
|
+
className="relative w-full h-1.5 bg-white/35 rounded-full cursor-pointer group/bar shadow-[0_0_0_1px_rgba(0,0,0,0.16)]"
|
|
157
157
|
>
|
|
158
158
|
{/* Filled portion */}
|
|
159
159
|
<div
|
|
160
|
-
className="absolute inset-y-0 left-0 bg-
|
|
160
|
+
className="absolute inset-y-0 left-0 bg-white rounded-full shadow-[0_0_10px_rgba(255,255,255,0.45)]"
|
|
161
161
|
style={{ width: pct + "%" }}
|
|
162
162
|
/>
|
|
163
163
|
|
|
@@ -95,10 +95,11 @@ export function CountdownOverlay({
|
|
|
95
95
|
|
|
96
96
|
<div
|
|
97
97
|
key={remaining}
|
|
98
|
-
className="flex
|
|
98
|
+
className="flex min-w-32 items-center justify-center text-[clamp(96px,22vmin,240px)] font-extrabold leading-none text-white duration-200 animate-in zoom-in-75 fade-in"
|
|
99
99
|
style={{
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
fontVariantNumeric: "tabular-nums",
|
|
101
|
+
textShadow:
|
|
102
|
+
"1px 0 1px rgba(17,24,39,0.22), -1px 0 1px rgba(17,24,39,0.22), 0 1px 1px rgba(17,24,39,0.22), 0 -1px 1px rgba(17,24,39,0.22), 0 2px 4px rgba(0,0,0,0.48), 0 12px 34px rgba(0,0,0,0.36), 0 0 2px rgba(0,0,0,0.72)",
|
|
102
103
|
}}
|
|
103
104
|
>
|
|
104
105
|
{remaining > 0 ? remaining : "Go"}
|
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
import {
|
|
59
59
|
NO_CAMERA_DEVICE_ID,
|
|
60
60
|
NO_MIC_DEVICE_ID,
|
|
61
|
+
normalizeDisplaySurfaceForRuntime,
|
|
62
|
+
supportsBrowserTabCapture,
|
|
61
63
|
type DisplaySurface,
|
|
62
64
|
type RecordingMode,
|
|
63
65
|
} from "./recorder-engine";
|
|
@@ -157,14 +159,20 @@ export function PreRecordPanel({
|
|
|
157
159
|
const t = useT();
|
|
158
160
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
159
161
|
const loomInputRef = useRef<HTMLInputElement>(null);
|
|
162
|
+
const browserTabCaptureSupported = useMemo(
|
|
163
|
+
() => supportsBrowserTabCapture(),
|
|
164
|
+
[],
|
|
165
|
+
);
|
|
160
166
|
// Saved selections from the last visit. A `?mode=`/`?surface=` deep link
|
|
161
167
|
// (initialMode/initialDisplaySurface) still takes precedence over them.
|
|
162
168
|
const savedPrefs = useMemo(() => loadRecorderPreferences(), []);
|
|
163
169
|
const [mode, setMode] = useState<RecordingMode>(
|
|
164
170
|
() => initialMode ?? savedPrefs.mode ?? "screen+camera",
|
|
165
171
|
);
|
|
166
|
-
const [displaySurface, setDisplaySurface] = useState<DisplaySurface>(
|
|
167
|
-
(
|
|
172
|
+
const [displaySurface, setDisplaySurface] = useState<DisplaySurface>(() =>
|
|
173
|
+
normalizeDisplaySurfaceForRuntime(
|
|
174
|
+
initialDisplaySurface ?? savedPrefs.displaySurface ?? "window",
|
|
175
|
+
),
|
|
168
176
|
);
|
|
169
177
|
const [sourceOpen, setSourceOpen] = useState(false);
|
|
170
178
|
const [deviceSettingsOpen, setDeviceSettingsOpen] = useState(false);
|
|
@@ -223,8 +231,8 @@ export function PreRecordPanel({
|
|
|
223
231
|
[isMobile, modeOptions],
|
|
224
232
|
);
|
|
225
233
|
|
|
226
|
-
const surfaceOptions = useMemo<SurfaceOption[]>(
|
|
227
|
-
|
|
234
|
+
const surfaceOptions = useMemo<SurfaceOption[]>(() => {
|
|
235
|
+
const options: SurfaceOption[] = [
|
|
228
236
|
{
|
|
229
237
|
value: "window",
|
|
230
238
|
label: t("preRecord.surfaceWindow"),
|
|
@@ -243,9 +251,11 @@ export function PreRecordPanel({
|
|
|
243
251
|
icon: IconDeviceScreen,
|
|
244
252
|
sub: t("preRecord.surfaceScreenDescription"),
|
|
245
253
|
},
|
|
246
|
-
]
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
];
|
|
255
|
+
return browserTabCaptureSupported
|
|
256
|
+
? options
|
|
257
|
+
: options.filter((option) => option.value !== "browser");
|
|
258
|
+
}, [browserTabCaptureSupported, t]);
|
|
249
259
|
|
|
250
260
|
useEffect(() => {
|
|
251
261
|
if (isMobile) {
|
|
@@ -258,9 +268,19 @@ export function PreRecordPanel({
|
|
|
258
268
|
}, [initialMode, isMobile]);
|
|
259
269
|
|
|
260
270
|
useEffect(() => {
|
|
261
|
-
if (initialDisplaySurface)
|
|
271
|
+
if (initialDisplaySurface) {
|
|
272
|
+
setDisplaySurface(
|
|
273
|
+
normalizeDisplaySurfaceForRuntime(initialDisplaySurface),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
262
276
|
}, [initialDisplaySurface]);
|
|
263
277
|
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (displaySurface !== "browser" || browserTabCaptureSupported) return;
|
|
280
|
+
setDisplaySurface("window");
|
|
281
|
+
saveRecorderPreferences({ displaySurface: "window" });
|
|
282
|
+
}, [browserTabCaptureSupported, displaySurface]);
|
|
283
|
+
|
|
264
284
|
const enumerateDevices = useCallback(async () => {
|
|
265
285
|
try {
|
|
266
286
|
if (!navigator.mediaDevices?.enumerateDevices) {
|
|
@@ -350,8 +370,9 @@ export function PreRecordPanel({
|
|
|
350
370
|
saveRecorderPreferences({ mode: value });
|
|
351
371
|
}, []);
|
|
352
372
|
const chooseDisplaySurface = useCallback((value: DisplaySurface) => {
|
|
353
|
-
|
|
354
|
-
|
|
373
|
+
const next = normalizeDisplaySurfaceForRuntime(value);
|
|
374
|
+
setDisplaySurface(next);
|
|
375
|
+
saveRecorderPreferences({ displaySurface: next });
|
|
355
376
|
}, []);
|
|
356
377
|
const chooseMic = useCallback((value: string) => {
|
|
357
378
|
setMicId(value);
|
|
@@ -676,7 +697,12 @@ export function PreRecordPanel({
|
|
|
676
697
|
</button>
|
|
677
698
|
</CollapsibleTrigger>
|
|
678
699
|
<CollapsibleContent>
|
|
679
|
-
<div
|
|
700
|
+
<div
|
|
701
|
+
className={cn(
|
|
702
|
+
"grid gap-2 px-6 pb-5",
|
|
703
|
+
surfaceOptions.length === 2 ? "grid-cols-2" : "grid-cols-3",
|
|
704
|
+
)}
|
|
705
|
+
>
|
|
680
706
|
{surfaceOptions.map((opt) => {
|
|
681
707
|
const Icon = opt.icon;
|
|
682
708
|
const active = opt.value === displaySurface;
|
|
@@ -913,7 +939,8 @@ export function PreRecordPanel({
|
|
|
913
939
|
// to acquire a webcam stream.
|
|
914
940
|
mode:
|
|
915
941
|
mode === "screen+camera" && !needsCamera ? "screen" : mode,
|
|
916
|
-
displaySurface
|
|
942
|
+
displaySurface:
|
|
943
|
+
normalizeDisplaySurfaceForRuntime(displaySurface),
|
|
917
944
|
micDeviceId: micId === "default" ? null : micId,
|
|
918
945
|
cameraDeviceId:
|
|
919
946
|
needsCamera && cameraId !== "default" ? cameraId : null,
|