@agent-native/core 0.164.3 → 0.164.11

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 (234) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
  3. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
  4. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
  5. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
  6. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
  7. package/corpus/templates/calendar/app/i18n-data.ts +120 -0
  8. package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
  9. package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
  10. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  11. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  12. package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  13. package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  14. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +1 -1
  15. package/corpus/templates/clips/app/components/player/video-player.tsx +12 -0
  16. package/corpus/templates/clips/app/routes/_app.dictate.tsx +18 -4
  17. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +114 -20
  18. package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -78
  19. package/corpus/templates/clips/changelog/2026-08-19-clips-nightly-builds-are-available-for-trying-the-latest-upd.md +6 -0
  20. package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +6 -0
  21. package/corpus/templates/clips/desktop/README.md +19 -4
  22. package/corpus/templates/clips/desktop/src/app.tsx +78 -16
  23. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -2
  24. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -0
  25. package/corpus/templates/clips/server/lib/slack-unfurls.ts +7 -13
  26. package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  27. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
  28. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  29. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
  30. package/corpus/templates/clips/server/routes/api/clips-updater.json.get.ts +41 -22
  31. package/corpus/templates/clips/shared/share-meta.ts +67 -3
  32. package/corpus/templates/content/actions/list-content-databases.ts +95 -1
  33. package/corpus/templates/content/changelog/2026-08-11-content-mutations-delegated-from-verified-slack-dms-now-use-.md +6 -0
  34. package/corpus/templates/content/shared/api.ts +11 -0
  35. package/corpus/templates/factory/AGENTS.md +1 -1
  36. package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
  37. package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
  38. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
  39. package/corpus/templates/factory/app/i18n/en-US.ts +4 -0
  40. package/corpus/templates/factory/app/routes/factory.tsx +209 -46
  41. package/corpus/templates/factory/server/connectors/slack.ts +44 -2
  42. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -7
  43. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
  44. package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
  45. package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
  46. package/corpus/templates/mail/actions/view-screen.ts +1 -1
  47. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
  48. package/corpus/templates/mail/server/handlers/google-auth.ts +125 -24
  49. package/corpus/templates/mail/server/lib/gmail-query.ts +5 -2
  50. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  51. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  52. package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  53. package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  54. package/corpus/templates/plan/app/pages/PlansPage.tsx +0 -12
  55. package/corpus/templates/plan/changelog/2026-08-19-plan-prompts-now-guide-you-to-connect-ai-before-sending-and-.md +6 -0
  56. package/corpus/templates/slides/actions/approve-deck-access-request.ts +234 -0
  57. package/corpus/templates/slides/actions/get-deck-access-status.ts +35 -2
  58. package/corpus/templates/slides/actions/request-deck-access.ts +263 -64
  59. package/corpus/templates/slides/app/components/editor/MissingDeckAccessPane.tsx +196 -0
  60. package/corpus/templates/slides/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
  61. package/corpus/templates/slides/app/hooks/use-deck-access.ts +7 -4
  62. package/corpus/templates/slides/app/i18n/en-US.ts +29 -6
  63. package/corpus/templates/slides/app/pages/DeckEditor.tsx +147 -169
  64. package/corpus/templates/slides/app/routes/access-request.approve.tsx +250 -0
  65. package/corpus/templates/slides/server/db/schema.ts +8 -0
  66. package/corpus/templates/slides/server/lib/access-request-email.ts +36 -0
  67. package/corpus/templates/slides/server/lib/emails.ts +26 -0
  68. package/corpus/templates/slides/server/plugins/agent-chat.ts +7 -0
  69. package/corpus/templates/slides/server/plugins/db.ts +50 -0
  70. package/corpus/templates/slides/shared/deck-access.ts +31 -0
  71. package/dist/a2a/artifact-response.d.ts +50 -0
  72. package/dist/a2a/artifact-response.js +266 -39
  73. package/dist/a2a/index.d.ts +2 -0
  74. package/dist/a2a/index.js +1 -0
  75. package/dist/a2a/task-store.d.ts +1 -0
  76. package/dist/a2a/task-store.js +1 -1
  77. package/dist/agent/observational-memory/store.d.ts +1 -0
  78. package/dist/agent/observational-memory/store.js +1 -1
  79. package/dist/agent/run-store.d.ts +1 -0
  80. package/dist/agent/run-store.js +1 -1
  81. package/dist/agent/tool-approval-store.d.ts +1 -0
  82. package/dist/agent/tool-approval-store.js +1 -1
  83. package/dist/application-state/store.d.ts +1 -0
  84. package/dist/application-state/store.js +1 -1
  85. package/dist/browser-sessions/store.d.ts +1 -0
  86. package/dist/browser-sessions/store.js +1 -1
  87. package/dist/checkpoints/store.d.ts +1 -0
  88. package/dist/checkpoints/store.js +1 -1
  89. package/dist/cli/code-agent-schedules.d.ts +11 -0
  90. package/dist/cli/code-agent-schedules.js +44 -5
  91. package/dist/client/AgentPanel.d.ts +5 -1
  92. package/dist/client/AgentPanel.js +2 -2
  93. package/dist/client/AssistantChat.d.ts +5 -1
  94. package/dist/client/AssistantChat.js +40 -6
  95. package/dist/client/EnvironmentBadge.d.ts +7 -13
  96. package/dist/client/EnvironmentBadge.js +43 -92
  97. package/dist/client/agent-chat/index.d.ts +1 -0
  98. package/dist/client/agent-chat/index.js +1 -0
  99. package/dist/client/agent-chat-adapter.js +76 -50
  100. package/dist/client/app-providers.js +4 -4
  101. package/dist/client/chat/message-components.d.ts +4 -0
  102. package/dist/client/chat/message-components.js +28 -1
  103. package/dist/client/chat/run-recovery.js +26 -0
  104. package/dist/client/chat-first/apps-rail.d.ts +1 -0
  105. package/dist/client/chat-first/apps-rail.js +28 -2
  106. package/dist/client/sse-event-processor.js +13 -8
  107. package/dist/client/use-active-agent-chat-run.d.ts +2 -0
  108. package/dist/client/use-active-agent-chat-run.js +22 -0
  109. package/dist/coding-tools/sandbox/executions-store.d.ts +1 -0
  110. package/dist/coding-tools/sandbox/executions-store.js +1 -1
  111. package/dist/collab/awareness-store.d.ts +1 -0
  112. package/dist/collab/awareness-store.js +1 -1
  113. package/dist/collab/routes.d.ts +1 -1
  114. package/dist/collab/storage.d.ts +1 -0
  115. package/dist/collab/storage.js +1 -1
  116. package/dist/db/client.js +3 -3
  117. package/dist/db/ddl-guard.js +8 -0
  118. package/dist/db/migration-runtime.d.ts +23 -0
  119. package/dist/db/migration-runtime.js +41 -0
  120. package/dist/db/migrations.d.ts +1 -2
  121. package/dist/db/migrations.js +2 -24
  122. package/dist/email-catalog/log.d.ts +1 -0
  123. package/dist/email-catalog/log.js +1 -1
  124. package/dist/guards/index.d.ts +1 -0
  125. package/dist/guards/index.js +1 -0
  126. package/dist/guards/release-schema-complete.d.ts +22 -0
  127. package/dist/guards/release-schema-complete.js +144 -0
  128. package/dist/integrations/a2a-continuations-store.d.ts +1 -0
  129. package/dist/integrations/a2a-continuations-store.js +1 -1
  130. package/dist/integrations/adapters/slack.js +58 -7
  131. package/dist/integrations/awaiting-input-store.d.ts +1 -0
  132. package/dist/integrations/awaiting-input-store.js +1 -1
  133. package/dist/integrations/config-store.d.ts +1 -0
  134. package/dist/integrations/config-store.js +1 -1
  135. package/dist/integrations/controls-store.d.ts +1 -0
  136. package/dist/integrations/controls-store.js +1 -1
  137. package/dist/integrations/identity-links-store.d.ts +1 -0
  138. package/dist/integrations/identity-links-store.js +1 -1
  139. package/dist/integrations/installations-store.d.ts +1 -0
  140. package/dist/integrations/installations-store.js +1 -1
  141. package/dist/integrations/remote-commands-store.d.ts +1 -0
  142. package/dist/integrations/remote-commands-store.js +1 -1
  143. package/dist/integrations/remote-devices-store.d.ts +1 -0
  144. package/dist/integrations/remote-devices-store.js +1 -1
  145. package/dist/integrations/remote-push-store.d.ts +1 -0
  146. package/dist/integrations/remote-push-store.js +1 -1
  147. package/dist/integrations/remote-run-events-store.d.ts +1 -0
  148. package/dist/integrations/remote-run-events-store.js +1 -1
  149. package/dist/integrations/scope-store.d.ts +1 -0
  150. package/dist/integrations/scope-store.js +1 -1
  151. package/dist/integrations/thread-mapping-store.d.ts +1 -0
  152. package/dist/integrations/thread-mapping-store.js +1 -1
  153. package/dist/integrations/usage-budget-store.d.ts +1 -0
  154. package/dist/integrations/usage-budget-store.js +1 -1
  155. package/dist/jobs/background-automation-runner.js +111 -9
  156. package/dist/jobs/run-history.d.ts +1 -0
  157. package/dist/jobs/run-history.js +1 -1
  158. package/dist/jobs/scheduler-health.d.ts +1 -0
  159. package/dist/jobs/scheduler-health.js +1 -1
  160. package/dist/mcp/approval-store.d.ts +1 -0
  161. package/dist/mcp/approval-store.js +1 -1
  162. package/dist/mcp/connect-store.d.ts +1 -0
  163. package/dist/mcp/connect-store.js +1 -1
  164. package/dist/mcp/oauth-store.d.ts +1 -0
  165. package/dist/mcp/oauth-store.js +1 -1
  166. package/dist/notifications/routes.d.ts +3 -3
  167. package/dist/notifications/store.d.ts +1 -0
  168. package/dist/notifications/store.js +1 -1
  169. package/dist/oauth-tokens/store.d.ts +1 -0
  170. package/dist/oauth-tokens/store.js +1 -1
  171. package/dist/observability/routes.d.ts +3 -3
  172. package/dist/progress/routes.d.ts +1 -1
  173. package/dist/progress/store.d.ts +1 -0
  174. package/dist/progress/store.js +1 -1
  175. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  176. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  177. package/dist/provider-api/corpus-jobs-store.d.ts +1 -0
  178. package/dist/provider-api/corpus-jobs-store.js +1 -1
  179. package/dist/provider-api/custom-registry.d.ts +1 -0
  180. package/dist/provider-api/custom-registry.js +1 -1
  181. package/dist/provider-api/quota-governor.d.ts +1 -0
  182. package/dist/provider-api/quota-governor.js +1 -1
  183. package/dist/provider-api/staged-datasets-store.d.ts +1 -0
  184. package/dist/provider-api/staged-datasets-store.js +1 -1
  185. package/dist/resources/handlers.d.ts +3 -3
  186. package/dist/resources/store.d.ts +1 -0
  187. package/dist/resources/store.js +1 -1
  188. package/dist/scripts/dev/index.js +15 -1
  189. package/dist/search/index.js +3 -0
  190. package/dist/secrets/storage.d.ts +1 -0
  191. package/dist/secrets/storage.js +1 -1
  192. package/dist/server/agent-chat/action-filters-a2a.d.ts +2 -1
  193. package/dist/server/agent-chat/action-filters-a2a.js +10 -2
  194. package/dist/server/agent-chat-plugin.js +71 -20
  195. package/dist/server/agent-teams-run-queue.d.ts +1 -0
  196. package/dist/server/agent-teams-run-queue.js +1 -1
  197. package/dist/server/auth-marketing.js +3 -1
  198. package/dist/server/auth.d.ts +18 -1
  199. package/dist/server/auth.js +370 -154
  200. package/dist/server/embed-session.d.ts +1 -0
  201. package/dist/server/embed-session.js +1 -1
  202. package/dist/server/google-auth-plugin.js +3 -1
  203. package/dist/server/google-oauth.d.ts +10 -0
  204. package/dist/server/google-oauth.js +25 -1
  205. package/dist/server/identity-sso-store.d.ts +1 -0
  206. package/dist/server/identity-sso-store.js +1 -1
  207. package/dist/server/index.d.ts +1 -1
  208. package/dist/server/index.js +1 -1
  209. package/dist/server/onboarding-html.js +200 -31
  210. package/dist/server/poll.d.ts +2 -0
  211. package/dist/server/poll.js +19 -1
  212. package/dist/server/realtime-token.d.ts +1 -1
  213. package/dist/server/release-migrations.js +5 -0
  214. package/dist/server/release-schema.d.ts +46 -0
  215. package/dist/server/release-schema.js +307 -0
  216. package/dist/settings/store.d.ts +1 -0
  217. package/dist/settings/store.js +1 -1
  218. package/dist/shared/environment-lanes.d.ts +28 -0
  219. package/dist/shared/environment-lanes.js +61 -0
  220. package/dist/sharing/actions/share-resource.js +22 -2
  221. package/dist/styles/agent-native.css +17 -15
  222. package/dist/triggers/routes.d.ts +11 -0
  223. package/dist/triggers/routes.js +10 -0
  224. package/dist/usage/alerts-store.d.ts +1 -0
  225. package/dist/usage/alerts-store.js +1 -1
  226. package/dist/usage/store.d.ts +1 -0
  227. package/dist/usage/store.js +1 -1
  228. package/docs/content/drop-in-agent.mdx +4 -4
  229. package/docs/content/faq.mdx +15 -15
  230. package/docs/content/getting-started.mdx +173 -41
  231. package/docs/content/key-concepts.mdx +10 -10
  232. package/docs/content/using-your-agent.mdx +2 -2
  233. package/docs/content/what-is-agent-native.mdx +79 -228
  234. package/package.json +2 -2
@@ -45,7 +45,7 @@ import {
45
45
  IconSparkles,
46
46
  IconExternalLink,
47
47
  } from "@tabler/icons-react";
48
- import { useQuery } from "@tanstack/react-query";
48
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
49
49
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
50
50
  import { createPortal } from "react-dom";
51
51
  import {
@@ -110,6 +110,64 @@ const PROCESSING_STUCK_TIMEOUT_MS = 12 * 60 * 1000;
110
110
  const READY_MEDIA_SETTLE_POLL_MS = 20 * 1000;
111
111
  const READY_MEDIA_SETTLE_POLL_INTERVAL_MS = 1000;
112
112
 
113
+ type RecordingReaction = {
114
+ id: string;
115
+ emoji: string;
116
+ videoTimestampMs: number;
117
+ };
118
+
119
+ type PendingRecordingReaction = RecordingReaction & {
120
+ recordingId: string;
121
+ };
122
+
123
+ export function mergeRecordingReactions(
124
+ serverReactions: RecordingReaction[] | undefined,
125
+ pendingReactions: PendingRecordingReaction[],
126
+ recordingId: string | undefined,
127
+ ) {
128
+ const seen = new Set<string>();
129
+ const merged: RecordingReaction[] = [];
130
+ const visiblePendingReactions = recordingId
131
+ ? pendingReactions.filter(
132
+ (reaction) => reaction.recordingId === recordingId,
133
+ )
134
+ : [];
135
+
136
+ for (const reaction of [
137
+ ...(serverReactions ?? []),
138
+ ...visiblePendingReactions,
139
+ ]) {
140
+ const key = `${reaction.id}:${reaction.emoji}:${reaction.videoTimestampMs}`;
141
+ if (seen.has(key)) continue;
142
+ seen.add(key);
143
+ merged.push(reaction);
144
+ }
145
+
146
+ return merged;
147
+ }
148
+
149
+ export function removePendingReaction(
150
+ pendingReactions: PendingRecordingReaction[],
151
+ pendingId: string,
152
+ ) {
153
+ return pendingReactions.filter((reaction) => reaction.id !== pendingId);
154
+ }
155
+
156
+ export function handleReactionWrite(
157
+ response: { ok: boolean; status: number },
158
+ refresh: () => Promise<unknown>,
159
+ ) {
160
+ if (!response.ok) throw new Error(`react failed: ${response.status}`);
161
+ // The write is authoritative. A transient read failure must not tell the
162
+ // player that the reaction failed.
163
+ void Promise.resolve()
164
+ .then(refresh)
165
+ .catch((refreshError) => {
166
+ console.warn("[clips] reaction refresh failed", refreshError);
167
+ });
168
+ return true;
169
+ }
170
+
113
171
  export function meta() {
114
172
  return [{ title: enMessages.recordingRoute.pageTitle }];
115
173
  }
@@ -263,16 +321,7 @@ export default function RecordingPage() {
263
321
  // live time bridge, so we fall back to the last position the player reported
264
322
  // via onTimeUpdate (seek/initial start).
265
323
  const resolvePlaybackMs = useCallback(() => {
266
- const liveCt = playerRef.current?.video?.currentTime;
267
- if (
268
- typeof liveCt === "number" &&
269
- Number.isFinite(liveCt) &&
270
- liveCt >= 0 &&
271
- liveCt < 1e7
272
- ) {
273
- return Math.floor(liveCt * 1000);
274
- }
275
- return currentMs;
324
+ return playerRef.current?.getCurrentOriginalMs() ?? currentMs;
276
325
  }, [currentMs]);
277
326
  // The compact layout stacks the panel below the video, so switching tabs
278
327
  // alone leaves the user looking at the player. Desktop always renders the
@@ -310,9 +359,13 @@ export default function RecordingPage() {
310
359
  recordingId ? { type: "recording" as const, id: recordingId } : null,
311
360
  [recordingId],
312
361
  );
362
+ const queryClient = useQueryClient();
313
363
  const lastPlayerStateWriteRef = useRef(0);
314
364
  const readyMediaPollRef = useRef<{ key: string; until: number } | null>(null);
315
365
  const [metadataRefreshUntil, setMetadataRefreshUntil] = useState(0);
366
+ const [pendingReactions, setPendingReactions] = useState<
367
+ PendingRecordingReaction[]
368
+ >([]);
316
369
 
317
370
  const playerDataQ = useActionQuery<any>(
318
371
  "get-recording-player-data",
@@ -414,7 +467,15 @@ export default function RecordingPage() {
414
467
  | "viewer"
415
468
  | undefined;
416
469
  const comments = playerDataQ.data?.comments ?? [];
417
- const reactions = playerDataQ.data?.reactions ?? [];
470
+ const reactions = useMemo(
471
+ () =>
472
+ mergeRecordingReactions(
473
+ playerDataQ.data?.reactions,
474
+ pendingReactions,
475
+ recordingId,
476
+ ),
477
+ [pendingReactions, playerDataQ.data?.reactions, recordingId],
478
+ );
418
479
  const chapters = playerDataQ.data?.chapters ?? [];
419
480
  const transcriptSegments = playerDataQ.data?.transcript?.segments ?? [];
420
481
  const transcriptFullText = playerDataQ.data?.transcript?.fullText ?? null;
@@ -1643,11 +1704,10 @@ export default function RecordingPage() {
1643
1704
  },
1644
1705
  )
1645
1706
  .then((res) => {
1646
- if (!res.ok)
1647
- throw new Error(`react failed: ${res.status}`);
1648
- return playerDataQ.refetch();
1707
+ return handleReactionWrite(res, () =>
1708
+ playerDataQ.refetch(),
1709
+ );
1649
1710
  })
1650
- .then(() => true)
1651
1711
  .catch((err) => {
1652
1712
  console.warn("[clips] react failed", err);
1653
1713
  return false;
@@ -1743,6 +1803,18 @@ export default function RecordingPage() {
1743
1803
  onReact={(emoji) => {
1744
1804
  tracking.reportReaction(emoji);
1745
1805
  const liveMs = resolvePlaybackMs();
1806
+ const pendingReaction: PendingRecordingReaction = {
1807
+ id: `pending-${Date.now()}-${Math.random()
1808
+ .toString(36)
1809
+ .slice(2)}`,
1810
+ emoji,
1811
+ videoTimestampMs: liveMs,
1812
+ recordingId: recording.id,
1813
+ };
1814
+ setPendingReactions((current) => [
1815
+ ...current,
1816
+ pendingReaction,
1817
+ ]);
1746
1818
  return fetch(
1747
1819
  agentNativePath(
1748
1820
  "/_agent-native/actions/react-to-recording",
@@ -1758,12 +1830,34 @@ export default function RecordingPage() {
1758
1830
  },
1759
1831
  )
1760
1832
  .then((res) => {
1761
- if (!res.ok)
1762
- throw new Error(`react failed: ${res.status}`);
1763
- return playerDataQ.refetch();
1833
+ const writeSucceeded = handleReactionWrite(
1834
+ res,
1835
+ () =>
1836
+ Promise.all([
1837
+ queryClient.invalidateQueries({
1838
+ queryKey: [
1839
+ "action",
1840
+ "get-recording-player-data",
1841
+ ],
1842
+ }),
1843
+ playerDataQ.refetch(),
1844
+ ]),
1845
+ );
1846
+ setPendingReactions((current) =>
1847
+ removePendingReaction(
1848
+ current,
1849
+ pendingReaction.id,
1850
+ ),
1851
+ );
1852
+ return writeSucceeded;
1764
1853
  })
1765
- .then(() => true)
1766
1854
  .catch((err) => {
1855
+ setPendingReactions((current) =>
1856
+ removePendingReaction(
1857
+ current,
1858
+ pendingReaction.id,
1859
+ ),
1860
+ );
1767
1861
  console.warn("[clips] react failed", err);
1768
1862
  return false;
1769
1863
  });
@@ -124,6 +124,7 @@ type SharePageMetaRecording = {
124
124
  animatedThumbnailUrl: string | null;
125
125
  visibility: "private" | "org" | "public";
126
126
  status: "uploading" | "processing" | "ready" | "failed";
127
+ hasPassword: boolean;
127
128
  archivedAt: string | null;
128
129
  trashedAt: string | null;
129
130
  };
@@ -270,6 +271,7 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
270
271
  animatedThumbnailUrl: null,
271
272
  visibility: rec.visibility,
272
273
  status: rec.status,
274
+ hasPassword: Boolean(rec.password),
273
275
  archivedAt: rec.archivedAt,
274
276
  trashedAt: rec.trashedAt,
275
277
  };
@@ -315,6 +317,7 @@ export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
315
317
  return buildClipsShareMeta({
316
318
  recording: loaderData?.recording ?? null,
317
319
  origin: loaderData?.origin ?? null,
320
+ basePath: appBasePath(),
318
321
  shareUrl: loaderData?.shareUrl ?? null,
319
322
  });
320
323
  };
@@ -676,6 +679,7 @@ export default function ShareRoute() {
676
679
  // an anonymous viewer sees the same controls but triggers the sign-in
677
680
  // prompt instead (see `requireSignIn` below).
678
681
  const viewerCanComment = Boolean(session) && viewerRole != null;
682
+ const viewerCanUseFullscreenInteractions = !session || viewerCanComment;
679
683
  const viewerIsOwner = Boolean(dataQ.data?.data?.viewer?.isOwner);
680
684
  const canReshareLink =
681
685
  (viewerRole === "viewer" || viewerRole === "commenter") &&
@@ -1249,66 +1253,85 @@ export default function ShareRoute() {
1249
1253
  cta={firstCta}
1250
1254
  onCtaClick={() => tracking.reportCtaClick()}
1251
1255
  onTimeUpdate={(ms) => setCurrentMs(ms)}
1252
- onCommentClick={() => setPanel("comments")}
1256
+ onCommentClick={
1257
+ viewerCanUseFullscreenInteractions
1258
+ ? () => setPanel("comments")
1259
+ : undefined
1260
+ }
1253
1261
  onFullscreenChange={setIsPlayerFullscreen}
1254
- enableComments={recording.enableComments}
1255
- onAddComment={() => {
1256
- if (!session) {
1257
- requireSignIn("comment");
1258
- return;
1259
- }
1260
- const liveCt = isLoomEmbedBacked
1261
- ? null
1262
- : playerRef.current?.video?.currentTime;
1263
- const liveMs =
1264
- typeof liveCt === "number" &&
1265
- Number.isFinite(liveCt) &&
1266
- liveCt >= 0 &&
1267
- liveCt < 1e7
1268
- ? Math.floor(liveCt * 1000)
1269
- : currentMs;
1270
- setCommentAtMs(liveMs);
1271
- setCommentOpen(true);
1272
- }}
1273
- enableReactions={recording.enableReactions}
1274
- onReact={(emoji) => {
1275
- if (!session) {
1276
- requireSignIn("react");
1277
- return false;
1278
- }
1279
- tracking.reportReaction(emoji);
1280
- const liveCt = isLoomEmbedBacked
1281
- ? null
1282
- : playerRef.current?.video?.currentTime;
1283
- const liveMs =
1284
- typeof liveCt === "number" &&
1285
- Number.isFinite(liveCt) &&
1286
- liveCt >= 0 &&
1287
- liveCt < 1e7
1288
- ? Math.floor(liveCt * 1000)
1289
- : currentMs;
1290
- return fetch(
1291
- agentNativePath("/_agent-native/actions/react-to-recording"),
1292
- {
1293
- method: "POST",
1294
- headers: { "Content-Type": "application/json" },
1295
- body: JSON.stringify({
1296
- recordingId: recording.id,
1297
- emoji,
1298
- videoTimestampMs: liveMs,
1299
- }),
1300
- },
1301
- )
1302
- .then((res) => {
1303
- if (!res.ok) throw new Error(`react failed: ${res.status}`);
1304
- return dataQ.refetch();
1305
- })
1306
- .then(() => true)
1307
- .catch((err) => {
1308
- console.warn("[clips] react failed", err);
1309
- return false;
1310
- });
1311
- }}
1262
+ enableComments={
1263
+ recording.enableComments && viewerCanUseFullscreenInteractions
1264
+ }
1265
+ onAddComment={
1266
+ viewerCanUseFullscreenInteractions
1267
+ ? () => {
1268
+ if (!session) {
1269
+ requireSignIn("comment");
1270
+ return;
1271
+ }
1272
+ const liveCt = isLoomEmbedBacked
1273
+ ? null
1274
+ : playerRef.current?.video?.currentTime;
1275
+ const liveMs =
1276
+ typeof liveCt === "number" &&
1277
+ Number.isFinite(liveCt) &&
1278
+ liveCt >= 0 &&
1279
+ liveCt < 1e7
1280
+ ? Math.floor(liveCt * 1000)
1281
+ : currentMs;
1282
+ setCommentAtMs(liveMs);
1283
+ setCommentOpen(true);
1284
+ }
1285
+ : undefined
1286
+ }
1287
+ enableReactions={
1288
+ recording.enableReactions && viewerCanUseFullscreenInteractions
1289
+ }
1290
+ onReact={
1291
+ viewerCanUseFullscreenInteractions
1292
+ ? (emoji) => {
1293
+ if (!session) {
1294
+ requireSignIn("react");
1295
+ return false;
1296
+ }
1297
+ tracking.reportReaction(emoji);
1298
+ const liveCt = isLoomEmbedBacked
1299
+ ? null
1300
+ : playerRef.current?.video?.currentTime;
1301
+ const liveMs =
1302
+ typeof liveCt === "number" &&
1303
+ Number.isFinite(liveCt) &&
1304
+ liveCt >= 0 &&
1305
+ liveCt < 1e7
1306
+ ? Math.floor(liveCt * 1000)
1307
+ : currentMs;
1308
+ return fetch(
1309
+ agentNativePath(
1310
+ "/_agent-native/actions/react-to-recording",
1311
+ ),
1312
+ {
1313
+ method: "POST",
1314
+ headers: { "Content-Type": "application/json" },
1315
+ body: JSON.stringify({
1316
+ recordingId: recording.id,
1317
+ emoji,
1318
+ videoTimestampMs: liveMs,
1319
+ }),
1320
+ },
1321
+ )
1322
+ .then((res) => {
1323
+ if (!res.ok)
1324
+ throw new Error(`react failed: ${res.status}`);
1325
+ return dataQ.refetch();
1326
+ })
1327
+ .then(() => true)
1328
+ .catch((err) => {
1329
+ console.warn("[clips] react failed", err);
1330
+ return false;
1331
+ });
1332
+ }
1333
+ : undefined
1334
+ }
1312
1335
  className="h-full w-full rounded-none sm:rounded-xl"
1313
1336
  />
1314
1337
  {commentOpen && viewerCanComment
@@ -1351,23 +1374,15 @@ export default function ShareRoute() {
1351
1374
  {recording.enableComments ? (
1352
1375
  <TimestampedCommentButton
1353
1376
  enableComments={recording.enableComments}
1354
- canComment
1377
+ canComment={!session || viewerCanComment}
1355
1378
  className="shrink-0"
1356
1379
  onOpen={() => {
1357
1380
  if (!session) {
1358
1381
  requireSignIn("comment");
1359
1382
  return;
1360
1383
  }
1361
- const liveCt = isLoomEmbedBacked
1362
- ? null
1363
- : playerRef.current?.video?.currentTime;
1364
1384
  const liveMs =
1365
- typeof liveCt === "number" &&
1366
- Number.isFinite(liveCt) &&
1367
- liveCt >= 0 &&
1368
- liveCt < 1e7
1369
- ? Math.floor(liveCt * 1000)
1370
- : currentMs;
1385
+ playerRef.current?.getCurrentOriginalMs() ?? currentMs;
1371
1386
  setCommentAtMs(liveMs);
1372
1387
  setCommentOpen(true);
1373
1388
  }}
@@ -1376,22 +1391,15 @@ export default function ShareRoute() {
1376
1391
  {recording.enableReactions ? (
1377
1392
  <ReactionsTray
1378
1393
  reactions={reactions}
1394
+ disabled={Boolean(session) && !viewerCanComment}
1379
1395
  onReact={(emoji) => {
1380
1396
  if (!session) {
1381
1397
  requireSignIn("react");
1382
1398
  return false;
1383
1399
  }
1384
1400
  tracking.reportReaction(emoji);
1385
- const liveCt = isLoomEmbedBacked
1386
- ? null
1387
- : playerRef.current?.video?.currentTime;
1388
1401
  const liveMs =
1389
- typeof liveCt === "number" &&
1390
- Number.isFinite(liveCt) &&
1391
- liveCt >= 0 &&
1392
- liveCt < 1e7
1393
- ? Math.floor(liveCt * 1000)
1394
- : currentMs;
1402
+ playerRef.current?.getCurrentOriginalMs() ?? currentMs;
1395
1403
  return fetch(
1396
1404
  agentNativePath(
1397
1405
  "/_agent-native/actions/react-to-recording",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-19
4
+ ---
5
+
6
+ Clips Nightly builds are available for trying the latest updates.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-19
4
+ ---
5
+
6
+ Shared clips now show a video frame in social previews when no thumbnail is stored.
@@ -79,13 +79,28 @@ Clips registers itself to open at login by default, then runs quietly in the men
79
79
 
80
80
  ## Releases + auto-update
81
81
 
82
- Clips Desktop ships on its own release channel — tag prefix `clips-v*`, separate from the main `v*` tags used by `packages/desktop-app` (Electron). The in-app updater pulls its manifest from the hosted Clips endpoint (`/api/clips-updater.json`), which proxies the stable pointer release (`clips-latest`) when a signed manifest exists and otherwise returns a no-update manifest so end users do not see release-channel setup errors.
82
+ Clips Desktop has separate stable and Nightly lanes. The stable app keeps the
83
+ `Clips` name, `Clips` binary, `clips://` scheme, and `com.clips.tray` identifier;
84
+ its releases use `clips-v*` tags, the `clips-latest` updater pointer, and
85
+ `/api/clips-updater.json`. Nightly builds are named `Clips Nightly`, use the
86
+ `Clips-Nightly` binary, the `clips-nightly://` scheme, and
87
+ `com.clips.tray.nightly`. They use the `clips-nightly-v*` tags,
88
+ `clips-nightly-latest` pointer, and `/api/clips-updater.json?channel=nightly`.
89
+ The in-app updater only sees the pointer for the channel that produced the
90
+ installed app, and the separate native binary/desktop entry keeps both lanes
91
+ installable at once.
83
92
 
84
93
  ### Shipping a release
85
94
 
86
- 1. Bump `templates/clips/desktop/package.json` version (or pass it via workflow input).
87
- 2. Trigger **Clips Desktop Release** in GitHub Actions (`.github/workflows/clips-desktop-release.yml`). It builds macOS (universal), Windows, and Linux x86_64 installers, signs updater artifacts, and uploads them to `clips-v{version}`.
88
- 3. After all three platforms finish, the `publish-release` job flips the versioned release out of draft and refreshes the `clips-latest` pointer release with the new manifest. Installed macOS, Windows, and Linux AppImage copies auto-download it in the background on their next hourly or app-focus update check.
95
+ 1. For a stable release, dispatch **Clips Desktop Release** in GitHub Actions
96
+ with `channel: production` and an explicit version. Stable releases are
97
+ deliberate workflow runs; pushes to `main` never replace the stable lane.
98
+ 2. Pushes to `main` automatically build the Nightly lane. A Nightly run can
99
+ also be dispatched explicitly with `channel: nightly`.
100
+ 3. The workflow builds macOS (universal), Windows, and Linux x86_64 installers,
101
+ signs updater artifacts, and publishes the versioned release plus that
102
+ channel's pointer manifest. Installed copies auto-download only updates
103
+ from their own lane on the next hourly or app-focus check.
89
104
 
90
105
  ### Auto-update flow (inside the app)
91
106
 
@@ -2062,11 +2062,19 @@ export function App() {
2062
2062
  const controller = new AbortController();
2063
2063
  const abortTimer = setTimeout(() => controller.abort(), POLL_ABORT_MS);
2064
2064
  try {
2065
- const exchangeParams = new URLSearchParams({ flow_id: flowId });
2066
- if (verifier) exchangeParams.set("verifier", verifier);
2067
2065
  const xr = await fetch(
2068
- `${base}/_agent-native/auth/desktop-exchange?${exchangeParams.toString()}`,
2069
- { credentials: "include", signal: controller.signal },
2066
+ `${base}/_agent-native/auth/desktop-exchange?flow_id=${encodeURIComponent(flowId)}`,
2067
+ {
2068
+ credentials: "include",
2069
+ ...(verifier
2070
+ ? {
2071
+ headers: {
2072
+ "X-Agent-Native-Desktop-Verifier": verifier,
2073
+ },
2074
+ }
2075
+ : {}),
2076
+ signal: controller.signal,
2077
+ },
2070
2078
  );
2071
2079
  if (!xr.ok) {
2072
2080
  if (Date.now() - start > TIMEOUT_MS) {
@@ -2130,25 +2138,79 @@ export function App() {
2130
2138
  void tick();
2131
2139
  }
2132
2140
 
2133
- // Google and magic-link verification open in the system browser because
2134
- // the Tauri WebView has its own cookie jar. Both flows return through the
2135
- // same short-lived server-side exchange.
2141
+ // Google verification stays in the bound Tauri WebView so the callback sees
2142
+ // the same browser-binding cookie. Magic-link verification still completes
2143
+ // in the system browser through its own exchange flow.
2136
2144
  async function signInExternal() {
2137
2145
  if (signInInflightRef.current) return;
2138
2146
  signInInflightRef.current = true;
2139
2147
 
2140
2148
  try {
2141
2149
  setSignInError(null);
2142
- const flowId =
2143
- crypto.randomUUID?.() ||
2144
- Math.random().toString(36).slice(2) + Date.now().toString(36);
2150
+ const flowId = crypto.randomUUID?.() ?? null;
2151
+ const verifier = (() => {
2152
+ const randomUuid = crypto.randomUUID;
2153
+ if (typeof randomUuid === "function") {
2154
+ return `${randomUuid.call(crypto)}${randomUuid.call(crypto)}`;
2155
+ }
2156
+ if (typeof crypto.getRandomValues === "function") {
2157
+ const bytes = new Uint8Array(32);
2158
+ crypto.getRandomValues(bytes);
2159
+ let binary = "";
2160
+ for (const byte of bytes) binary += String.fromCharCode(byte);
2161
+ return btoa(binary)
2162
+ .replace(/\+/g, "-")
2163
+ .replace(/\//g, "_")
2164
+ .replace(/=+$/, "");
2165
+ }
2166
+ return null;
2167
+ })();
2168
+ if (!flowId || !verifier) {
2169
+ // A Math.random flow id is guessable, which lets anyone else claim this
2170
+ // sign-in's exchange slot; fail closed rather than weaken the credential.
2171
+ throw new Error("Secure OAuth flow generation is unavailable.");
2172
+ }
2145
2173
  const base = serverUrl.replace(/\/+$/, "");
2146
2174
 
2147
- await openExternal(
2148
- `${base}/_agent-native/google/auth-url?desktop=1&flow_id=${flowId}&redirect=1`,
2175
+ const authParams = new URLSearchParams({
2176
+ desktop: "1",
2177
+ flow_id: flowId,
2178
+ webview: "1",
2179
+ });
2180
+ const authResponse = await fetch(
2181
+ `${base}/_agent-native/google/auth-url?${authParams.toString()}`,
2182
+ {
2183
+ method: "POST",
2184
+ headers: {
2185
+ Accept: "application/json",
2186
+ "X-Agent-Native-Desktop-Verifier": verifier,
2187
+ },
2188
+ },
2149
2189
  );
2190
+ let authPayload: {
2191
+ url?: unknown;
2192
+ error?: unknown;
2193
+ message?: unknown;
2194
+ };
2195
+ try {
2196
+ authPayload = (await authResponse.json()) as typeof authPayload;
2197
+ } catch {
2198
+ throw new Error("Could not start Google sign-in.");
2199
+ }
2200
+ if (!authResponse.ok || typeof authPayload?.url !== "string") {
2201
+ const message =
2202
+ typeof authPayload.message === "string"
2203
+ ? authPayload.message
2204
+ : typeof authPayload.error === "string"
2205
+ ? authPayload.error
2206
+ : "Could not start Google sign-in.";
2207
+ throw new Error(message);
2208
+ }
2150
2209
  setSignInPending("google");
2151
- startDesktopAuthExchange(flowId, "google");
2210
+ // Stay in this exact WebView: the auth bootstrap set the HttpOnly
2211
+ // browser-binding cookie here, and the callback returns this page with
2212
+ // the staged session cookie after Google completes.
2213
+ window.location.href = authPayload.url;
2152
2214
  } catch (err) {
2153
2215
  console.error("[clips-tray] signInExternal failed:", err);
2154
2216
  signInInflightRef.current = false;
@@ -3912,8 +3974,8 @@ export function App() {
3912
3974
  // (not a separate Tauri window). This avoids Tauri 2's separate-WebKit-
3913
3975
  // data-store-per-WebviewWindow cookie-jar issue — the cookie is set in
3914
3976
  // the same webview that reads it on the next /auth/session poll.
3915
- // Google and magic-link verification use the system browser, while the
3916
- // password fallback stays inline in this WebView.
3977
+ // Google verification uses the bound WebView, while magic-link verification
3978
+ // uses the system browser and password stays inline here.
3917
3979
  if (authStatus === "anon") {
3918
3980
  return (
3919
3981
  <div className="app" ref={appRef}>
@@ -4822,7 +4884,7 @@ function SignInForm({
4822
4884
  type="button"
4823
4885
  className="signin-google"
4824
4886
  onClick={onUseBrowser}
4825
- title="Opens your default browser to complete Google sign-in"
4887
+ title="Returns to Clips to complete Google sign-in"
4826
4888
  >
4827
4889
  <GoogleIcon />
4828
4890
  Sign in with Google
@@ -15,8 +15,9 @@ crate-type = ["lib", "cdylib", "staticlib"]
15
15
 
16
16
  # Force the dev binary name so macOS shows "Clips" in the Dock / app switcher
17
17
  # (macOS derives the display name from the executable filename when the app
18
- # is unbundled — e.g. during `tauri dev`). Bundled .app builds read the name
19
- # from `productName` in tauri.conf.json instead.
18
+ # is unbundled — e.g. during `tauri dev`). The Nightly release workflow
19
+ # rewrites this target and tauri.conf.json's mainBinaryName to
20
+ # `Clips-Nightly` before packaging so both lanes can coexist.
20
21
  [[bin]]
21
22
  name = "Clips"
22
23
  path = "src/main.rs"
@@ -2,6 +2,7 @@
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Clips",
4
4
  "version": "0.1.1",
5
+ "mainBinaryName": "Clips",
5
6
  "identifier": "com.clips.tray",
6
7
  "build": {
7
8
  "beforeDevCommand": "pnpm vite:dev",