@agent-native/core 0.68.3 → 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.
Files changed (212) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +54 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/changelog/parse.ts +245 -0
  5. package/corpus/core/src/cli/changelog.ts +199 -0
  6. package/corpus/core/src/cli/index.ts +19 -0
  7. package/corpus/core/src/cli/recap.ts +24 -1
  8. package/corpus/core/src/cli/skills.ts +72 -45
  9. package/corpus/core/src/client/CommandMenu.tsx +183 -75
  10. package/corpus/core/src/client/agent-chat-adapter.ts +44 -7
  11. package/corpus/core/src/client/analytics.ts +181 -0
  12. package/corpus/core/src/client/changelog/Changelog.tsx +321 -0
  13. package/corpus/core/src/client/index.ts +11 -0
  14. package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +54 -0
  15. package/corpus/core/src/client/settings/useBuilderStatus.ts +82 -20
  16. package/corpus/core/src/extensions/web-search-tool.ts +45 -4
  17. package/corpus/core/src/mcp/build-server.ts +1 -1
  18. package/corpus/core/src/mcp/embed-app.ts +112 -6
  19. package/corpus/core/src/secrets/register-framework-secrets.ts +7 -0
  20. package/corpus/core/src/server/attribution.ts +211 -0
  21. package/corpus/core/src/server/auth.ts +6 -0
  22. package/corpus/core/src/server/better-auth-instance.ts +49 -5
  23. package/corpus/core/src/server/builder-browser.ts +27 -1
  24. package/corpus/core/src/server/core-routes-plugin.ts +58 -0
  25. package/corpus/core/src/server/google-oauth.ts +19 -6
  26. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  27. package/corpus/core/src/styles/agent-conversation.css +21 -0
  28. package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  29. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  30. package/corpus/templates/analytics/CHANGELOG.md +10 -0
  31. package/corpus/templates/analytics/actions/install-dashboard-template.ts +2 -2
  32. package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -1
  33. package/corpus/templates/analytics/actions/update-dashboard.ts +5 -3
  34. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +180 -144
  35. package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -2
  36. package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
  37. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +221 -213
  38. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +6 -3
  39. package/corpus/templates/analytics/app/lib/demo-dashboard-path.ts +1 -1
  40. package/corpus/templates/analytics/app/lib/last-opened.ts +2 -1
  41. package/corpus/templates/analytics/app/pages/Settings.tsx +4 -0
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -3
  43. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +2 -2
  44. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +101 -99
  45. package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +1 -1
  46. package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +18 -5
  47. package/corpus/templates/analytics/app/routes/catalog.tsx +1 -1
  48. package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +9 -0
  49. package/corpus/templates/analytics/app/routes/traffic.tsx +1 -1
  50. package/corpus/templates/analytics/changelog/2026-06-23-the-dashboards-and-analyses-sidebar-settings-buttons-now-sta.md +6 -0
  51. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +145 -0
  52. package/corpus/templates/analytics/server/db/index.ts +1 -1
  53. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -2
  54. package/corpus/templates/analytics/server/lib/demo-dashboards.ts +1 -1
  55. package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -2
  56. package/corpus/templates/analytics/server/plugins/core-routes.ts +6 -6
  57. package/corpus/templates/assets/CHANGELOG.md +10 -0
  58. package/corpus/templates/assets/app/root.tsx +7 -1
  59. package/corpus/templates/assets/app/routes/settings.tsx +6 -0
  60. package/corpus/templates/brain/CHANGELOG.md +10 -0
  61. package/corpus/templates/brain/app/root.tsx +7 -1
  62. package/corpus/templates/brain/app/routes/settings.tsx +8 -1
  63. package/corpus/templates/calendar/CHANGELOG.md +10 -0
  64. package/corpus/templates/calendar/app/pages/Settings.tsx +4 -0
  65. package/corpus/templates/calendar/app/root.tsx +7 -1
  66. package/corpus/templates/chat/CHANGELOG.md +10 -0
  67. package/corpus/templates/chat/app/root.tsx +7 -1
  68. package/corpus/templates/clips/CHANGELOG.md +10 -0
  69. package/corpus/templates/clips/app/components/player/share-dialog.tsx +33 -7
  70. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +8 -0
  71. package/corpus/templates/clips/app/components/player/video-player.tsx +150 -20
  72. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +72 -20
  73. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +69 -2
  74. package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +71 -0
  75. package/corpus/templates/clips/app/lib/recorder-preferences.ts +67 -0
  76. package/corpus/templates/clips/app/root.tsx +7 -1
  77. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +15 -1
  78. package/corpus/templates/clips/app/routes/_app.trash.tsx +21 -0
  79. package/corpus/templates/clips/app/routes/record.tsx +92 -8
  80. package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -5
  81. package/corpus/templates/clips/changelog/2026-06-23-clips-shared-in-slack-no-longer-show-a-could-not-start-playb.md +6 -0
  82. package/corpus/templates/clips/changelog/2026-06-23-made-the-play-button-on-shared-and-embedded-clips-scale-with.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-06-23-public-clip-share-links-now-show-whether-they-ll-play-inline.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-06-23-screen-camera-recordings-now-keep-the-presenter-s-face-in-th.md +6 -0
  85. package/corpus/templates/clips/desktop/src/lib/recorder.ts +20 -0
  86. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  87. package/corpus/templates/clips/desktop/src/overlays/region-record-border.tsx +76 -0
  88. package/corpus/templates/clips/desktop/src/styles.css +49 -0
  89. package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
  90. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +67 -1
  91. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
  92. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +4 -1
  93. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +51 -1
  94. package/corpus/templates/clips/shared/share-attribution.ts +79 -0
  95. package/corpus/templates/content/CHANGELOG.md +10 -0
  96. package/corpus/templates/content/app/root.tsx +7 -1
  97. package/corpus/templates/content/vitest.config.ts +3 -0
  98. package/corpus/templates/design/CHANGELOG.md +10 -0
  99. package/corpus/templates/design/actions/generate-design.ts +4 -0
  100. package/corpus/templates/design/app/root.tsx +7 -1
  101. package/corpus/templates/design/app/routes/settings.tsx +10 -2
  102. package/corpus/templates/dispatch/CHANGELOG.md +10 -0
  103. package/corpus/templates/dispatch/app/root.tsx +7 -1
  104. package/corpus/templates/forms/CHANGELOG.md +10 -0
  105. package/corpus/templates/forms/app/root.tsx +7 -1
  106. package/corpus/templates/macros/CHANGELOG.md +10 -0
  107. package/corpus/templates/macros/app/root.tsx +7 -1
  108. package/corpus/templates/mail/CHANGELOG.md +10 -0
  109. package/corpus/templates/mail/app/components/layout/CommandPalette.tsx +3 -0
  110. package/corpus/templates/mail/app/pages/SettingsPage.tsx +27 -0
  111. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +31 -17
  112. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -4
  113. package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +34 -24
  114. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -4
  115. package/corpus/templates/plan/AGENTS.md +4 -0
  116. package/corpus/templates/plan/CHANGELOG.md +10 -0
  117. package/corpus/templates/plan/actions/view-screen.ts +5 -0
  118. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +98 -1
  119. package/corpus/templates/plan/app/pages/PlansPage.tsx +107 -7
  120. package/corpus/templates/plan/app/root.tsx +7 -1
  121. package/corpus/templates/plan/shared/share-attribution.ts +72 -0
  122. package/corpus/templates/slides/CHANGELOG.md +10 -0
  123. package/corpus/templates/slides/app/root.tsx +7 -1
  124. package/corpus/templates/videos/CHANGELOG.md +4 -151
  125. package/corpus/templates/videos/app/root.tsx +7 -1
  126. package/dist/changelog/parse.d.ts +70 -0
  127. package/dist/changelog/parse.d.ts.map +1 -0
  128. package/dist/changelog/parse.js +200 -0
  129. package/dist/changelog/parse.js.map +1 -0
  130. package/dist/cli/changelog.d.ts +4 -0
  131. package/dist/cli/changelog.d.ts.map +1 -0
  132. package/dist/cli/changelog.js +166 -0
  133. package/dist/cli/changelog.js.map +1 -0
  134. package/dist/cli/index.js +18 -0
  135. package/dist/cli/index.js.map +1 -1
  136. package/dist/cli/recap.d.ts.map +1 -1
  137. package/dist/cli/recap.js +22 -1
  138. package/dist/cli/recap.js.map +1 -1
  139. package/dist/cli/skills.d.ts +3 -3
  140. package/dist/cli/skills.d.ts.map +1 -1
  141. package/dist/cli/skills.js +72 -45
  142. package/dist/cli/skills.js.map +1 -1
  143. package/dist/client/CommandMenu.d.ts +17 -1
  144. package/dist/client/CommandMenu.d.ts.map +1 -1
  145. package/dist/client/CommandMenu.js +49 -13
  146. package/dist/client/CommandMenu.js.map +1 -1
  147. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  148. package/dist/client/agent-chat-adapter.js +43 -6
  149. package/dist/client/agent-chat-adapter.js.map +1 -1
  150. package/dist/client/analytics.d.ts +18 -0
  151. package/dist/client/analytics.d.ts.map +1 -1
  152. package/dist/client/analytics.js +177 -0
  153. package/dist/client/analytics.js.map +1 -1
  154. package/dist/client/changelog/Changelog.d.ts +47 -0
  155. package/dist/client/changelog/Changelog.d.ts.map +1 -0
  156. package/dist/client/changelog/Changelog.js +138 -0
  157. package/dist/client/changelog/Changelog.js.map +1 -0
  158. package/dist/client/index.d.ts +2 -1
  159. package/dist/client/index.d.ts.map +1 -1
  160. package/dist/client/index.js +2 -1
  161. package/dist/client/index.js.map +1 -1
  162. package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
  163. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  164. package/dist/client/mcp-apps/McpAppRenderer.js +42 -1
  165. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
  166. package/dist/client/settings/useBuilderStatus.d.ts +2 -0
  167. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  168. package/dist/client/settings/useBuilderStatus.js +58 -10
  169. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  170. package/dist/extensions/web-search-tool.d.ts +5 -3
  171. package/dist/extensions/web-search-tool.d.ts.map +1 -1
  172. package/dist/extensions/web-search-tool.js +36 -4
  173. package/dist/extensions/web-search-tool.js.map +1 -1
  174. package/dist/mcp/build-server.js +1 -1
  175. package/dist/mcp/build-server.js.map +1 -1
  176. package/dist/mcp/embed-app.d.ts.map +1 -1
  177. package/dist/mcp/embed-app.js +112 -6
  178. package/dist/mcp/embed-app.js.map +1 -1
  179. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  180. package/dist/secrets/register-framework-secrets.js +6 -0
  181. package/dist/secrets/register-framework-secrets.js.map +1 -1
  182. package/dist/server/attribution.d.ts +83 -0
  183. package/dist/server/attribution.d.ts.map +1 -0
  184. package/dist/server/attribution.js +178 -0
  185. package/dist/server/attribution.js.map +1 -0
  186. package/dist/server/auth.d.ts.map +1 -1
  187. package/dist/server/auth.js +7 -0
  188. package/dist/server/auth.js.map +1 -1
  189. package/dist/server/better-auth-instance.d.ts +9 -1
  190. package/dist/server/better-auth-instance.d.ts.map +1 -1
  191. package/dist/server/better-auth-instance.js +32 -2
  192. package/dist/server/better-auth-instance.js.map +1 -1
  193. package/dist/server/builder-browser.d.ts +4 -0
  194. package/dist/server/builder-browser.d.ts.map +1 -1
  195. package/dist/server/builder-browser.js +20 -1
  196. package/dist/server/builder-browser.js.map +1 -1
  197. package/dist/server/core-routes-plugin.d.ts +25 -0
  198. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  199. package/dist/server/core-routes-plugin.js +38 -0
  200. package/dist/server/core-routes-plugin.js.map +1 -1
  201. package/dist/server/google-oauth.d.ts.map +1 -1
  202. package/dist/server/google-oauth.js +16 -2
  203. package/dist/server/google-oauth.js.map +1 -1
  204. package/dist/shared/mcp-embed-headers.js +1 -1
  205. package/dist/shared/mcp-embed-headers.js.map +1 -1
  206. package/dist/styles/agent-conversation.css +21 -0
  207. package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  208. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  209. package/package.json +1 -1
  210. package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  211. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  212. /package/corpus/templates/analytics/app/routes/{dashboards.tsx → dashboards._index.tsx} +0 -0
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable user-facing changes to Agent-Native Calendar are documented here. Open it any
4
+ time from the command menu (Cmd+K → "What's new") or from Settings.
5
+
6
+ ## 2026-06-23
7
+
8
+ ### Added
9
+
10
+ - See what's new right inside Agent-Native Calendar — a changelog now lives in the command menu (Cmd+K) and in Settings.
@@ -26,8 +26,10 @@ import { useSettings, useUpdateSettings } from "@/hooks/use-settings";
26
26
  import {
27
27
  AppearancePicker,
28
28
  callAction,
29
+ ChangelogSettingsCard,
29
30
  type AppearancePresetId,
30
31
  } from "@agent-native/core/client";
32
+ import changelog from "../../CHANGELOG.md?raw";
31
33
  import {
32
34
  useGoogleAuthStatus,
33
35
  useGoogleAuthUrl,
@@ -151,6 +153,8 @@ export default function Settings() {
151
153
  </p>
152
154
  </div>
153
155
 
156
+ <ChangelogSettingsCard markdown={changelog} />
157
+
154
158
  {/* Google Calendar Connection */}
155
159
  <Card>
156
160
  <CardHeader>
@@ -22,6 +22,7 @@ import {
22
22
  useDbSync,
23
23
  } from "@agent-native/core/client";
24
24
  import { IconSun, IconMoon } from "@tabler/icons-react";
25
+ import changelog from "../CHANGELOG.md?raw";
25
26
  import type { LinksFunction } from "react-router";
26
27
  import stylesheet from "./global.css?url";
27
28
  configureTracking({
@@ -137,7 +138,12 @@ function AppContent() {
137
138
  return (
138
139
  <>
139
140
  <DbSyncSetup />
140
- <CommandMenu open={cmdkOpen} onOpenChange={setCmdkOpen}>
141
+ <CommandMenu
142
+ open={cmdkOpen}
143
+ onOpenChange={setCmdkOpen}
144
+ changelog={changelog}
145
+ changelogKey="calendar"
146
+ >
141
147
  <CommandMenu.Group heading="Actions">
142
148
  <CommandMenu.Item onSelect={() => {}}>Search</CommandMenu.Item>
143
149
  </CommandMenu.Group>
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable user-facing changes to Chat are documented here. Open it any
4
+ time from the command menu (Cmd+K → "What's new").
5
+
6
+ ## 2026-06-23
7
+
8
+ ### Added
9
+
10
+ - See what's new right inside Chat — a changelog now lives in the command menu (Cmd+K).
@@ -14,6 +14,7 @@ import {
14
14
  useCommandMenuShortcut,
15
15
  } from "@agent-native/core/client";
16
16
  import { IconSun, IconMoon } from "@tabler/icons-react";
17
+ import changelog from "../CHANGELOG.md?raw";
17
18
  import { Layout as AppLayout } from "@/components/layout/Layout";
18
19
  import { TAB_ID } from "@/lib/tab-id";
19
20
  import { APP_TITLE } from "@/lib/app-config";
@@ -97,7 +98,12 @@ function AppContent() {
97
98
  useCommandMenuShortcut(useCallback(() => setCmdkOpen(true), []));
98
99
  return (
99
100
  <>
100
- <CommandMenu open={cmdkOpen} onOpenChange={setCmdkOpen}>
101
+ <CommandMenu
102
+ open={cmdkOpen}
103
+ onOpenChange={setCmdkOpen}
104
+ changelog={changelog}
105
+ changelogKey="chat"
106
+ >
101
107
  <CommandMenu.Group heading="Actions">
102
108
  <CommandMenu.Item onSelect={() => {}}>Search</CommandMenu.Item>
103
109
  </CommandMenu.Group>
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable user-facing changes to Clips are documented here. Open it any time
4
+ from the command menu (Cmd+K → "What's new") or from Settings.
5
+
6
+ ## 2026-06-23
7
+
8
+ ### Added
9
+
10
+ - See what's new right inside Clips — a changelog now lives in the command menu (Cmd+K) and in Settings.
@@ -9,6 +9,7 @@ import {
9
9
  appBasePath,
10
10
  appPath,
11
11
  useActionQuery,
12
+ useSession,
12
13
  } from "@agent-native/core/client";
13
14
  import {
14
15
  Popover,
@@ -34,8 +35,10 @@ import {
34
35
  type SharesResponse,
35
36
  type Visibility,
36
37
  } from "@/components/sharing/share-ui";
38
+ import { SlackShareHint } from "@/components/sharing/slack-share-hint";
37
39
  import { buildAgentApiUrls } from "../../../shared/agent-context";
38
40
  import { isLoomEmbedUrl } from "../../../shared/loom";
41
+ import { withShareAttribution } from "../../../shared/share-attribution";
39
42
 
40
43
  const PUBLIC_DESCRIPTION =
41
44
  "Anyone with the link can view — sign in to comment or react";
@@ -156,11 +159,6 @@ function ShareRecordingContent({
156
159
  hasPassword?: boolean;
157
160
  reserveCloseButton?: boolean;
158
161
  }) {
159
- const shareUrl =
160
- typeof window === "undefined"
161
- ? ""
162
- : absoluteAppUrl(`/share/${recordingId}`);
163
-
164
162
  const sharesQuery = useActionQuery<SharesResponse>("list-resource-shares", {
165
163
  resourceType: "recording",
166
164
  resourceId: recordingId,
@@ -168,6 +166,23 @@ function ShareRecordingContent({
168
166
 
169
167
  const data = sharesQuery.data;
170
168
  const canManage = data?.role === "owner" || data?.role === "admin";
169
+
170
+ // Attribution `via` must be a stable non-PII id, never an email. The only
171
+ // owner id available client-side is the *current* session's userId, which is
172
+ // the clip owner only when the viewer is the owner. Anyone else (e.g. a
173
+ // share-admin) gets an untagged `via` so we never attribute the link to the
174
+ // wrong person or leak the owner's email.
175
+ const { session } = useSession();
176
+ const ownerViaId =
177
+ data?.role === "owner" ? (session?.userId ?? undefined) : undefined;
178
+
179
+ const shareUrl =
180
+ typeof window === "undefined"
181
+ ? ""
182
+ : withShareAttribution(
183
+ absoluteAppUrl(`/share/${recordingId}`),
184
+ ownerViaId,
185
+ );
171
186
  const titleText = recordingTitle
172
187
  ? `Share "${recordingTitle}"`
173
188
  : "Share recording";
@@ -224,6 +239,7 @@ function ShareRecordingContent({
224
239
  recordingId={recordingId}
225
240
  sharesQuery={sharesQuery}
226
241
  canManage={canManage}
242
+ ownerViaId={ownerViaId}
227
243
  />
228
244
  </TabsContent>
229
245
  </Tabs>
@@ -321,6 +337,10 @@ function LinkTab({
321
337
  disabled={isPending || (!isPublic && canManage)}
322
338
  />
323
339
 
340
+ {/* Public links unfurl into a playable video in Slack; surface that here
341
+ (and a connect link) instead of leaving it buried in Settings. */}
342
+ {isPublic ? <SlackShareHint canManage={canManage} /> : null}
343
+
324
344
  <CopyField
325
345
  label="Share with agents"
326
346
  value={agentContextUrl}
@@ -389,10 +409,12 @@ function ClipsEmbedConfigurator({
389
409
  recordingId,
390
410
  sharesQuery,
391
411
  canManage,
412
+ ownerViaId,
392
413
  }: {
393
414
  recordingId: string;
394
415
  sharesQuery: SharesQuery;
395
416
  canManage: boolean;
417
+ ownerViaId?: string;
396
418
  }) {
397
419
  const [autoplay, setAutoplay] = useState(false);
398
420
  const [startMs, setStartMs] = useState(0);
@@ -416,8 +438,12 @@ function ClipsEmbedConfigurator({
416
438
  if (autoplay) params.push("autoplay=1");
417
439
  if (startMs > 0) params.push(`t=${Math.round(startMs / 1000)}`);
418
440
  const qs = params.length ? `?${params.join("&")}` : "";
419
- return absoluteAppUrl(`/embed/${recordingId}${qs}`);
420
- }, [recordingId, autoplay, startMs]);
441
+ // Keep autoplay/t intact and also self-attribute the embed.
442
+ return withShareAttribution(
443
+ absoluteAppUrl(`/embed/${recordingId}${qs}`),
444
+ ownerViaId,
445
+ );
446
+ }, [recordingId, autoplay, startMs, ownerViaId]);
421
447
 
422
448
  const code =
423
449
  mode === "responsive"
@@ -21,6 +21,12 @@ export interface SignInPromptDialogProps {
21
21
  * fires there and forwards to `returnTo` once the viewer is signed in.
22
22
  */
23
23
  returnTo?: string;
24
+ /**
25
+ * Fired when the viewer activates the "Sign in" button, before navigation.
26
+ * Used by the public share page to emit the signin funnel event. Must not
27
+ * change navigation behavior.
28
+ */
29
+ onSignIn?: () => void;
24
30
  }
25
31
 
26
32
  function buildSignInHref(returnTo: string | undefined): string {
@@ -34,6 +40,7 @@ export function SignInPromptDialog({
34
40
  onOpenChange,
35
41
  intent,
36
42
  returnTo,
43
+ onSignIn,
37
44
  }: SignInPromptDialogProps) {
38
45
  return (
39
46
  <Dialog open={open} onOpenChange={onOpenChange}>
@@ -51,6 +58,7 @@ export function SignInPromptDialog({
51
58
  </Button>
52
59
  <Button
53
60
  onClick={() => {
61
+ onSignIn?.();
54
62
  window.location.href = buildSignInHref(returnTo);
55
63
  }}
56
64
  >
@@ -232,7 +232,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
232
232
  const [currentMs, setCurrentMs] = useState(startMs ?? 0);
233
233
  const [loomStartMs, setLoomStartMs] = useState<number | null>(null);
234
234
  const [volume, setVolume] = useState(1);
235
- const [muted, setMuted] = useState(false);
235
+ // Autoplaying players (e.g. the Slack unfurl embed, `?autoplay=1`) must
236
+ // start muted or the browser blocks autoplay with a NotAllowedError. The
237
+ // share page (no autoplay) keeps full sound.
238
+ const [muted, setMuted] = useState(() => !!autoPlay);
236
239
  const [speed, setSpeed] = useState(() =>
237
240
  readPlaybackSpeedPreference(defaultSpeed),
238
241
  );
@@ -354,18 +357,30 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
354
357
  setIsPreparing(false);
355
358
  }, []);
356
359
 
357
- const rejectPlayAttempt = useCallback((attemptId: number, err: unknown) => {
358
- if (attemptId !== playAttemptIdRef.current) return;
359
- playAttemptPendingRef.current = false;
360
- setIsPlayPending(false);
361
- setIsBuffering(false);
362
-
363
- const name = err instanceof DOMException ? err.name : "";
364
- if (name === "AbortError") return;
365
-
366
- console.warn("[clips] playback start failed", err);
367
- setPlayError("Could not start playback. Try again.");
368
- }, []);
360
+ const rejectPlayAttempt = useCallback(
361
+ (attemptId: number, err: unknown) => {
362
+ if (attemptId !== playAttemptIdRef.current) return;
363
+ playAttemptPendingRef.current = false;
364
+ setIsPlayPending(false);
365
+ setIsBuffering(false);
366
+
367
+ const name = err instanceof DOMException ? err.name : "";
368
+ // AbortError: a newer load/seek superseded this play() — not a failure.
369
+ // NotAllowedError: the browser blocked autoplay because there was no
370
+ // user gesture (this is what happens inside Slack's cross-origin unfurl
371
+ // iframe). Both are expected — fall back to the click-to-play overlay
372
+ // instead of showing a scary "Could not start playback" message.
373
+ if (name === "AbortError" || name === "NotAllowedError") return;
374
+
375
+ console.warn("[clips] playback start failed", err);
376
+ reportPlaybackIssue("play-start-failed", err, videoRef.current, {
377
+ recordingId,
378
+ autoPlay: !!autoPlay,
379
+ });
380
+ setPlayError("Could not start playback. Try again.");
381
+ },
382
+ [autoPlay, recordingId],
383
+ );
369
384
 
370
385
  const attachPlayPromise = useCallback(
371
386
  (playPromise: Promise<void> | undefined, attemptId: number) => {
@@ -882,7 +897,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
882
897
  <div
883
898
  ref={containerRef}
884
899
  className={cn(
885
- "relative bg-black overflow-hidden select-none group",
900
+ // `@container` lets the center play button scale with the player
901
+ // width (see CenterPlaybackOverlay) so it isn't oversized inside
902
+ // small embeds like the Slack unfurl iframe.
903
+ "relative @container bg-black overflow-hidden select-none group",
886
904
  theaterMode ? "fixed inset-0 z-40" : "rounded-xl",
887
905
  className,
888
906
  )}
@@ -910,12 +928,19 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
910
928
  ref={videoRef}
911
929
  src={activeVideoSrc}
912
930
  poster={resolveLocalUrl(thumbnailUrl)}
913
- crossOrigin="anonymous"
931
+ // `crossOrigin` is only needed so the owner's canvas thumbnail
932
+ // capture isn't tainted. For everyone else (viewers, and the Slack
933
+ // unfurl embed) it adds nothing — but if the player is ever framed
934
+ // into a sandboxed/opaque-origin context (Slack double-iframes the
935
+ // embed), the resulting CORS check on the media bytes fails and the
936
+ // video won't load. Scope it to owners so embeds load cleanly.
937
+ crossOrigin={role === "owner" ? "anonymous" : undefined}
914
938
  className={cn(
915
939
  "w-full h-full",
916
940
  cover ? "object-cover" : "object-contain",
917
941
  )}
918
942
  autoPlay={autoPlay}
943
+ muted={muted}
919
944
  playsInline
920
945
  onLoadStart={() => {
921
946
  setCanPlay(false);
@@ -1014,12 +1039,26 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1014
1039
  setIsBuffering(false);
1015
1040
  onEnded?.();
1016
1041
  }}
1017
- onError={() => {
1042
+ onError={(e) => {
1018
1043
  playAttemptPendingRef.current = false;
1019
1044
  setIsPlayPending(false);
1020
1045
  setIsBuffering(false);
1021
1046
  setIsPreparing(false);
1022
- setPlayError("Video could not be loaded.");
1047
+ const desc = describeMediaError(e.currentTarget.error);
1048
+ reportPlaybackIssue(
1049
+ "media-load-failed",
1050
+ e.currentTarget.error,
1051
+ e.currentTarget,
1052
+ {
1053
+ recordingId,
1054
+ videoSrc: activeVideoSrc,
1055
+ },
1056
+ );
1057
+ setPlayError(
1058
+ desc
1059
+ ? `Video could not be loaded (${desc.label}).`
1060
+ : "Video could not be loaded.",
1061
+ );
1023
1062
  }}
1024
1063
  onVolumeChange={(e) => {
1025
1064
  setVolume(e.currentTarget.volume);
@@ -1039,7 +1078,17 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1039
1078
  durationMs={resolvedDurationMs}
1040
1079
  speed={speed}
1041
1080
  playError={playError}
1042
- onPlay={requestPlay}
1081
+ onPlay={() => {
1082
+ // An explicit click means the user wants to watch with sound, so
1083
+ // undo the muted-autoplay default (see `muted` state) before we
1084
+ // start playback.
1085
+ const v = videoRef.current;
1086
+ if (v && v.muted) {
1087
+ v.muted = false;
1088
+ setMuted(false);
1089
+ }
1090
+ requestPlay();
1091
+ }}
1043
1092
  onSpeedChange={applySpeed}
1044
1093
  menuPortalContainer={fullscreenMenuContainer}
1045
1094
  />
@@ -1193,9 +1242,9 @@ function CenterPlaybackOverlay({
1193
1242
  e.stopPropagation();
1194
1243
  onPlay();
1195
1244
  }}
1196
- className="pointer-events-auto flex h-24 w-24 items-center justify-center rounded-full bg-white text-black shadow-2xl ring-1 ring-white/35 transition-transform duration-150 hover:scale-105 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-black"
1245
+ className="pointer-events-auto flex h-[clamp(3rem,13cqw,6rem)] w-[clamp(3rem,13cqw,6rem)] items-center justify-center rounded-full bg-white text-black shadow-2xl ring-1 ring-white/35 transition-transform duration-150 hover:scale-105 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-black"
1197
1246
  >
1198
- <IconPlayerPlay className="ml-1 h-12 w-12 fill-current" />
1247
+ <IconPlayerPlay className="ml-[6%] h-[clamp(1.5rem,6.5cqw,3rem)] w-[clamp(1.5rem,6.5cqw,3rem)] fill-current" />
1199
1248
  </button>
1200
1249
 
1201
1250
  <div
@@ -1302,6 +1351,87 @@ function formatSpeedLabel(rate: number): string {
1302
1351
  return `${Number.isInteger(rate) ? rate : rate.toFixed(1)}x`;
1303
1352
  }
1304
1353
 
1354
+ /** Human-readable label for an HTMLMediaElement `error` (MediaError). */
1355
+ function describeMediaError(
1356
+ err: MediaError | null,
1357
+ ): { code: number; label: string } | null {
1358
+ if (!err) return null;
1359
+ const labels: Record<number, string> = {
1360
+ 1: "load aborted",
1361
+ 2: "network error",
1362
+ 3: "decode error",
1363
+ 4: "format not supported",
1364
+ };
1365
+ return { code: err.code, label: labels[err.code] ?? "unknown error" };
1366
+ }
1367
+
1368
+ /**
1369
+ * Surface a playback failure to the console and Sentry with enough context to
1370
+ * debug it remotely — e.g. when a clip "could not be loaded" inside a Slack
1371
+ * unfurl where there's no visible console. Best-effort; never throws. Expected,
1372
+ * benign cases (AbortError / autoplay-blocked NotAllowedError) are filtered out
1373
+ * by the callers and never reach here.
1374
+ */
1375
+ function reportPlaybackIssue(
1376
+ reason: string,
1377
+ err: unknown,
1378
+ video: HTMLVideoElement | null,
1379
+ extra: Record<string, unknown>,
1380
+ ) {
1381
+ const mediaError =
1382
+ err && typeof err === "object" && "code" in err
1383
+ ? (err as MediaError)
1384
+ : (video?.error ?? null);
1385
+ const name =
1386
+ err instanceof DOMException || err instanceof Error ? err.name : undefined;
1387
+ const message = err instanceof Error ? err.message : undefined;
1388
+
1389
+ let videoHost: string | undefined;
1390
+ try {
1391
+ if (video?.currentSrc) videoHost = new URL(video.currentSrc).host;
1392
+ } catch {
1393
+ // ignore unparseable src
1394
+ }
1395
+ let inIframe = false;
1396
+ try {
1397
+ inIframe = typeof window !== "undefined" && window.self !== window.top;
1398
+ } catch {
1399
+ inIframe = true;
1400
+ }
1401
+
1402
+ const detail = {
1403
+ ...extra,
1404
+ errorName: name,
1405
+ errorMessage: message,
1406
+ mediaErrorCode: mediaError?.code,
1407
+ mediaErrorLabel: describeMediaError(mediaError)?.label,
1408
+ videoHost,
1409
+ inIframe,
1410
+ readyState: video?.readyState,
1411
+ networkState: video?.networkState,
1412
+ };
1413
+ console.warn(`[clips] playback issue: ${reason}`, detail);
1414
+
1415
+ try {
1416
+ const reportable =
1417
+ err instanceof Error
1418
+ ? err
1419
+ : new Error(
1420
+ `clips playback ${reason}: ${message ?? name ?? describeMediaError(mediaError)?.label ?? "unknown"}`,
1421
+ );
1422
+ captureClientException(reportable, {
1423
+ tags: {
1424
+ area: "clips-player",
1425
+ playbackIssue: reason,
1426
+ inIframe: String(inIframe),
1427
+ },
1428
+ extra: detail,
1429
+ });
1430
+ } catch {
1431
+ // Diagnostics must never break playback UI.
1432
+ }
1433
+ }
1434
+
1305
1435
  function formatWatchDuration(ms: number): string {
1306
1436
  if (!Number.isFinite(ms) || ms <= 0) return "0 sec";
1307
1437
  const totalSeconds = Math.max(1, Math.round(ms / 1000));
@@ -48,6 +48,10 @@ import {
48
48
  type RecordingMode,
49
49
  } from "./recorder-engine";
50
50
  import type { CameraBubbleSize } from "./camera-bubble";
51
+ import {
52
+ loadRecorderPreferences,
53
+ saveRecorderPreferences,
54
+ } from "@/lib/recorder-preferences";
51
55
  import { CameraVisualizer, type CameraTestStatus } from "./camera-visualizer";
52
56
  import {
53
57
  MicrophoneVisualizer,
@@ -184,11 +188,14 @@ export function PreRecordPanel({
184
188
  }: PreRecordPanelProps) {
185
189
  const fileInputRef = useRef<HTMLInputElement>(null);
186
190
  const loomInputRef = useRef<HTMLInputElement>(null);
191
+ // Saved selections from the last visit. A `?mode=`/`?surface=` deep link
192
+ // (initialMode/initialDisplaySurface) still takes precedence over them.
193
+ const savedPrefs = useMemo(() => loadRecorderPreferences(), []);
187
194
  const [mode, setMode] = useState<RecordingMode>(
188
- () => initialMode ?? "screen+camera",
195
+ () => initialMode ?? savedPrefs.mode ?? "screen+camera",
189
196
  );
190
197
  const [displaySurface, setDisplaySurface] = useState<DisplaySurface>(
191
- () => initialDisplaySurface ?? "window",
198
+ () => initialDisplaySurface ?? savedPrefs.displaySurface ?? "window",
192
199
  );
193
200
  const [sourceOpen, setSourceOpen] = useState(false);
194
201
  const [deviceSettingsOpen, setDeviceSettingsOpen] = useState(false);
@@ -197,8 +204,12 @@ export function PreRecordPanel({
197
204
  const [loomError, setLoomError] = useState<string | null>(null);
198
205
  const [mics, setMics] = useState<MediaDeviceInfo[]>([]);
199
206
  const [cameras, setCameras] = useState<MediaDeviceInfo[]>([]);
200
- const [micId, setMicId] = useState<string>("default");
201
- const [cameraId, setCameraId] = useState<string>("default");
207
+ const [micId, setMicId] = useState<string>(
208
+ () => savedPrefs.micId ?? "default",
209
+ );
210
+ const [cameraId, setCameraId] = useState<string>(
211
+ () => savedPrefs.cameraId ?? "default",
212
+ );
202
213
  const [enumError, setEnumError] = useState<string | null>(null);
203
214
  const [micAccessStatus, setMicAccessStatus] =
204
215
  useState<DeviceAccessStatus>("idle");
@@ -274,13 +285,57 @@ export function PreRecordPanel({
274
285
  [mics],
275
286
  );
276
287
 
288
+ // Reset to "default" only once a populated list genuinely excludes the saved
289
+ // device — an empty list means "not enumerated yet", and resetting then would
290
+ // wipe a restored preference before devices load.
277
291
  useEffect(() => {
278
292
  if (micId === "default" || micId === NO_MIC_DEVICE_ID) return;
279
- if (!mics.some((mic) => mic.deviceId === micId)) {
293
+ if (mics.length > 0 && !mics.some((mic) => mic.deviceId === micId)) {
280
294
  setMicId("default");
281
295
  }
282
296
  }, [micId, mics]);
283
297
 
298
+ // Same guard for cameras. Not persisted, so a temporarily missing device
299
+ // doesn't erase the saved choice.
300
+ useEffect(() => {
301
+ if (cameraId === "default" || cameraId === NO_CAMERA_DEVICE_ID) return;
302
+ if (
303
+ cameras.length > 0 &&
304
+ !cameras.some((camera) => camera.deviceId === cameraId)
305
+ ) {
306
+ setCameraId("default");
307
+ }
308
+ }, [cameraId, cameras]);
309
+
310
+ // Camera-only mode needs a camera — coerce a restored "off" sentinel to
311
+ // "default" so Start doesn't forward it as an exact deviceId. This is the
312
+ // single owner of that coercion: it covers both the mode-button click and the
313
+ // ?mode=camera deep-link/restore path.
314
+ useEffect(() => {
315
+ if (mode === "camera" && cameraId === NO_CAMERA_DEVICE_ID) {
316
+ setCameraId("default");
317
+ }
318
+ }, [mode, cameraId]);
319
+
320
+ // Persist deliberate picks only (not the resets above), so an unavailable
321
+ // device on load can't clobber the stored preference.
322
+ const chooseMode = useCallback((value: RecordingMode) => {
323
+ setMode(value);
324
+ saveRecorderPreferences({ mode: value });
325
+ }, []);
326
+ const chooseDisplaySurface = useCallback((value: DisplaySurface) => {
327
+ setDisplaySurface(value);
328
+ saveRecorderPreferences({ displaySurface: value });
329
+ }, []);
330
+ const chooseMic = useCallback((value: string) => {
331
+ setMicId(value);
332
+ saveRecorderPreferences({ micId: value });
333
+ }, []);
334
+ const chooseCamera = useCallback((value: string) => {
335
+ setCameraId(value);
336
+ saveRecorderPreferences({ cameraId: value });
337
+ }, []);
338
+
284
339
  const requestMicrophoneChoices = useCallback(async () => {
285
340
  if (!navigator.mediaDevices?.getUserMedia) {
286
341
  setMicAccessStatus("error");
@@ -376,9 +431,9 @@ export function PreRecordPanel({
376
431
  void requestMicrophoneChoices();
377
432
  return;
378
433
  }
379
- setMicId(value);
434
+ chooseMic(value);
380
435
  },
381
- [requestMicrophoneChoices],
436
+ [requestMicrophoneChoices, chooseMic],
382
437
  );
383
438
 
384
439
  const handleMicSignalChange = useCallback((hasSignal: boolean) => {
@@ -529,15 +584,10 @@ export function PreRecordPanel({
529
584
  <button
530
585
  key={opt.value}
531
586
  type="button"
532
- onClick={() => {
533
- setMode(opt.value);
534
- if (
535
- opt.value === "camera" &&
536
- cameraId === NO_CAMERA_DEVICE_ID
537
- ) {
538
- setCameraId("default");
539
- }
540
- }}
587
+ // Camera-only mode needs a camera; the [mode, cameraId] effect
588
+ // below coerces a restored "off" sentinel to "default" for both
589
+ // this click and the ?mode=camera deep-link path.
590
+ onClick={() => chooseMode(opt.value)}
541
591
  className={cn(
542
592
  "flex min-h-20 min-w-0 flex-col justify-between rounded-xl border p-3 text-left transition-colors",
543
593
  active
@@ -605,7 +655,7 @@ export function PreRecordPanel({
605
655
  <button
606
656
  key={opt.value}
607
657
  type="button"
608
- onClick={() => setDisplaySurface(opt.value)}
658
+ onClick={() => chooseDisplaySurface(opt.value)}
609
659
  className={cn(
610
660
  "flex min-h-[76px] flex-col rounded-lg border p-2 text-left transition-colors",
611
661
  active
@@ -703,7 +753,7 @@ export function PreRecordPanel({
703
753
  <Switch
704
754
  checked={audioEnabled}
705
755
  onCheckedChange={(checked) =>
706
- setMicId(checked ? "default" : NO_MIC_DEVICE_ID)
756
+ chooseMic(checked ? "default" : NO_MIC_DEVICE_ID)
707
757
  }
708
758
  disabled={busy}
709
759
  aria-label="Include audio in this recording"
@@ -740,7 +790,7 @@ export function PreRecordPanel({
740
790
  </div>
741
791
  <Select
742
792
  value={cameraId}
743
- onValueChange={setCameraId}
793
+ onValueChange={chooseCamera}
744
794
  disabled={!needsCamera}
745
795
  >
746
796
  <SelectTrigger className="h-9 min-w-0 flex-1 border-0 bg-transparent px-2 shadow-none hover:bg-muted/45 focus:ring-0 focus:ring-offset-0 disabled:opacity-60">
@@ -762,7 +812,9 @@ export function PreRecordPanel({
762
812
  <Switch
763
813
  checked={needsCamera}
764
814
  onCheckedChange={(checked) =>
765
- setCameraId(checked ? "default" : NO_CAMERA_DEVICE_ID)
815
+ chooseCamera(
816
+ checked ? "default" : NO_CAMERA_DEVICE_ID,
817
+ )
766
818
  }
767
819
  disabled={busy}
768
820
  aria-label="Include camera in this recording"