@agent-native/core 0.164.4 → 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 (214) 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/app/components/player/video-player.tsx +12 -0
  15. package/corpus/templates/clips/app/routes/_app.dictate.tsx +18 -4
  16. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +114 -20
  17. package/corpus/templates/clips/app/routes/share.$shareId.tsx +83 -78
  18. package/corpus/templates/clips/changelog/2026-08-19-clips-nightly-builds-are-available-for-trying-the-latest-upd.md +6 -0
  19. package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +1 -0
  20. package/corpus/templates/clips/desktop/README.md +19 -4
  21. package/corpus/templates/clips/desktop/src/app.tsx +78 -16
  22. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -2
  23. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -0
  24. package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  25. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
  26. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  27. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
  28. package/corpus/templates/clips/server/routes/api/clips-updater.json.get.ts +41 -22
  29. package/corpus/templates/factory/AGENTS.md +1 -1
  30. package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
  31. package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
  32. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
  33. package/corpus/templates/factory/app/i18n/en-US.ts +4 -0
  34. package/corpus/templates/factory/app/routes/factory.tsx +209 -46
  35. package/corpus/templates/factory/server/connectors/slack.ts +44 -2
  36. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -7
  37. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
  38. package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
  39. package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
  40. package/corpus/templates/mail/actions/view-screen.ts +1 -1
  41. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
  42. package/corpus/templates/mail/server/handlers/google-auth.ts +125 -24
  43. package/corpus/templates/mail/server/lib/gmail-query.ts +5 -2
  44. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  45. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  46. package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  47. package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  48. package/corpus/templates/plan/app/pages/PlansPage.tsx +0 -12
  49. package/corpus/templates/plan/changelog/2026-08-19-plan-prompts-now-guide-you-to-connect-ai-before-sending-and-.md +6 -0
  50. package/corpus/templates/slides/actions/approve-deck-access-request.ts +234 -0
  51. package/corpus/templates/slides/actions/get-deck-access-status.ts +35 -2
  52. package/corpus/templates/slides/actions/request-deck-access.ts +263 -64
  53. package/corpus/templates/slides/app/components/editor/MissingDeckAccessPane.tsx +196 -0
  54. package/corpus/templates/slides/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
  55. package/corpus/templates/slides/app/hooks/use-deck-access.ts +7 -4
  56. package/corpus/templates/slides/app/i18n/en-US.ts +29 -6
  57. package/corpus/templates/slides/app/pages/DeckEditor.tsx +147 -169
  58. package/corpus/templates/slides/app/routes/access-request.approve.tsx +250 -0
  59. package/corpus/templates/slides/server/db/schema.ts +8 -0
  60. package/corpus/templates/slides/server/lib/access-request-email.ts +36 -0
  61. package/corpus/templates/slides/server/lib/emails.ts +26 -0
  62. package/corpus/templates/slides/server/plugins/agent-chat.ts +7 -0
  63. package/corpus/templates/slides/server/plugins/db.ts +50 -0
  64. package/corpus/templates/slides/shared/deck-access.ts +31 -0
  65. package/dist/a2a/task-store.d.ts +1 -0
  66. package/dist/a2a/task-store.js +1 -1
  67. package/dist/agent/observational-memory/store.d.ts +1 -0
  68. package/dist/agent/observational-memory/store.js +1 -1
  69. package/dist/agent/run-store.d.ts +1 -0
  70. package/dist/agent/run-store.js +1 -1
  71. package/dist/agent/tool-approval-store.d.ts +1 -0
  72. package/dist/agent/tool-approval-store.js +1 -1
  73. package/dist/application-state/store.d.ts +1 -0
  74. package/dist/application-state/store.js +1 -1
  75. package/dist/browser-sessions/store.d.ts +1 -0
  76. package/dist/browser-sessions/store.js +1 -1
  77. package/dist/checkpoints/store.d.ts +1 -0
  78. package/dist/checkpoints/store.js +1 -1
  79. package/dist/cli/code-agent-schedules.d.ts +11 -0
  80. package/dist/cli/code-agent-schedules.js +44 -5
  81. package/dist/client/AgentPanel.d.ts +5 -1
  82. package/dist/client/AgentPanel.js +2 -2
  83. package/dist/client/AssistantChat.d.ts +5 -1
  84. package/dist/client/AssistantChat.js +40 -6
  85. package/dist/client/EnvironmentBadge.d.ts +7 -13
  86. package/dist/client/EnvironmentBadge.js +43 -92
  87. package/dist/client/agent-chat/index.d.ts +1 -0
  88. package/dist/client/agent-chat/index.js +1 -0
  89. package/dist/client/agent-chat-adapter.js +76 -50
  90. package/dist/client/app-providers.js +4 -4
  91. package/dist/client/chat/message-components.d.ts +4 -0
  92. package/dist/client/chat/message-components.js +28 -1
  93. package/dist/client/chat/run-recovery.js +26 -0
  94. package/dist/client/chat-first/apps-rail.d.ts +1 -0
  95. package/dist/client/chat-first/apps-rail.js +28 -2
  96. package/dist/client/sse-event-processor.js +13 -8
  97. package/dist/client/use-active-agent-chat-run.d.ts +2 -0
  98. package/dist/client/use-active-agent-chat-run.js +22 -0
  99. package/dist/coding-tools/sandbox/executions-store.d.ts +1 -0
  100. package/dist/coding-tools/sandbox/executions-store.js +1 -1
  101. package/dist/collab/awareness-store.d.ts +1 -0
  102. package/dist/collab/awareness-store.js +1 -1
  103. package/dist/collab/awareness.d.ts +2 -2
  104. package/dist/collab/routes.d.ts +1 -1
  105. package/dist/collab/storage.d.ts +1 -0
  106. package/dist/collab/storage.js +1 -1
  107. package/dist/db/client.js +3 -3
  108. package/dist/db/ddl-guard.js +8 -0
  109. package/dist/db/migration-runtime.d.ts +23 -0
  110. package/dist/db/migration-runtime.js +41 -0
  111. package/dist/db/migrations.d.ts +1 -2
  112. package/dist/db/migrations.js +2 -24
  113. package/dist/email-catalog/log.d.ts +1 -0
  114. package/dist/email-catalog/log.js +1 -1
  115. package/dist/guards/index.d.ts +1 -0
  116. package/dist/guards/index.js +1 -0
  117. package/dist/guards/release-schema-complete.d.ts +22 -0
  118. package/dist/guards/release-schema-complete.js +144 -0
  119. package/dist/integrations/a2a-continuations-store.d.ts +1 -0
  120. package/dist/integrations/a2a-continuations-store.js +1 -1
  121. package/dist/integrations/awaiting-input-store.d.ts +1 -0
  122. package/dist/integrations/awaiting-input-store.js +1 -1
  123. package/dist/integrations/config-store.d.ts +1 -0
  124. package/dist/integrations/config-store.js +1 -1
  125. package/dist/integrations/controls-store.d.ts +1 -0
  126. package/dist/integrations/controls-store.js +1 -1
  127. package/dist/integrations/identity-links-store.d.ts +1 -0
  128. package/dist/integrations/identity-links-store.js +1 -1
  129. package/dist/integrations/installations-store.d.ts +1 -0
  130. package/dist/integrations/installations-store.js +1 -1
  131. package/dist/integrations/remote-commands-store.d.ts +1 -0
  132. package/dist/integrations/remote-commands-store.js +1 -1
  133. package/dist/integrations/remote-devices-store.d.ts +1 -0
  134. package/dist/integrations/remote-devices-store.js +1 -1
  135. package/dist/integrations/remote-push-store.d.ts +1 -0
  136. package/dist/integrations/remote-push-store.js +1 -1
  137. package/dist/integrations/remote-run-events-store.d.ts +1 -0
  138. package/dist/integrations/remote-run-events-store.js +1 -1
  139. package/dist/integrations/scope-store.d.ts +1 -0
  140. package/dist/integrations/scope-store.js +1 -1
  141. package/dist/integrations/thread-mapping-store.d.ts +1 -0
  142. package/dist/integrations/thread-mapping-store.js +1 -1
  143. package/dist/integrations/usage-budget-store.d.ts +1 -0
  144. package/dist/integrations/usage-budget-store.js +1 -1
  145. package/dist/jobs/background-automation-runner.js +111 -9
  146. package/dist/jobs/run-history.d.ts +1 -0
  147. package/dist/jobs/run-history.js +1 -1
  148. package/dist/jobs/scheduler-health.d.ts +1 -0
  149. package/dist/jobs/scheduler-health.js +1 -1
  150. package/dist/mcp/approval-store.d.ts +1 -0
  151. package/dist/mcp/approval-store.js +1 -1
  152. package/dist/mcp/connect-store.d.ts +1 -0
  153. package/dist/mcp/connect-store.js +1 -1
  154. package/dist/mcp/oauth-store.d.ts +1 -0
  155. package/dist/mcp/oauth-store.js +1 -1
  156. package/dist/notifications/routes.d.ts +3 -3
  157. package/dist/notifications/store.d.ts +1 -0
  158. package/dist/notifications/store.js +1 -1
  159. package/dist/oauth-tokens/store.d.ts +1 -0
  160. package/dist/oauth-tokens/store.js +1 -1
  161. package/dist/observability/routes.d.ts +3 -3
  162. package/dist/progress/store.d.ts +1 -0
  163. package/dist/progress/store.js +1 -1
  164. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  165. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  166. package/dist/provider-api/corpus-jobs-store.d.ts +1 -0
  167. package/dist/provider-api/corpus-jobs-store.js +1 -1
  168. package/dist/provider-api/custom-registry.d.ts +1 -0
  169. package/dist/provider-api/custom-registry.js +1 -1
  170. package/dist/provider-api/quota-governor.d.ts +1 -0
  171. package/dist/provider-api/quota-governor.js +1 -1
  172. package/dist/provider-api/staged-datasets-store.d.ts +1 -0
  173. package/dist/provider-api/staged-datasets-store.js +1 -1
  174. package/dist/resources/handlers.d.ts +3 -3
  175. package/dist/resources/store.d.ts +1 -0
  176. package/dist/resources/store.js +1 -1
  177. package/dist/scripts/dev/index.js +15 -1
  178. package/dist/search/index.js +3 -0
  179. package/dist/secrets/storage.d.ts +1 -0
  180. package/dist/secrets/storage.js +1 -1
  181. package/dist/server/agent-chat-plugin.js +38 -9
  182. package/dist/server/agent-teams-run-queue.d.ts +1 -0
  183. package/dist/server/agent-teams-run-queue.js +1 -1
  184. package/dist/server/auth-marketing.js +3 -1
  185. package/dist/server/auth.d.ts +18 -1
  186. package/dist/server/auth.js +370 -154
  187. package/dist/server/embed-session.d.ts +1 -0
  188. package/dist/server/embed-session.js +1 -1
  189. package/dist/server/google-auth-plugin.js +3 -1
  190. package/dist/server/google-oauth.d.ts +10 -0
  191. package/dist/server/google-oauth.js +25 -1
  192. package/dist/server/identity-sso-store.d.ts +1 -0
  193. package/dist/server/identity-sso-store.js +1 -1
  194. package/dist/server/index.d.ts +1 -1
  195. package/dist/server/index.js +1 -1
  196. package/dist/server/onboarding-html.js +200 -31
  197. package/dist/server/poll.d.ts +2 -0
  198. package/dist/server/poll.js +19 -1
  199. package/dist/server/release-migrations.js +5 -0
  200. package/dist/server/release-schema.d.ts +46 -0
  201. package/dist/server/release-schema.js +307 -0
  202. package/dist/settings/store.d.ts +1 -0
  203. package/dist/settings/store.js +1 -1
  204. package/dist/shared/environment-lanes.d.ts +28 -0
  205. package/dist/shared/environment-lanes.js +61 -0
  206. package/dist/sharing/actions/share-resource.js +22 -2
  207. package/dist/styles/agent-native.css +17 -15
  208. package/dist/triggers/routes.d.ts +11 -0
  209. package/dist/triggers/routes.js +10 -0
  210. package/dist/usage/alerts-store.d.ts +1 -0
  211. package/dist/usage/alerts-store.js +1 -1
  212. package/dist/usage/store.d.ts +1 -0
  213. package/dist/usage/store.js +1 -1
  214. 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
  });
@@ -679,6 +679,7 @@ export default function ShareRoute() {
679
679
  // an anonymous viewer sees the same controls but triggers the sign-in
680
680
  // prompt instead (see `requireSignIn` below).
681
681
  const viewerCanComment = Boolean(session) && viewerRole != null;
682
+ const viewerCanUseFullscreenInteractions = !session || viewerCanComment;
682
683
  const viewerIsOwner = Boolean(dataQ.data?.data?.viewer?.isOwner);
683
684
  const canReshareLink =
684
685
  (viewerRole === "viewer" || viewerRole === "commenter") &&
@@ -1252,66 +1253,85 @@ export default function ShareRoute() {
1252
1253
  cta={firstCta}
1253
1254
  onCtaClick={() => tracking.reportCtaClick()}
1254
1255
  onTimeUpdate={(ms) => setCurrentMs(ms)}
1255
- onCommentClick={() => setPanel("comments")}
1256
+ onCommentClick={
1257
+ viewerCanUseFullscreenInteractions
1258
+ ? () => setPanel("comments")
1259
+ : undefined
1260
+ }
1256
1261
  onFullscreenChange={setIsPlayerFullscreen}
1257
- enableComments={recording.enableComments}
1258
- onAddComment={() => {
1259
- if (!session) {
1260
- requireSignIn("comment");
1261
- return;
1262
- }
1263
- const liveCt = isLoomEmbedBacked
1264
- ? null
1265
- : playerRef.current?.video?.currentTime;
1266
- const liveMs =
1267
- typeof liveCt === "number" &&
1268
- Number.isFinite(liveCt) &&
1269
- liveCt >= 0 &&
1270
- liveCt < 1e7
1271
- ? Math.floor(liveCt * 1000)
1272
- : currentMs;
1273
- setCommentAtMs(liveMs);
1274
- setCommentOpen(true);
1275
- }}
1276
- enableReactions={recording.enableReactions}
1277
- onReact={(emoji) => {
1278
- if (!session) {
1279
- requireSignIn("react");
1280
- return false;
1281
- }
1282
- tracking.reportReaction(emoji);
1283
- const liveCt = isLoomEmbedBacked
1284
- ? null
1285
- : playerRef.current?.video?.currentTime;
1286
- const liveMs =
1287
- typeof liveCt === "number" &&
1288
- Number.isFinite(liveCt) &&
1289
- liveCt >= 0 &&
1290
- liveCt < 1e7
1291
- ? Math.floor(liveCt * 1000)
1292
- : currentMs;
1293
- return fetch(
1294
- agentNativePath("/_agent-native/actions/react-to-recording"),
1295
- {
1296
- method: "POST",
1297
- headers: { "Content-Type": "application/json" },
1298
- body: JSON.stringify({
1299
- recordingId: recording.id,
1300
- emoji,
1301
- videoTimestampMs: liveMs,
1302
- }),
1303
- },
1304
- )
1305
- .then((res) => {
1306
- if (!res.ok) throw new Error(`react failed: ${res.status}`);
1307
- return dataQ.refetch();
1308
- })
1309
- .then(() => true)
1310
- .catch((err) => {
1311
- console.warn("[clips] react failed", err);
1312
- return false;
1313
- });
1314
- }}
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
+ }
1315
1335
  className="h-full w-full rounded-none sm:rounded-xl"
1316
1336
  />
1317
1337
  {commentOpen && viewerCanComment
@@ -1354,23 +1374,15 @@ export default function ShareRoute() {
1354
1374
  {recording.enableComments ? (
1355
1375
  <TimestampedCommentButton
1356
1376
  enableComments={recording.enableComments}
1357
- canComment
1377
+ canComment={!session || viewerCanComment}
1358
1378
  className="shrink-0"
1359
1379
  onOpen={() => {
1360
1380
  if (!session) {
1361
1381
  requireSignIn("comment");
1362
1382
  return;
1363
1383
  }
1364
- const liveCt = isLoomEmbedBacked
1365
- ? null
1366
- : playerRef.current?.video?.currentTime;
1367
1384
  const liveMs =
1368
- typeof liveCt === "number" &&
1369
- Number.isFinite(liveCt) &&
1370
- liveCt >= 0 &&
1371
- liveCt < 1e7
1372
- ? Math.floor(liveCt * 1000)
1373
- : currentMs;
1385
+ playerRef.current?.getCurrentOriginalMs() ?? currentMs;
1374
1386
  setCommentAtMs(liveMs);
1375
1387
  setCommentOpen(true);
1376
1388
  }}
@@ -1379,22 +1391,15 @@ export default function ShareRoute() {
1379
1391
  {recording.enableReactions ? (
1380
1392
  <ReactionsTray
1381
1393
  reactions={reactions}
1394
+ disabled={Boolean(session) && !viewerCanComment}
1382
1395
  onReact={(emoji) => {
1383
1396
  if (!session) {
1384
1397
  requireSignIn("react");
1385
1398
  return false;
1386
1399
  }
1387
1400
  tracking.reportReaction(emoji);
1388
- const liveCt = isLoomEmbedBacked
1389
- ? null
1390
- : playerRef.current?.video?.currentTime;
1391
1401
  const liveMs =
1392
- typeof liveCt === "number" &&
1393
- Number.isFinite(liveCt) &&
1394
- liveCt >= 0 &&
1395
- liveCt < 1e7
1396
- ? Math.floor(liveCt * 1000)
1397
- : currentMs;
1402
+ playerRef.current?.getCurrentOriginalMs() ?? currentMs;
1398
1403
  return fetch(
1399
1404
  agentNativePath(
1400
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.
@@ -2,4 +2,5 @@
2
2
  type: fixed
3
3
  date: 2026-08-19
4
4
  ---
5
+
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",
@@ -0,0 +1 @@
1
+ export { default } from "../../../_agent-native/google/auth-url.get.js";