@agent-native/core 0.69.0 → 0.70.0
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 +2 -2
- package/corpus/core/CHANGELOG.md +48 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/changelog/parse.ts +245 -0
- package/corpus/core/src/cli/changelog.ts +199 -0
- package/corpus/core/src/cli/index.ts +19 -0
- package/corpus/core/src/cli/recap.ts +24 -1
- package/corpus/core/src/cli/skills.ts +72 -45
- package/corpus/core/src/client/CommandMenu.tsx +183 -75
- package/corpus/core/src/client/agent-chat-adapter.ts +44 -7
- package/corpus/core/src/client/analytics.ts +181 -0
- package/corpus/core/src/client/changelog/Changelog.tsx +321 -0
- package/corpus/core/src/client/index.ts +11 -0
- package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +54 -0
- package/corpus/core/src/client/settings/useBuilderStatus.ts +82 -20
- package/corpus/core/src/mcp/build-server.ts +1 -1
- package/corpus/core/src/mcp/embed-app.ts +112 -6
- package/corpus/core/src/server/attribution.ts +211 -0
- package/corpus/core/src/server/auth.ts +6 -0
- package/corpus/core/src/server/better-auth-instance.ts +49 -5
- package/corpus/core/src/server/builder-browser.ts +27 -1
- package/corpus/core/src/server/core-routes-plugin.ts +58 -0
- package/corpus/core/src/server/google-oauth.ts +19 -6
- package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
- package/corpus/core/src/styles/agent-conversation.css +21 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/corpus/templates/analytics/CHANGELOG.md +10 -0
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +2 -2
- package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +5 -3
- package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +180 -144
- package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -2
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +221 -213
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +6 -3
- package/corpus/templates/analytics/app/lib/demo-dashboard-path.ts +1 -1
- package/corpus/templates/analytics/app/lib/last-opened.ts +2 -1
- package/corpus/templates/analytics/app/pages/Settings.tsx +4 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +2 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +101 -99
- package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +1 -1
- package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +18 -5
- package/corpus/templates/analytics/app/routes/catalog.tsx +1 -1
- package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +9 -0
- package/corpus/templates/analytics/app/routes/traffic.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-06-23-the-dashboards-and-analyses-sidebar-settings-buttons-now-sta.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +145 -0
- package/corpus/templates/analytics/server/db/index.ts +1 -1
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -2
- package/corpus/templates/analytics/server/lib/demo-dashboards.ts +1 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -2
- package/corpus/templates/analytics/server/plugins/core-routes.ts +6 -6
- package/corpus/templates/assets/CHANGELOG.md +10 -0
- package/corpus/templates/assets/app/root.tsx +7 -1
- package/corpus/templates/assets/app/routes/settings.tsx +6 -0
- package/corpus/templates/brain/CHANGELOG.md +10 -0
- package/corpus/templates/brain/app/root.tsx +7 -1
- package/corpus/templates/brain/app/routes/settings.tsx +8 -1
- package/corpus/templates/calendar/CHANGELOG.md +10 -0
- package/corpus/templates/calendar/app/pages/Settings.tsx +4 -0
- package/corpus/templates/calendar/app/root.tsx +7 -1
- package/corpus/templates/chat/CHANGELOG.md +10 -0
- package/corpus/templates/chat/app/root.tsx +7 -1
- package/corpus/templates/clips/CHANGELOG.md +10 -0
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +33 -7
- package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +8 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +150 -20
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +7 -11
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +31 -0
- package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +71 -0
- package/corpus/templates/clips/app/root.tsx +7 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +15 -1
- package/corpus/templates/clips/app/routes/record.tsx +78 -5
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -5
- package/corpus/templates/clips/changelog/2026-06-23-clips-shared-in-slack-no-longer-show-a-could-not-start-playb.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-made-the-play-button-on-shared-and-embedded-clips-scale-with.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-public-clip-share-links-now-show-whether-they-ll-play-inline.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-screen-camera-recordings-now-keep-the-presenter-s-face-in-th.md +6 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/region-record-border.tsx +76 -0
- package/corpus/templates/clips/desktop/src/styles.css +49 -0
- package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +67 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +4 -1
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +51 -1
- package/corpus/templates/clips/shared/share-attribution.ts +79 -0
- package/corpus/templates/content/CHANGELOG.md +10 -0
- package/corpus/templates/content/app/root.tsx +7 -1
- package/corpus/templates/design/CHANGELOG.md +10 -0
- package/corpus/templates/design/actions/generate-design.ts +4 -0
- package/corpus/templates/design/app/root.tsx +7 -1
- package/corpus/templates/design/app/routes/settings.tsx +10 -2
- package/corpus/templates/dispatch/CHANGELOG.md +10 -0
- package/corpus/templates/dispatch/app/root.tsx +7 -1
- package/corpus/templates/forms/CHANGELOG.md +10 -0
- package/corpus/templates/forms/app/root.tsx +7 -1
- package/corpus/templates/macros/CHANGELOG.md +10 -0
- package/corpus/templates/macros/app/root.tsx +7 -1
- package/corpus/templates/mail/CHANGELOG.md +10 -0
- package/corpus/templates/mail/app/components/layout/CommandPalette.tsx +3 -0
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +27 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +31 -17
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -4
- package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +34 -24
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -4
- package/corpus/templates/plan/AGENTS.md +4 -0
- package/corpus/templates/plan/CHANGELOG.md +10 -0
- package/corpus/templates/plan/actions/view-screen.ts +5 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +98 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +107 -7
- package/corpus/templates/plan/app/root.tsx +7 -1
- package/corpus/templates/plan/shared/share-attribution.ts +72 -0
- package/corpus/templates/slides/CHANGELOG.md +10 -0
- package/corpus/templates/slides/app/root.tsx +7 -1
- package/corpus/templates/videos/CHANGELOG.md +4 -151
- package/corpus/templates/videos/app/root.tsx +7 -1
- package/dist/changelog/parse.d.ts +70 -0
- package/dist/changelog/parse.d.ts.map +1 -0
- package/dist/changelog/parse.js +200 -0
- package/dist/changelog/parse.js.map +1 -0
- package/dist/cli/changelog.d.ts +4 -0
- package/dist/cli/changelog.d.ts.map +1 -0
- package/dist/cli/changelog.js +166 -0
- package/dist/cli/changelog.js.map +1 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +22 -1
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -3
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +72 -45
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/CommandMenu.d.ts +17 -1
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +49 -13
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +43 -6
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts +18 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +177 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/changelog/Changelog.d.ts +47 -0
- package/dist/client/changelog/Changelog.d.ts.map +1 -0
- package/dist/client/changelog/Changelog.js +138 -0
- package/dist/client/changelog/Changelog.js.map +1 -0
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +42 -1
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +2 -0
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +58 -10
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/mcp/build-server.js +1 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +112 -6
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/server/attribution.d.ts +83 -0
- package/dist/server/attribution.d.ts.map +1 -0
- package/dist/server/attribution.js +178 -0
- package/dist/server/attribution.js.map +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +7 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +9 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +32 -2
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/builder-browser.d.ts +4 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +20 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +25 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +38 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +16 -2
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/styles/agent-conversation.css +21 -0
- package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- /package/corpus/templates/analytics/app/routes/{dashboards.tsx → dashboards._index.tsx} +0 -0
|
@@ -16,10 +16,18 @@ import {
|
|
|
16
16
|
appApiPath,
|
|
17
17
|
openBuilderConnectPopup,
|
|
18
18
|
useActionQuery,
|
|
19
|
+
ChangelogSettingsCard,
|
|
19
20
|
} from "@agent-native/core/client";
|
|
21
|
+
import changelog from "../../CHANGELOG.md?raw";
|
|
20
22
|
import { toast } from "sonner";
|
|
21
23
|
import { Button } from "@/components/ui/button";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
Card,
|
|
26
|
+
CardContent,
|
|
27
|
+
CardDescription,
|
|
28
|
+
CardHeader,
|
|
29
|
+
CardTitle,
|
|
30
|
+
} from "@/components/ui/card";
|
|
23
31
|
import { Input } from "@/components/ui/input";
|
|
24
32
|
import { Label } from "@/components/ui/label";
|
|
25
33
|
import { PageHeader } from "@/components/library/page-header";
|
|
@@ -389,6 +397,8 @@ export default function SettingsIndexRoute() {
|
|
|
389
397
|
Preferences and connected services for this Clips workspace.
|
|
390
398
|
</p>
|
|
391
399
|
|
|
400
|
+
<ChangelogSettingsCard markdown={changelog} />
|
|
401
|
+
|
|
392
402
|
<Card id="video-storage" className="scroll-mt-16">
|
|
393
403
|
<CardHeader>
|
|
394
404
|
<CardTitle className="text-base flex items-center gap-2">
|
|
@@ -462,6 +472,10 @@ export default function SettingsIndexRoute() {
|
|
|
462
472
|
<IconBrandSlack className="size-4 text-primary" />
|
|
463
473
|
Agent-Native Clips for Slack
|
|
464
474
|
</CardTitle>
|
|
475
|
+
<CardDescription>
|
|
476
|
+
Share a public clip, paste the link in Slack, and it plays inline
|
|
477
|
+
— no extra steps for viewers. Connect each workspace once.
|
|
478
|
+
</CardDescription>
|
|
465
479
|
</CardHeader>
|
|
466
480
|
<CardContent className="space-y-4">
|
|
467
481
|
<div className="flex flex-col gap-3 rounded-md border border-border bg-accent/30 px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
@@ -486,6 +486,65 @@ function captureThumbnailFromPreview(
|
|
|
486
486
|
});
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
+
/** Resolve once the off-screen video has a decoded frame, or after a timeout. */
|
|
490
|
+
function waitForVideoFrame(
|
|
491
|
+
video: HTMLVideoElement,
|
|
492
|
+
timeoutMs: number,
|
|
493
|
+
): Promise<void> {
|
|
494
|
+
if (video.videoWidth > 0 && video.readyState >= 2) return Promise.resolve();
|
|
495
|
+
return new Promise((resolve) => {
|
|
496
|
+
let settled = false;
|
|
497
|
+
const finish = () => {
|
|
498
|
+
if (settled) return;
|
|
499
|
+
settled = true;
|
|
500
|
+
video.removeEventListener("loadeddata", onReady);
|
|
501
|
+
video.removeEventListener("canplay", onReady);
|
|
502
|
+
clearTimeout(timer);
|
|
503
|
+
resolve();
|
|
504
|
+
};
|
|
505
|
+
const onReady = () => {
|
|
506
|
+
if (video.videoWidth > 0 && video.readyState >= 2) finish();
|
|
507
|
+
};
|
|
508
|
+
video.addEventListener("loadeddata", onReady);
|
|
509
|
+
video.addEventListener("canplay", onReady);
|
|
510
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
511
|
+
onReady();
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Capture a thumbnail from a MediaStream that isn't bound to a visible element
|
|
517
|
+
* — used for the screen+camera composite so the thumbnail includes the camera
|
|
518
|
+
* bubble. Best effort: if it misses, the player's backfill path (which reads
|
|
519
|
+
* the recorded file, also composited) still produces a face thumbnail.
|
|
520
|
+
*/
|
|
521
|
+
function captureThumbnailFromStream(
|
|
522
|
+
stream: MediaStream,
|
|
523
|
+
recordingId: string,
|
|
524
|
+
): void {
|
|
525
|
+
const video = document.createElement("video");
|
|
526
|
+
video.srcObject = stream;
|
|
527
|
+
video.muted = true;
|
|
528
|
+
video.playsInline = true;
|
|
529
|
+
void (async () => {
|
|
530
|
+
try {
|
|
531
|
+
await video.play().catch(() => {});
|
|
532
|
+
await waitForVideoFrame(video, 1500);
|
|
533
|
+
const blob = await captureVideoThumbnailBlob(video);
|
|
534
|
+
if (blob) await uploadRecordingThumbnail(recordingId, blob);
|
|
535
|
+
} catch {
|
|
536
|
+
// best effort — the player has a backfill path if this misses.
|
|
537
|
+
} finally {
|
|
538
|
+
try {
|
|
539
|
+
video.pause();
|
|
540
|
+
} catch {
|
|
541
|
+
// ignore
|
|
542
|
+
}
|
|
543
|
+
video.srcObject = null;
|
|
544
|
+
}
|
|
545
|
+
})();
|
|
546
|
+
}
|
|
547
|
+
|
|
489
548
|
interface PendingRecording {
|
|
490
549
|
id: string;
|
|
491
550
|
uploadChunkUrl: string;
|
|
@@ -1024,7 +1083,14 @@ export default function RecordRoute() {
|
|
|
1024
1083
|
// chose a specific mic, do not start a parallel system-default mic
|
|
1025
1084
|
// session for instant transcription; the recorded audio still uses
|
|
1026
1085
|
// the exact selected device and can be transcribed after upload.
|
|
1027
|
-
|
|
1086
|
+
//
|
|
1087
|
+
// The one exception is when a stale saved mic forced the engine to
|
|
1088
|
+
// fall back to the system default during acquire(): the recording is
|
|
1089
|
+
// now on the default device, so live transcription would use the same
|
|
1090
|
+
// mic and is safe to start.
|
|
1091
|
+
const usingDefaultMic =
|
|
1092
|
+
!opts.micDeviceId || engine.didMicFallBackToDefault();
|
|
1093
|
+
if (wantsMic && usingDefaultMic && liveTranscription.supported) {
|
|
1028
1094
|
liveTranscription.start();
|
|
1029
1095
|
}
|
|
1030
1096
|
|
|
@@ -1673,10 +1739,17 @@ export default function RecordRoute() {
|
|
|
1673
1739
|
}
|
|
1674
1740
|
setUiState("uploading");
|
|
1675
1741
|
try {
|
|
1676
|
-
// Capture a still-frame thumbnail
|
|
1677
|
-
//
|
|
1678
|
-
//
|
|
1679
|
-
|
|
1742
|
+
// Capture a still-frame thumbnail while the stream is still live —
|
|
1743
|
+
// otherwise the library would show a blank card until the owner opens the
|
|
1744
|
+
// recording and triggers the player's backfill path. In screen+camera
|
|
1745
|
+
// mode the visible preview is screen-only, so grab the composited stream
|
|
1746
|
+
// (screen + camera bubble) to keep the presenter's face in the thumbnail.
|
|
1747
|
+
const compositeStream = engine.getCompositeStream();
|
|
1748
|
+
if (compositeStream) {
|
|
1749
|
+
captureThumbnailFromStream(compositeStream, pending.id);
|
|
1750
|
+
} else {
|
|
1751
|
+
captureThumbnailFromPreview(previewVideoRef.current, pending.id);
|
|
1752
|
+
}
|
|
1680
1753
|
|
|
1681
1754
|
// Stop live transcription and save the native web transcript before the
|
|
1682
1755
|
// engine finalizes. This gives the recording an instant transcript
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useCallback,
|
|
3
3
|
useEffect,
|
|
4
|
+
useMemo,
|
|
4
5
|
useRef,
|
|
5
6
|
useState,
|
|
6
7
|
type ReactNode,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
agentNativePath,
|
|
23
24
|
appBasePath,
|
|
24
25
|
appPath,
|
|
26
|
+
track,
|
|
25
27
|
useSession,
|
|
26
28
|
AgentPanel,
|
|
27
29
|
getBrowserTabId,
|
|
@@ -70,6 +72,10 @@ import {
|
|
|
70
72
|
clipsSharePageTitle,
|
|
71
73
|
displayRecordingTitle,
|
|
72
74
|
} from "../../shared/share-meta";
|
|
75
|
+
import {
|
|
76
|
+
buildSignupAttributionQuery,
|
|
77
|
+
readShareAttribution,
|
|
78
|
+
} from "../../shared/share-attribution";
|
|
73
79
|
|
|
74
80
|
type SharePageMetaRecording = {
|
|
75
81
|
id: string;
|
|
@@ -274,6 +280,61 @@ export default function ShareRoute() {
|
|
|
274
280
|
const loaderData = useLoaderData<typeof loader>() as SharePageLoaderData;
|
|
275
281
|
const { shareId } = useParams<{ shareId: string }>();
|
|
276
282
|
const navigate = useNavigate();
|
|
283
|
+
|
|
284
|
+
// Viral attribution: read the `ref`/`via` the visitor arrived on (the tagged
|
|
285
|
+
// share link) so we can fire funnel events and forward attribution into the
|
|
286
|
+
// signup URL even when cookies are blocked or `document.referrer` is empty.
|
|
287
|
+
const attribution = useMemo(
|
|
288
|
+
() =>
|
|
289
|
+
readShareAttribution(
|
|
290
|
+
typeof window === "undefined" ? "" : window.location.search,
|
|
291
|
+
),
|
|
292
|
+
[],
|
|
293
|
+
);
|
|
294
|
+
const recordingId = shareId ?? "";
|
|
295
|
+
|
|
296
|
+
// share_cta_click — fired alongside (never instead of) the real navigation.
|
|
297
|
+
// `track` is non-throwing, but guard anyway so tracking can never break a CTA.
|
|
298
|
+
const fireShareCtaClick = useCallback(
|
|
299
|
+
(cta: "signup" | "download" | "try_clips" | "signin") => {
|
|
300
|
+
try {
|
|
301
|
+
void track("share_cta_click", {
|
|
302
|
+
surface: "clip",
|
|
303
|
+
recording_id: recordingId,
|
|
304
|
+
cta,
|
|
305
|
+
ref: attribution.ref,
|
|
306
|
+
via: attribution.via,
|
|
307
|
+
});
|
|
308
|
+
} catch {
|
|
309
|
+
// Never let analytics break a CTA.
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
[recordingId, attribution.ref, attribution.via],
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Forward attribution into the signup URL so it survives blocked cookies.
|
|
316
|
+
const signupHref = appPath(
|
|
317
|
+
`/signup?${buildSignupAttributionQuery(attribution.via)}`,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// share_view — fire once when the public share page mounts. The ref guard
|
|
321
|
+
// prevents double-fire across re-renders / StrictMode double-invocation.
|
|
322
|
+
const shareViewFiredRef = useRef(false);
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
if (shareViewFiredRef.current) return;
|
|
325
|
+
shareViewFiredRef.current = true;
|
|
326
|
+
try {
|
|
327
|
+
void track("share_view", {
|
|
328
|
+
surface: "clip",
|
|
329
|
+
recording_id: recordingId,
|
|
330
|
+
ref: attribution.ref,
|
|
331
|
+
via: attribution.via,
|
|
332
|
+
});
|
|
333
|
+
} catch {
|
|
334
|
+
// Never let analytics break the page render.
|
|
335
|
+
}
|
|
336
|
+
}, [recordingId, attribution.ref, attribution.via]);
|
|
337
|
+
|
|
277
338
|
const playerRef = useRef<VideoPlayerHandle | null>(null);
|
|
278
339
|
const [password, setPassword] = useState<string | null>(() => {
|
|
279
340
|
if (typeof window === "undefined" || !shareId) return null;
|
|
@@ -690,7 +751,11 @@ export default function ShareRoute() {
|
|
|
690
751
|
</Button>
|
|
691
752
|
) : session ? null : (
|
|
692
753
|
<Button variant="ghost" size="sm" asChild>
|
|
693
|
-
<a
|
|
754
|
+
<a
|
|
755
|
+
href={appPath("/")}
|
|
756
|
+
className="gap-1.5"
|
|
757
|
+
onClick={() => fireShareCtaClick("try_clips")}
|
|
758
|
+
>
|
|
694
759
|
Try Clips
|
|
695
760
|
<IconExternalLink className="h-3.5 w-3.5" />
|
|
696
761
|
</a>
|
|
@@ -879,7 +944,10 @@ export default function ShareRoute() {
|
|
|
879
944
|
browserTabId={getBrowserTabId()}
|
|
880
945
|
/>
|
|
881
946
|
) : (
|
|
882
|
-
<PublicAgentEmptyState
|
|
947
|
+
<PublicAgentEmptyState
|
|
948
|
+
signupHref={signupHref}
|
|
949
|
+
onCtaClick={fireShareCtaClick}
|
|
950
|
+
/>
|
|
883
951
|
)}
|
|
884
952
|
</TabsContent>
|
|
885
953
|
<TabsContent
|
|
@@ -932,6 +1000,7 @@ export default function ShareRoute() {
|
|
|
932
1000
|
if (!open) setSignInIntent(null);
|
|
933
1001
|
}}
|
|
934
1002
|
intent={signInIntent ?? "comment"}
|
|
1003
|
+
onSignIn={() => fireShareCtaClick("signin")}
|
|
935
1004
|
/>
|
|
936
1005
|
</div>
|
|
937
1006
|
);
|
|
@@ -947,7 +1016,13 @@ function sanitizeFilename(name: string): string {
|
|
|
947
1016
|
);
|
|
948
1017
|
}
|
|
949
1018
|
|
|
950
|
-
function PublicAgentEmptyState(
|
|
1019
|
+
function PublicAgentEmptyState({
|
|
1020
|
+
signupHref,
|
|
1021
|
+
onCtaClick,
|
|
1022
|
+
}: {
|
|
1023
|
+
signupHref: string;
|
|
1024
|
+
onCtaClick: (cta: "signup" | "download" | "try_clips" | "signin") => void;
|
|
1025
|
+
}) {
|
|
951
1026
|
const [platform, setPlatform] = useState<ViewerPlatform | null>(null);
|
|
952
1027
|
|
|
953
1028
|
useEffect(() => {
|
|
@@ -1005,12 +1080,18 @@ function PublicAgentEmptyState() {
|
|
|
1005
1080
|
Loom alternative
|
|
1006
1081
|
</p>
|
|
1007
1082
|
<div className="mt-7 flex w-full max-w-[220px] flex-col gap-2">
|
|
1008
|
-
<CaptureInstallButton
|
|
1083
|
+
<CaptureInstallButton
|
|
1084
|
+
className="w-full gap-2"
|
|
1085
|
+
align="center"
|
|
1086
|
+
onClick={() => onCtaClick("download")}
|
|
1087
|
+
>
|
|
1009
1088
|
<IconDownload className="h-4 w-4" />
|
|
1010
1089
|
{downloadLabel}
|
|
1011
1090
|
</CaptureInstallButton>
|
|
1012
1091
|
<Button asChild variant="outline" className="w-full">
|
|
1013
|
-
<a href={
|
|
1092
|
+
<a href={signupHref} onClick={() => onCtaClick("signup")}>
|
|
1093
|
+
Sign up
|
|
1094
|
+
</a>
|
|
1014
1095
|
</Button>
|
|
1015
1096
|
</div>
|
|
1016
1097
|
</div>
|
|
@@ -1473,6 +1473,22 @@ async function showRegionGuidesForRecording(wantsScreen: boolean) {
|
|
|
1473
1473
|
});
|
|
1474
1474
|
}
|
|
1475
1475
|
|
|
1476
|
+
// Frame the chosen screen region with a live border so the user can see exactly
|
|
1477
|
+
// what's being captured throughout the countdown and recording. The overlay is
|
|
1478
|
+
// capture-excluded and draws its stroke outside the captured pixels, so it never
|
|
1479
|
+
// lands in the video. Torn down with the rest of the recording chrome on stop.
|
|
1480
|
+
async function showRegionRecordBorder(region: RegionCaptureRect | null) {
|
|
1481
|
+
if (!region) return;
|
|
1482
|
+
await invoke("show_region_record_border", {
|
|
1483
|
+
x: region.x,
|
|
1484
|
+
y: region.y,
|
|
1485
|
+
width: region.width,
|
|
1486
|
+
height: region.height,
|
|
1487
|
+
}).catch((err) => {
|
|
1488
|
+
console.warn("[clips-recorder] show_region_record_border failed:", err);
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1476
1492
|
async function runRecordingCountdown(wantsScreen: boolean, audioCue: AudioCue) {
|
|
1477
1493
|
const countdownEvent = waitForCountdownEvent(
|
|
1478
1494
|
COUNTDOWN_EVENT_TIMEOUT_MS,
|
|
@@ -1566,6 +1582,10 @@ async function startNativeFullscreenRecording(
|
|
|
1566
1582
|
|
|
1567
1583
|
if (params.source === "region") {
|
|
1568
1584
|
captureRegion = await selectRegionForRecording();
|
|
1585
|
+
// Frame the selected area now so it stays visible through the countdown
|
|
1586
|
+
// and the whole recording. hide_recording_chrome / hide_overlays tear it
|
|
1587
|
+
// down on stop, cancel, and the error paths below.
|
|
1588
|
+
await showRegionRecordBorder(captureRegion);
|
|
1569
1589
|
}
|
|
1570
1590
|
|
|
1571
1591
|
if (localOnly && localRecordingMode === "separate" && wantsCamera) {
|
|
@@ -12,6 +12,7 @@ import { MeetingNub } from "./overlays/meeting-nub";
|
|
|
12
12
|
import { FlowBar } from "./overlays/flow-bar";
|
|
13
13
|
import { RecordingPill } from "./overlays/recording-pill";
|
|
14
14
|
import { RegionGuideEditor, RegionGuides } from "./overlays/region-guides";
|
|
15
|
+
import { RegionRecordBorder } from "./overlays/region-record-border";
|
|
15
16
|
import "./styles.css";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -55,6 +56,8 @@ function pickRoute(route: string): React.ReactElement {
|
|
|
55
56
|
return <RegionGuideEditor />;
|
|
56
57
|
case "region-capture-selector":
|
|
57
58
|
return <RegionGuideEditor mode="capture" />;
|
|
59
|
+
case "region-record-border":
|
|
60
|
+
return <RegionRecordBorder />;
|
|
58
61
|
default:
|
|
59
62
|
return <App />;
|
|
60
63
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
3
|
+
|
|
4
|
+
interface BorderRect {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function clamp01(value: number): number {
|
|
12
|
+
if (!Number.isFinite(value)) return 0;
|
|
13
|
+
return Math.min(1, Math.max(0, value));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The live recording region is handed to this overlay window as a normalized
|
|
18
|
+
* rect in the URL query (`?region=x,y,width,height`) so it never has to wait
|
|
19
|
+
* on a Tauri event (which could race the webview load). All four values are
|
|
20
|
+
* fractions [0,1] of the recording monitor — the same space the region-capture
|
|
21
|
+
* selector emitted them in.
|
|
22
|
+
*/
|
|
23
|
+
function parseRect(search: string): BorderRect | null {
|
|
24
|
+
const raw = new URLSearchParams(search).get("region");
|
|
25
|
+
if (!raw) return null;
|
|
26
|
+
const parts = raw.split(",").map((value) => Number.parseFloat(value));
|
|
27
|
+
if (parts.length !== 4 || parts.some((value) => !Number.isFinite(value))) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const [x, y, width, height] = parts;
|
|
31
|
+
if (width <= 0 || height <= 0) return null;
|
|
32
|
+
return {
|
|
33
|
+
x: clamp01(x),
|
|
34
|
+
y: clamp01(y),
|
|
35
|
+
width: clamp01(width),
|
|
36
|
+
height: clamp01(height),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A thin frame painted around the screen region currently being recorded. The
|
|
42
|
+
* window itself is full-screen, click-through, and capture-excluded (see
|
|
43
|
+
* `show_region_record_border` on the Rust side). The frame is drawn entirely
|
|
44
|
+
* OUTWARD via an outset box-shadow with no fill, so its pixels sit just outside
|
|
45
|
+
* the captured rect and never leak into the recording even on the macOS 15.4+
|
|
46
|
+
* builds where `NSWindowSharingNone` is occasionally bypassed.
|
|
47
|
+
*/
|
|
48
|
+
export function RegionRecordBorder() {
|
|
49
|
+
const [rect] = useState<BorderRect | null>(() =>
|
|
50
|
+
typeof window === "undefined" ? null : parseRect(window.location.search),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (rect) return;
|
|
55
|
+
// No usable region — close the overlay so an empty window never lingers.
|
|
56
|
+
getCurrentWindow()
|
|
57
|
+
.close()
|
|
58
|
+
.catch(() => {});
|
|
59
|
+
}, [rect]);
|
|
60
|
+
|
|
61
|
+
if (!rect) return null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="region-record-border-layer" aria-hidden>
|
|
65
|
+
<div
|
|
66
|
+
className="region-record-border-frame"
|
|
67
|
+
style={{
|
|
68
|
+
left: `${rect.x * 100}%`,
|
|
69
|
+
top: `${rect.y * 100}%`,
|
|
70
|
+
width: `${rect.width * 100}%`,
|
|
71
|
+
height: `${rect.height * 100}%`,
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -3749,6 +3749,55 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
3749
3749
|
backdrop-filter: blur(14px);
|
|
3750
3750
|
}
|
|
3751
3751
|
|
|
3752
|
+
/* ------------------------------------------------------------------------- */
|
|
3753
|
+
/* Overlay: live recording-region border */
|
|
3754
|
+
/* ------------------------------------------------------------------------- */
|
|
3755
|
+
|
|
3756
|
+
.region-record-border-layer {
|
|
3757
|
+
position: fixed;
|
|
3758
|
+
inset: 0;
|
|
3759
|
+
width: 100vw;
|
|
3760
|
+
height: 100vh;
|
|
3761
|
+
overflow: hidden;
|
|
3762
|
+
pointer-events: none;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
.region-record-border-frame {
|
|
3766
|
+
position: absolute;
|
|
3767
|
+
border-radius: 5px;
|
|
3768
|
+
/* No fill, and the stroke is painted OUTWARD with an outset box-shadow so it
|
|
3769
|
+
lands just outside the captured rect — never inside the recorded video. */
|
|
3770
|
+
box-shadow:
|
|
3771
|
+
0 0 0 2px rgba(239, 68, 68, 0.95),
|
|
3772
|
+
0 0 0 4px rgba(239, 68, 68, 0.32),
|
|
3773
|
+
0 0 26px 6px rgba(239, 68, 68, 0.26);
|
|
3774
|
+
animation: region-record-border-pulse 1.8s ease-in-out infinite;
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
/* Gentle pulse that mirrors the live recording dot, signalling "recording
|
|
3778
|
+
now". Honour reduced-motion by holding the frame steady. */
|
|
3779
|
+
@keyframes region-record-border-pulse {
|
|
3780
|
+
0%,
|
|
3781
|
+
100% {
|
|
3782
|
+
box-shadow:
|
|
3783
|
+
0 0 0 2px rgba(239, 68, 68, 0.9),
|
|
3784
|
+
0 0 0 4px rgba(239, 68, 68, 0.28),
|
|
3785
|
+
0 0 22px 5px rgba(239, 68, 68, 0.2);
|
|
3786
|
+
}
|
|
3787
|
+
50% {
|
|
3788
|
+
box-shadow:
|
|
3789
|
+
0 0 0 2px rgba(239, 68, 68, 1),
|
|
3790
|
+
0 0 0 4px rgba(239, 68, 68, 0.44),
|
|
3791
|
+
0 0 30px 8px rgba(239, 68, 68, 0.34);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3796
|
+
.region-record-border-frame {
|
|
3797
|
+
animation: none;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3752
3801
|
@keyframes flow-bar-in {
|
|
3753
3802
|
from {
|
|
3754
3803
|
opacity: 0;
|
|
@@ -31,6 +31,7 @@ const FINALIZING_LABEL: &str = "finalizing";
|
|
|
31
31
|
const FLOW_BAR_LABEL: &str = "flow-bar";
|
|
32
32
|
const REGION_GUIDES_LABEL: &str = "region-guides";
|
|
33
33
|
const REGION_GUIDE_EDITOR_LABEL: &str = "region-guide-editor";
|
|
34
|
+
const REGION_RECORD_BORDER_LABEL: &str = "region-record-border";
|
|
34
35
|
|
|
35
36
|
/// Physical-pixel bubble sizes. Logical px on retina = physical / 2, so these
|
|
36
37
|
/// map to ~96 (small) and ~180 (medium) logical px — matching Loom's camera
|
|
@@ -448,6 +449,65 @@ pub async fn hide_region_guides(app: AppHandle) -> Result<(), String> {
|
|
|
448
449
|
Ok(())
|
|
449
450
|
}
|
|
450
451
|
|
|
452
|
+
/// Live border framing the region currently being recorded. Like the region
|
|
453
|
+
/// guides this is a full-screen, click-through, capture-excluded overlay — but
|
|
454
|
+
/// the rect is ephemeral (it belongs to one recording, not the saved preset),
|
|
455
|
+
/// so it's handed in via the URL query rather than read from config. The React
|
|
456
|
+
/// view paints only an OUTWARD frame so the stroke stays out of the captured
|
|
457
|
+
/// pixels. The recording flow owns this window; it's torn down by
|
|
458
|
+
/// `hide_recording_chrome` / `hide_overlays` when capture stops.
|
|
459
|
+
#[tauri::command]
|
|
460
|
+
pub async fn show_region_record_border(
|
|
461
|
+
app: AppHandle,
|
|
462
|
+
x: f64,
|
|
463
|
+
y: f64,
|
|
464
|
+
width: f64,
|
|
465
|
+
height: f64,
|
|
466
|
+
) -> Result<(), String> {
|
|
467
|
+
if !(width > 0.0 && height > 0.0) {
|
|
468
|
+
return Ok(());
|
|
469
|
+
}
|
|
470
|
+
if let Some(existing) = app.get_webview_window(REGION_RECORD_BORDER_LABEL) {
|
|
471
|
+
let _ = existing.close();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
|
|
475
|
+
let url = WebviewUrl::App(
|
|
476
|
+
format!("index.html?region={x:.6},{y:.6},{width:.6},{height:.6}#region-record-border")
|
|
477
|
+
.into(),
|
|
478
|
+
);
|
|
479
|
+
let win = WebviewWindowBuilder::new(&app, REGION_RECORD_BORDER_LABEL, url)
|
|
480
|
+
.title("Clips Recording Region")
|
|
481
|
+
.decorations(false)
|
|
482
|
+
.transparent(true)
|
|
483
|
+
.always_on_top(true)
|
|
484
|
+
.skip_taskbar(true)
|
|
485
|
+
.resizable(false)
|
|
486
|
+
.shadow(false)
|
|
487
|
+
.visible(false)
|
|
488
|
+
.focused(false)
|
|
489
|
+
.build()
|
|
490
|
+
.map_err(|e| {
|
|
491
|
+
eprintln!("[clips-tray] region record border build failed: {}", e);
|
|
492
|
+
e.to_string()
|
|
493
|
+
})?;
|
|
494
|
+
let _ = win.set_size(tauri::Size::Physical(PhysicalSize::new(mw, mh)));
|
|
495
|
+
let _ = win.set_position(PhysicalPosition::new(mx, my));
|
|
496
|
+
let _ = win.set_ignore_cursor_events(true);
|
|
497
|
+
set_capture_excluded_always(&win);
|
|
498
|
+
configure_overlay_behavior(&win);
|
|
499
|
+
crate::util::show_without_activation(&win);
|
|
500
|
+
Ok(())
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#[tauri::command]
|
|
504
|
+
pub async fn hide_region_record_border(app: AppHandle) -> Result<(), String> {
|
|
505
|
+
if let Some(w) = app.get_webview_window(REGION_RECORD_BORDER_LABEL) {
|
|
506
|
+
let _ = w.close();
|
|
507
|
+
}
|
|
508
|
+
Ok(())
|
|
509
|
+
}
|
|
510
|
+
|
|
451
511
|
/// Reconcile the always-on region-guides overlay with the current config.
|
|
452
512
|
/// Called from config saves and on startup so the guides window matches the
|
|
453
513
|
/// `always_visible` toggle without needing a recording-flow round trip. When a
|
|
@@ -748,6 +808,7 @@ pub async fn hide_overlays(app: AppHandle) -> Result<(), String> {
|
|
|
748
808
|
FINALIZING_LABEL,
|
|
749
809
|
FLOW_BAR_LABEL,
|
|
750
810
|
REGION_GUIDES_LABEL,
|
|
811
|
+
REGION_RECORD_BORDER_LABEL,
|
|
751
812
|
] {
|
|
752
813
|
if let Some(w) = app.get_webview_window(label) {
|
|
753
814
|
let _ = w.close();
|
|
@@ -772,7 +833,9 @@ pub async fn hide_recording_chrome(app: AppHandle) -> Result<(), String> {
|
|
|
772
833
|
// toggle — otherwise we'd flicker close→reopen right after stop.
|
|
773
834
|
let g = crate::config::feature_config(&app).region_guides;
|
|
774
835
|
let keep_region_guides = g.always_visible && g.enabled && !g.rects.is_empty();
|
|
775
|
-
|
|
836
|
+
// The recording-region border belongs to a single recording (never pinned),
|
|
837
|
+
// so it always tears down here alongside the countdown + toolbar.
|
|
838
|
+
let mut labels: Vec<&str> = vec![COUNTDOWN_LABEL, TOOLBAR_LABEL, REGION_RECORD_BORDER_LABEL];
|
|
776
839
|
if !keep_region_guides {
|
|
777
840
|
labels.push(REGION_GUIDES_LABEL);
|
|
778
841
|
}
|
|
@@ -1438,6 +1501,9 @@ pub async fn reset_state(app: AppHandle) -> Result<(), String> {
|
|
|
1438
1501
|
if let Some(w) = app.get_webview_window(REGION_GUIDES_LABEL) {
|
|
1439
1502
|
let _ = w.close();
|
|
1440
1503
|
}
|
|
1504
|
+
if let Some(w) = app.get_webview_window(REGION_RECORD_BORDER_LABEL) {
|
|
1505
|
+
let _ = w.close();
|
|
1506
|
+
}
|
|
1441
1507
|
if let Some(window) = app.get_webview_window("popover") {
|
|
1442
1508
|
// Restore normal size in case the window was shrunk to a pinhole
|
|
1443
1509
|
// during recording — otherwise it would reappear as a 2×2 dot.
|
|
@@ -68,6 +68,8 @@ pub fn run() {
|
|
|
68
68
|
clips::hide_recording_chrome,
|
|
69
69
|
clips::show_region_guides,
|
|
70
70
|
clips::hide_region_guides,
|
|
71
|
+
clips::show_region_record_border,
|
|
72
|
+
clips::hide_region_record_border,
|
|
71
73
|
clips::show_region_guide_editor,
|
|
72
74
|
clips::show_region_capture_selector,
|
|
73
75
|
clips::close_bubble,
|
|
@@ -208,7 +208,10 @@ pub fn reapply_capture_exclusion_to_overlays(app: &tauri::AppHandle) {
|
|
|
208
208
|
set_window_capture_excluded(window, false);
|
|
209
209
|
continue;
|
|
210
210
|
}
|
|
211
|
-
let private_guide = matches!(
|
|
211
|
+
let private_guide = matches!(
|
|
212
|
+
label.as_str(),
|
|
213
|
+
"region-guides" | "region-guide-editor" | "region-record-border"
|
|
214
|
+
);
|
|
212
215
|
set_window_capture_excluded(window, private_guide || !visible);
|
|
213
216
|
}
|
|
214
217
|
}
|