@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.
Files changed (200) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +48 -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/mcp/build-server.ts +1 -1
  17. package/corpus/core/src/mcp/embed-app.ts +112 -6
  18. package/corpus/core/src/server/attribution.ts +211 -0
  19. package/corpus/core/src/server/auth.ts +6 -0
  20. package/corpus/core/src/server/better-auth-instance.ts +49 -5
  21. package/corpus/core/src/server/builder-browser.ts +27 -1
  22. package/corpus/core/src/server/core-routes-plugin.ts +58 -0
  23. package/corpus/core/src/server/google-oauth.ts +19 -6
  24. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  25. package/corpus/core/src/styles/agent-conversation.css +21 -0
  26. package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  27. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  28. package/corpus/templates/analytics/CHANGELOG.md +10 -0
  29. package/corpus/templates/analytics/actions/install-dashboard-template.ts +2 -2
  30. package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -1
  31. package/corpus/templates/analytics/actions/update-dashboard.ts +5 -3
  32. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +180 -144
  33. package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -2
  34. package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
  35. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +221 -213
  36. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +6 -3
  37. package/corpus/templates/analytics/app/lib/demo-dashboard-path.ts +1 -1
  38. package/corpus/templates/analytics/app/lib/last-opened.ts +2 -1
  39. package/corpus/templates/analytics/app/pages/Settings.tsx +4 -0
  40. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -3
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +2 -2
  42. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +101 -99
  43. package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +1 -1
  44. package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +18 -5
  45. package/corpus/templates/analytics/app/routes/catalog.tsx +1 -1
  46. package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +9 -0
  47. package/corpus/templates/analytics/app/routes/traffic.tsx +1 -1
  48. package/corpus/templates/analytics/changelog/2026-06-23-the-dashboards-and-analyses-sidebar-settings-buttons-now-sta.md +6 -0
  49. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +145 -0
  50. package/corpus/templates/analytics/server/db/index.ts +1 -1
  51. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -2
  52. package/corpus/templates/analytics/server/lib/demo-dashboards.ts +1 -1
  53. package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -2
  54. package/corpus/templates/analytics/server/plugins/core-routes.ts +6 -6
  55. package/corpus/templates/assets/CHANGELOG.md +10 -0
  56. package/corpus/templates/assets/app/root.tsx +7 -1
  57. package/corpus/templates/assets/app/routes/settings.tsx +6 -0
  58. package/corpus/templates/brain/CHANGELOG.md +10 -0
  59. package/corpus/templates/brain/app/root.tsx +7 -1
  60. package/corpus/templates/brain/app/routes/settings.tsx +8 -1
  61. package/corpus/templates/calendar/CHANGELOG.md +10 -0
  62. package/corpus/templates/calendar/app/pages/Settings.tsx +4 -0
  63. package/corpus/templates/calendar/app/root.tsx +7 -1
  64. package/corpus/templates/chat/CHANGELOG.md +10 -0
  65. package/corpus/templates/chat/app/root.tsx +7 -1
  66. package/corpus/templates/clips/CHANGELOG.md +10 -0
  67. package/corpus/templates/clips/app/components/player/share-dialog.tsx +33 -7
  68. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +8 -0
  69. package/corpus/templates/clips/app/components/player/video-player.tsx +150 -20
  70. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +7 -11
  71. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +31 -0
  72. package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +71 -0
  73. package/corpus/templates/clips/app/root.tsx +7 -1
  74. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +15 -1
  75. package/corpus/templates/clips/app/routes/record.tsx +78 -5
  76. package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -5
  77. package/corpus/templates/clips/changelog/2026-06-23-clips-shared-in-slack-no-longer-show-a-could-not-start-playb.md +6 -0
  78. package/corpus/templates/clips/changelog/2026-06-23-made-the-play-button-on-shared-and-embedded-clips-scale-with.md +6 -0
  79. package/corpus/templates/clips/changelog/2026-06-23-public-clip-share-links-now-show-whether-they-ll-play-inline.md +6 -0
  80. package/corpus/templates/clips/changelog/2026-06-23-screen-camera-recordings-now-keep-the-presenter-s-face-in-th.md +6 -0
  81. package/corpus/templates/clips/desktop/src/lib/recorder.ts +20 -0
  82. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  83. package/corpus/templates/clips/desktop/src/overlays/region-record-border.tsx +76 -0
  84. package/corpus/templates/clips/desktop/src/styles.css +49 -0
  85. package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
  86. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +67 -1
  87. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
  88. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +4 -1
  89. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +51 -1
  90. package/corpus/templates/clips/shared/share-attribution.ts +79 -0
  91. package/corpus/templates/content/CHANGELOG.md +10 -0
  92. package/corpus/templates/content/app/root.tsx +7 -1
  93. package/corpus/templates/design/CHANGELOG.md +10 -0
  94. package/corpus/templates/design/actions/generate-design.ts +4 -0
  95. package/corpus/templates/design/app/root.tsx +7 -1
  96. package/corpus/templates/design/app/routes/settings.tsx +10 -2
  97. package/corpus/templates/dispatch/CHANGELOG.md +10 -0
  98. package/corpus/templates/dispatch/app/root.tsx +7 -1
  99. package/corpus/templates/forms/CHANGELOG.md +10 -0
  100. package/corpus/templates/forms/app/root.tsx +7 -1
  101. package/corpus/templates/macros/CHANGELOG.md +10 -0
  102. package/corpus/templates/macros/app/root.tsx +7 -1
  103. package/corpus/templates/mail/CHANGELOG.md +10 -0
  104. package/corpus/templates/mail/app/components/layout/CommandPalette.tsx +3 -0
  105. package/corpus/templates/mail/app/pages/SettingsPage.tsx +27 -0
  106. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +31 -17
  107. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -4
  108. package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +34 -24
  109. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -4
  110. package/corpus/templates/plan/AGENTS.md +4 -0
  111. package/corpus/templates/plan/CHANGELOG.md +10 -0
  112. package/corpus/templates/plan/actions/view-screen.ts +5 -0
  113. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +98 -1
  114. package/corpus/templates/plan/app/pages/PlansPage.tsx +107 -7
  115. package/corpus/templates/plan/app/root.tsx +7 -1
  116. package/corpus/templates/plan/shared/share-attribution.ts +72 -0
  117. package/corpus/templates/slides/CHANGELOG.md +10 -0
  118. package/corpus/templates/slides/app/root.tsx +7 -1
  119. package/corpus/templates/videos/CHANGELOG.md +4 -151
  120. package/corpus/templates/videos/app/root.tsx +7 -1
  121. package/dist/changelog/parse.d.ts +70 -0
  122. package/dist/changelog/parse.d.ts.map +1 -0
  123. package/dist/changelog/parse.js +200 -0
  124. package/dist/changelog/parse.js.map +1 -0
  125. package/dist/cli/changelog.d.ts +4 -0
  126. package/dist/cli/changelog.d.ts.map +1 -0
  127. package/dist/cli/changelog.js +166 -0
  128. package/dist/cli/changelog.js.map +1 -0
  129. package/dist/cli/index.js +18 -0
  130. package/dist/cli/index.js.map +1 -1
  131. package/dist/cli/recap.d.ts.map +1 -1
  132. package/dist/cli/recap.js +22 -1
  133. package/dist/cli/recap.js.map +1 -1
  134. package/dist/cli/skills.d.ts +3 -3
  135. package/dist/cli/skills.d.ts.map +1 -1
  136. package/dist/cli/skills.js +72 -45
  137. package/dist/cli/skills.js.map +1 -1
  138. package/dist/client/CommandMenu.d.ts +17 -1
  139. package/dist/client/CommandMenu.d.ts.map +1 -1
  140. package/dist/client/CommandMenu.js +49 -13
  141. package/dist/client/CommandMenu.js.map +1 -1
  142. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  143. package/dist/client/agent-chat-adapter.js +43 -6
  144. package/dist/client/agent-chat-adapter.js.map +1 -1
  145. package/dist/client/analytics.d.ts +18 -0
  146. package/dist/client/analytics.d.ts.map +1 -1
  147. package/dist/client/analytics.js +177 -0
  148. package/dist/client/analytics.js.map +1 -1
  149. package/dist/client/changelog/Changelog.d.ts +47 -0
  150. package/dist/client/changelog/Changelog.d.ts.map +1 -0
  151. package/dist/client/changelog/Changelog.js +138 -0
  152. package/dist/client/changelog/Changelog.js.map +1 -0
  153. package/dist/client/index.d.ts +2 -1
  154. package/dist/client/index.d.ts.map +1 -1
  155. package/dist/client/index.js +2 -1
  156. package/dist/client/index.js.map +1 -1
  157. package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
  158. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  159. package/dist/client/mcp-apps/McpAppRenderer.js +42 -1
  160. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
  161. package/dist/client/settings/useBuilderStatus.d.ts +2 -0
  162. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  163. package/dist/client/settings/useBuilderStatus.js +58 -10
  164. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  165. package/dist/mcp/build-server.js +1 -1
  166. package/dist/mcp/build-server.js.map +1 -1
  167. package/dist/mcp/embed-app.d.ts.map +1 -1
  168. package/dist/mcp/embed-app.js +112 -6
  169. package/dist/mcp/embed-app.js.map +1 -1
  170. package/dist/server/attribution.d.ts +83 -0
  171. package/dist/server/attribution.d.ts.map +1 -0
  172. package/dist/server/attribution.js +178 -0
  173. package/dist/server/attribution.js.map +1 -0
  174. package/dist/server/auth.d.ts.map +1 -1
  175. package/dist/server/auth.js +7 -0
  176. package/dist/server/auth.js.map +1 -1
  177. package/dist/server/better-auth-instance.d.ts +9 -1
  178. package/dist/server/better-auth-instance.d.ts.map +1 -1
  179. package/dist/server/better-auth-instance.js +32 -2
  180. package/dist/server/better-auth-instance.js.map +1 -1
  181. package/dist/server/builder-browser.d.ts +4 -0
  182. package/dist/server/builder-browser.d.ts.map +1 -1
  183. package/dist/server/builder-browser.js +20 -1
  184. package/dist/server/builder-browser.js.map +1 -1
  185. package/dist/server/core-routes-plugin.d.ts +25 -0
  186. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  187. package/dist/server/core-routes-plugin.js +38 -0
  188. package/dist/server/core-routes-plugin.js.map +1 -1
  189. package/dist/server/google-oauth.d.ts.map +1 -1
  190. package/dist/server/google-oauth.js +16 -2
  191. package/dist/server/google-oauth.js.map +1 -1
  192. package/dist/shared/mcp-embed-headers.js +1 -1
  193. package/dist/shared/mcp-embed-headers.js.map +1 -1
  194. package/dist/styles/agent-conversation.css +21 -0
  195. package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  196. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  197. package/package.json +1 -1
  198. package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  199. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  200. /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));
@@ -308,8 +308,9 @@ export function PreRecordPanel({
308
308
  }, [cameraId, cameras]);
309
309
 
310
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 (covers the
312
- // ?mode=camera deep-link/restore path the mode buttons already handle).
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.
313
314
  useEffect(() => {
314
315
  if (mode === "camera" && cameraId === NO_CAMERA_DEVICE_ID) {
315
316
  setCameraId("default");
@@ -583,15 +584,10 @@ export function PreRecordPanel({
583
584
  <button
584
585
  key={opt.value}
585
586
  type="button"
586
- onClick={() => {
587
- chooseMode(opt.value);
588
- if (
589
- opt.value === "camera" &&
590
- cameraId === NO_CAMERA_DEVICE_ID
591
- ) {
592
- chooseCamera("default");
593
- }
594
- }}
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)}
595
591
  className={cn(
596
592
  "flex min-h-20 min-w-0 flex-col justify-between rounded-xl border p-3 text-left transition-colors",
597
593
  active
@@ -420,6 +420,13 @@ export class RecorderEngine {
420
420
  */
421
421
  private cameraDisconnectNotified = false;
422
422
  private micDisconnectNotified = false;
423
+ /**
424
+ * Set when a stale/unavailable `micDeviceId` forces a fallback to the system
425
+ * default mic during `acquire()`. The recording then runs on the default
426
+ * device, so live transcription (which can only ever use the default mic) is
427
+ * safe to start even though a specific mic was originally requested.
428
+ */
429
+ private micFellBackToDefault = false;
423
430
 
424
431
  private state: RecorderState = "idle";
425
432
 
@@ -448,10 +455,30 @@ export class RecorderEngine {
448
455
  return this.cameraStream;
449
456
  }
450
457
 
458
+ /**
459
+ * True when `acquire()` had to fall back from the requested `micDeviceId` to
460
+ * the system default mic. Lets the UI start live transcription for this
461
+ * session even though a specific mic was originally selected.
462
+ */
463
+ didMicFallBackToDefault(): boolean {
464
+ return this.micFellBackToDefault;
465
+ }
466
+
451
467
  getPreviewStream(): MediaStream | null {
452
468
  return this.previewStream;
453
469
  }
454
470
 
471
+ /**
472
+ * The composited screen+camera canvas stream that actually gets recorded
473
+ * (screen with the camera bubble drawn in), or `null` for screen-only /
474
+ * camera-only modes where the visible preview already matches the recording.
475
+ * Used to grab a thumbnail that includes the presenter's camera — the raw
476
+ * preview stream in screen+camera mode is screen-only and has no face.
477
+ */
478
+ getCompositeStream(): MediaStream | null {
479
+ return this.cameraComposite?.stream ?? null;
480
+ }
481
+
455
482
  getElapsedMs(): number {
456
483
  if (this.startedAtMs === null) return 0;
457
484
  const now = performance.now();
@@ -515,6 +542,7 @@ export class RecorderEngine {
515
542
  const wantsCamera =
516
543
  this.opts.mode === "camera" || this.opts.mode === "screen+camera";
517
544
  const wantsMic = this.opts.micDeviceId !== NO_MIC_DEVICE_ID;
545
+ this.micFellBackToDefault = false;
518
546
 
519
547
  try {
520
548
  if (!isBrowserSecureContext()) {
@@ -634,6 +662,9 @@ export class RecorderEngine {
634
662
  audio: voiceFocusedAudioConstraints(),
635
663
  video: false,
636
664
  });
665
+ // Recording is now on the system default mic, so live
666
+ // transcription can run for this session after all.
667
+ this.micFellBackToDefault = true;
637
668
  } catch (retryErr) {
638
669
  throw this.friendlyError(retryErr, "microphone");
639
670
  }
@@ -0,0 +1,71 @@
1
+ import { IconBrandSlack } from "@tabler/icons-react";
2
+ import { appPath, useActionQuery } from "@agent-native/core/client";
3
+
4
+ interface SlackInstallation {
5
+ status: string;
6
+ }
7
+
8
+ interface SlackInstallationsResponse {
9
+ oauthConfigured: boolean;
10
+ installations: SlackInstallation[];
11
+ }
12
+
13
+ /**
14
+ * Contextual nudge shown in the share popover's Link tab for public clips.
15
+ *
16
+ * A public Clips link unfurls into a playable video when pasted in a connected
17
+ * Slack workspace, but that integration is otherwise only discoverable in
18
+ * Settings. When a workspace is connected we confirm the payoff; when none is,
19
+ * clip owners/admins get a one-click link to connect one in Settings.
20
+ *
21
+ * Only rendered for public clips (the caller gates on `isPublic`), so the
22
+ * `list-slack-installations` query never runs for private shares.
23
+ */
24
+ export function SlackShareHint({ canManage }: { canManage: boolean }) {
25
+ const slack = useActionQuery<SlackInstallationsResponse>(
26
+ "list-slack-installations",
27
+ undefined,
28
+ { retry: false },
29
+ );
30
+
31
+ const data = slack.data;
32
+ const connected =
33
+ data?.installations?.some((i) => i.status === "connected") ?? false;
34
+
35
+ if (connected) {
36
+ return (
37
+ <div className="flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2">
38
+ <IconBrandSlack className="h-4 w-4 shrink-0 text-muted-foreground" />
39
+ <div className="min-w-0">
40
+ <div className="text-xs font-medium">Plays inline in Slack</div>
41
+ <p className="text-[11px] text-muted-foreground">
42
+ Paste this link in any connected workspace to play it inline.
43
+ </p>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ // No workspace connected yet. Connecting needs the deployment's Slack OAuth
50
+ // credentials, so only point managers at Settings when it's actually set up —
51
+ // otherwise the Connect button there is disabled and the link is a dead end.
52
+ // While the query is loading or errored, `data` is undefined and we render
53
+ // nothing rather than guess at the workspace state.
54
+ if (!data || !canManage || !data.oauthConfigured) return null;
55
+
56
+ return (
57
+ <a
58
+ href={appPath("/settings#slack")}
59
+ className="flex items-center gap-2.5 rounded-md border border-border px-3 py-2 transition-colors hover:bg-muted/50"
60
+ >
61
+ <IconBrandSlack className="h-4 w-4 shrink-0 text-muted-foreground" />
62
+ <div className="min-w-0 flex-1">
63
+ <div className="text-xs font-medium">Make it play inline in Slack</div>
64
+ <p className="text-[11px] text-muted-foreground">
65
+ Connect a workspace so this link unfurls as a video.
66
+ </p>
67
+ </div>
68
+ <span className="shrink-0 text-xs font-medium text-primary">Connect</span>
69
+ </a>
70
+ );
71
+ }
@@ -21,6 +21,7 @@ import {
21
21
  } from "@agent-native/core/client";
22
22
  import { IconCheck, IconSun, IconMoon } from "@tabler/icons-react";
23
23
  import { useTheme } from "next-themes";
24
+ import changelog from "../CHANGELOG.md?raw";
24
25
  import { Toaster } from "@/components/ui/sonner";
25
26
  import { Button } from "@/components/ui/button";
26
27
  import {
@@ -258,7 +259,12 @@ function AppContent() {
258
259
  {standalonePublic ? null : <DbSyncSetup />}
259
260
  {standalonePublic ? null : <ClipsExtensionAuthBridge />}
260
261
  {standalonePublic ? null : (
261
- <CommandMenu open={cmdkOpen} onOpenChange={setCmdkOpen}>
262
+ <CommandMenu
263
+ open={cmdkOpen}
264
+ onOpenChange={setCmdkOpen}
265
+ changelog={changelog}
266
+ changelogKey="clips"
267
+ >
262
268
  <CommandMenu.Group heading="Actions">
263
269
  <CommandMenu.Item onSelect={() => {}}>Search</CommandMenu.Item>
264
270
  </CommandMenu.Group>