@agent-native/core 0.164.4 → 0.164.12

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 (268) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +84 -0
  3. package/corpus/templates/calendar/AGENTS.md +5 -0
  4. package/corpus/templates/calendar/actions/delete-event.ts +1 -1
  5. package/corpus/templates/calendar/actions/delete-events.ts +508 -0
  6. package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
  7. package/corpus/templates/calendar/actions/list-events.ts +53 -0
  8. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
  9. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
  10. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
  11. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
  12. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
  13. package/corpus/templates/calendar/app/i18n-data.ts +120 -0
  14. package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
  15. package/corpus/templates/calendar/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
  16. package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
  17. package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
  18. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  19. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  20. package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  21. package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  22. package/corpus/templates/clips/app/components/player/video-player.tsx +12 -0
  23. package/corpus/templates/clips/app/routes/_app.dictate.tsx +18 -4
  24. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +114 -20
  25. package/corpus/templates/clips/app/routes/share.$shareId.tsx +83 -78
  26. package/corpus/templates/clips/changelog/2026-08-19-clips-nightly-builds-are-available-for-trying-the-latest-upd.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +1 -0
  28. package/corpus/templates/clips/desktop/README.md +19 -4
  29. package/corpus/templates/clips/desktop/src/app.tsx +78 -16
  30. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -2
  31. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -0
  32. package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  33. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
  34. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  35. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
  36. package/corpus/templates/clips/server/routes/api/clips-updater.json.get.ts +41 -22
  37. package/corpus/templates/factory/AGENTS.md +3 -2
  38. package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
  39. package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
  40. package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
  41. package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
  42. package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
  43. package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
  44. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
  45. package/corpus/templates/factory/app/components/factory/FactoryHistoryView.tsx +709 -0
  46. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
  47. package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
  48. package/corpus/templates/factory/app/i18n/en-US.ts +47 -0
  49. package/corpus/templates/factory/app/routes/factory.tsx +324 -61
  50. package/corpus/templates/factory/server/connectors/slack.ts +44 -2
  51. package/corpus/templates/factory/server/plugins/agent-chat.ts +12 -8
  52. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
  53. package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
  54. package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
  55. package/corpus/templates/mail/actions/view-screen.ts +1 -1
  56. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
  57. package/corpus/templates/mail/server/handlers/google-auth.ts +125 -24
  58. package/corpus/templates/mail/server/lib/gmail-query.ts +5 -2
  59. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  60. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  61. package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  62. package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  63. package/corpus/templates/plan/app/pages/PlansPage.tsx +0 -12
  64. package/corpus/templates/plan/changelog/2026-08-19-plan-prompts-now-guide-you-to-connect-ai-before-sending-and-.md +6 -0
  65. package/corpus/templates/slides/actions/approve-deck-access-request.ts +234 -0
  66. package/corpus/templates/slides/actions/get-deck-access-status.ts +35 -2
  67. package/corpus/templates/slides/actions/request-deck-access.ts +263 -64
  68. package/corpus/templates/slides/app/components/editor/MissingDeckAccessPane.tsx +196 -0
  69. package/corpus/templates/slides/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
  70. package/corpus/templates/slides/app/hooks/use-deck-access.ts +7 -4
  71. package/corpus/templates/slides/app/i18n/en-US.ts +29 -6
  72. package/corpus/templates/slides/app/pages/DeckEditor.tsx +147 -169
  73. package/corpus/templates/slides/app/routes/access-request.approve.tsx +250 -0
  74. package/corpus/templates/slides/server/db/schema.ts +8 -0
  75. package/corpus/templates/slides/server/lib/access-request-email.ts +36 -0
  76. package/corpus/templates/slides/server/lib/emails.ts +26 -0
  77. package/corpus/templates/slides/server/plugins/agent-chat.ts +7 -0
  78. package/corpus/templates/slides/server/plugins/db.ts +50 -0
  79. package/corpus/templates/slides/shared/deck-access.ts +31 -0
  80. package/dist/a2a/task-store.d.ts +1 -0
  81. package/dist/a2a/task-store.js +1 -1
  82. package/dist/agent/observational-memory/store.d.ts +1 -0
  83. package/dist/agent/observational-memory/store.js +1 -1
  84. package/dist/agent/run-store.d.ts +1 -0
  85. package/dist/agent/run-store.js +1 -1
  86. package/dist/agent/tool-approval-store.d.ts +1 -0
  87. package/dist/agent/tool-approval-store.js +1 -1
  88. package/dist/application-state/store.d.ts +1 -0
  89. package/dist/application-state/store.js +1 -1
  90. package/dist/browser-sessions/store.d.ts +1 -0
  91. package/dist/browser-sessions/store.js +1 -1
  92. package/dist/checkpoints/store.d.ts +1 -0
  93. package/dist/checkpoints/store.js +1 -1
  94. package/dist/cli/code-agent-schedules.d.ts +11 -0
  95. package/dist/cli/code-agent-schedules.js +44 -5
  96. package/dist/client/AgentPanel.d.ts +5 -1
  97. package/dist/client/AgentPanel.js +2 -2
  98. package/dist/client/AssistantChat.d.ts +5 -1
  99. package/dist/client/AssistantChat.js +40 -6
  100. package/dist/client/EnvironmentBadge.d.ts +7 -13
  101. package/dist/client/EnvironmentBadge.js +43 -92
  102. package/dist/client/agent-chat/index.d.ts +1 -0
  103. package/dist/client/agent-chat/index.js +1 -0
  104. package/dist/client/agent-chat-adapter.js +76 -50
  105. package/dist/client/app-providers.js +4 -4
  106. package/dist/client/chat/message-components.d.ts +4 -0
  107. package/dist/client/chat/message-components.js +28 -1
  108. package/dist/client/chat/run-recovery.js +26 -0
  109. package/dist/client/chat-first/apps-rail.d.ts +1 -0
  110. package/dist/client/chat-first/apps-rail.js +28 -2
  111. package/dist/client/sse-event-processor.js +13 -8
  112. package/dist/client/use-active-agent-chat-run.d.ts +2 -0
  113. package/dist/client/use-active-agent-chat-run.js +22 -0
  114. package/dist/coding-tools/sandbox/executions-store.d.ts +1 -0
  115. package/dist/coding-tools/sandbox/executions-store.js +1 -1
  116. package/dist/collab/awareness-store.d.ts +1 -0
  117. package/dist/collab/awareness-store.js +1 -1
  118. package/dist/collab/routes.d.ts +1 -1
  119. package/dist/collab/storage.d.ts +1 -0
  120. package/dist/collab/storage.js +1 -1
  121. package/dist/collab/struct-routes.d.ts +1 -1
  122. package/dist/db/client.js +3 -3
  123. package/dist/db/ddl-guard.js +8 -0
  124. package/dist/db/migration-runtime.d.ts +23 -0
  125. package/dist/db/migration-runtime.js +41 -0
  126. package/dist/db/migrations.d.ts +1 -2
  127. package/dist/db/migrations.js +2 -24
  128. package/dist/email-catalog/log.d.ts +1 -0
  129. package/dist/email-catalog/log.js +1 -1
  130. package/dist/guards/index.d.ts +1 -0
  131. package/dist/guards/index.js +1 -0
  132. package/dist/guards/release-schema-complete.d.ts +22 -0
  133. package/dist/guards/release-schema-complete.js +144 -0
  134. package/dist/integrations/a2a-continuations-store.d.ts +1 -0
  135. package/dist/integrations/a2a-continuations-store.js +1 -1
  136. package/dist/integrations/awaiting-input-store.d.ts +1 -0
  137. package/dist/integrations/awaiting-input-store.js +1 -1
  138. package/dist/integrations/config-store.d.ts +1 -0
  139. package/dist/integrations/config-store.js +1 -1
  140. package/dist/integrations/controls-store.d.ts +1 -0
  141. package/dist/integrations/controls-store.js +1 -1
  142. package/dist/integrations/identity-links-store.d.ts +1 -0
  143. package/dist/integrations/identity-links-store.js +1 -1
  144. package/dist/integrations/installations-store.d.ts +1 -0
  145. package/dist/integrations/installations-store.js +1 -1
  146. package/dist/integrations/remote-commands-store.d.ts +1 -0
  147. package/dist/integrations/remote-commands-store.js +1 -1
  148. package/dist/integrations/remote-devices-store.d.ts +1 -0
  149. package/dist/integrations/remote-devices-store.js +1 -1
  150. package/dist/integrations/remote-push-store.d.ts +1 -0
  151. package/dist/integrations/remote-push-store.js +1 -1
  152. package/dist/integrations/remote-run-events-store.d.ts +1 -0
  153. package/dist/integrations/remote-run-events-store.js +1 -1
  154. package/dist/integrations/scope-store.d.ts +1 -0
  155. package/dist/integrations/scope-store.js +1 -1
  156. package/dist/integrations/thread-mapping-store.d.ts +1 -0
  157. package/dist/integrations/thread-mapping-store.js +1 -1
  158. package/dist/integrations/usage-budget-store.d.ts +1 -0
  159. package/dist/integrations/usage-budget-store.js +1 -1
  160. package/dist/jobs/background-automation-runner.js +111 -9
  161. package/dist/jobs/run-history.d.ts +1 -0
  162. package/dist/jobs/run-history.js +1 -1
  163. package/dist/jobs/scheduler-health.d.ts +1 -0
  164. package/dist/jobs/scheduler-health.js +1 -1
  165. package/dist/mcp/approval-store.d.ts +1 -0
  166. package/dist/mcp/approval-store.js +1 -1
  167. package/dist/mcp/connect-store.d.ts +1 -0
  168. package/dist/mcp/connect-store.js +1 -1
  169. package/dist/mcp/oauth-store.d.ts +1 -0
  170. package/dist/mcp/oauth-store.js +1 -1
  171. package/dist/notifications/store.d.ts +1 -0
  172. package/dist/notifications/store.js +1 -1
  173. package/dist/oauth-tokens/store.d.ts +1 -0
  174. package/dist/oauth-tokens/store.js +1 -1
  175. package/dist/observability/routes.d.ts +5 -5
  176. package/dist/progress/routes.d.ts +1 -1
  177. package/dist/progress/store.d.ts +1 -0
  178. package/dist/progress/store.js +1 -1
  179. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  180. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  181. package/dist/provider-api/corpus-jobs-store.d.ts +1 -0
  182. package/dist/provider-api/corpus-jobs-store.js +1 -1
  183. package/dist/provider-api/custom-registry.d.ts +1 -0
  184. package/dist/provider-api/custom-registry.js +1 -1
  185. package/dist/provider-api/quota-governor.d.ts +1 -0
  186. package/dist/provider-api/quota-governor.js +1 -1
  187. package/dist/provider-api/staged-datasets-store.d.ts +1 -0
  188. package/dist/provider-api/staged-datasets-store.js +1 -1
  189. package/dist/resources/handlers.d.ts +3 -3
  190. package/dist/resources/store.d.ts +1 -0
  191. package/dist/resources/store.js +1 -1
  192. package/dist/scripts/dev/index.js +15 -1
  193. package/dist/search/index.js +3 -0
  194. package/dist/secrets/routes.d.ts +9 -9
  195. package/dist/secrets/storage.d.ts +1 -0
  196. package/dist/secrets/storage.js +1 -1
  197. package/dist/server/agent-chat-plugin.js +38 -9
  198. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  199. package/dist/server/agent-teams-run-queue.d.ts +1 -0
  200. package/dist/server/agent-teams-run-queue.js +1 -1
  201. package/dist/server/auth-marketing.js +3 -1
  202. package/dist/server/auth.d.ts +18 -1
  203. package/dist/server/auth.js +370 -154
  204. package/dist/server/embed-session.d.ts +1 -0
  205. package/dist/server/embed-session.js +1 -1
  206. package/dist/server/google-auth-plugin.js +3 -1
  207. package/dist/server/google-oauth.d.ts +10 -0
  208. package/dist/server/google-oauth.js +25 -1
  209. package/dist/server/identity-sso-store.d.ts +1 -0
  210. package/dist/server/identity-sso-store.js +1 -1
  211. package/dist/server/index.d.ts +1 -1
  212. package/dist/server/index.js +1 -1
  213. package/dist/server/onboarding-html.js +200 -31
  214. package/dist/server/poll.d.ts +2 -0
  215. package/dist/server/poll.js +19 -1
  216. package/dist/server/realtime-token.d.ts +1 -1
  217. package/dist/server/release-migrations.js +5 -0
  218. package/dist/server/release-schema.d.ts +46 -0
  219. package/dist/server/release-schema.js +307 -0
  220. package/dist/settings/store.d.ts +1 -0
  221. package/dist/settings/store.js +1 -1
  222. package/dist/shared/environment-lanes.d.ts +28 -0
  223. package/dist/shared/environment-lanes.js +61 -0
  224. package/dist/sharing/actions/share-resource.js +22 -2
  225. package/dist/styles/agent-native.css +17 -15
  226. package/dist/triggers/routes.d.ts +11 -0
  227. package/dist/triggers/routes.js +10 -0
  228. package/dist/usage/alerts-store.d.ts +1 -0
  229. package/dist/usage/alerts-store.js +1 -1
  230. package/dist/usage/store.d.ts +1 -0
  231. package/dist/usage/store.js +1 -1
  232. package/docs/content/actions-access-control.mdx +136 -2
  233. package/docs/content/actions-agent-tools.mdx +101 -0
  234. package/docs/content/actions-defining.mdx +2 -2
  235. package/docs/content/audit-log.mdx +1 -1
  236. package/docs/content/authentication.mdx +1 -1
  237. package/docs/content/cloudflare.mdx +43 -0
  238. package/docs/content/creating-templates.mdx +1 -1
  239. package/docs/content/deploy-an-app.mdx +109 -0
  240. package/docs/content/deployment-environment-variables.mdx +101 -0
  241. package/docs/content/deployment.mdx +125 -792
  242. package/docs/content/docs-components.mdx +2 -2
  243. package/docs/content/environment-variables.mdx +41 -41
  244. package/docs/content/file-uploads.mdx +74 -40
  245. package/docs/content/harness-agents.mdx +2 -2
  246. package/docs/content/internationalization.mdx +61 -65
  247. package/docs/content/key-concepts.mdx +3 -3
  248. package/docs/content/multi-app-workspace.mdx +2 -2
  249. package/docs/content/netlify.mdx +47 -0
  250. package/docs/content/node-docker.mdx +138 -0
  251. package/docs/content/organizations-teams-permissions.mdx +5 -5
  252. package/docs/content/other-platforms.mdx +27 -0
  253. package/docs/content/ssr-caching.mdx +45 -0
  254. package/docs/content/template-assets-developers.mdx +2 -2
  255. package/docs/content/template-chat-developers.mdx +2 -2
  256. package/docs/content/template-content-developers.mdx +2 -2
  257. package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
  258. package/docs/content/template-content-sync.mdx +2 -2
  259. package/docs/content/toolkit-agent-ux.mdx +1 -1
  260. package/docs/content/toolkit-resources.mdx +2 -2
  261. package/docs/content/updating-ui-in-production.mdx +35 -0
  262. package/docs/content/vercel.mdx +38 -0
  263. package/docs/content/what-is-agent-native.mdx +1 -1
  264. package/docs/content/workspace-deployment.mdx +350 -0
  265. package/package.json +2 -2
  266. package/docs/content/agents.mdx +0 -9
  267. package/docs/content/database.mdx +0 -317
  268. package/docs/content/human-approval.mdx +0 -156
@@ -0,0 +1,112 @@
1
+ const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
2
+
3
+ export const WEEKDAY_NAMES = [
4
+ "sunday",
5
+ "monday",
6
+ "tuesday",
7
+ "wednesday",
8
+ "thursday",
9
+ "friday",
10
+ "saturday",
11
+ ] as const;
12
+
13
+ export type WeekdayName = (typeof WEEKDAY_NAMES)[number];
14
+
15
+ const WEEKDAY_ALIASES = new Map<string, WeekdayName>([
16
+ ...WEEKDAY_NAMES.map((day) => [day, day] as const),
17
+ ...WEEKDAY_NAMES.map((day) => [day.slice(0, 3), day] as const),
18
+ ["tues", "tuesday"],
19
+ ["thur", "thursday"],
20
+ ["thurs", "thursday"],
21
+ ["weekend", "saturday"],
22
+ ]);
23
+
24
+ /**
25
+ * Accepts the shapes a natural-language request arrives in — "Saturday",
26
+ * "sat", "SUN", or an array/comma string mixing them — and rejects anything
27
+ * else loudly. A silently dropped day name would delete a different set of
28
+ * events than the user asked about.
29
+ */
30
+ export function normalizeWeekdays(
31
+ input: string | readonly string[] | undefined,
32
+ ): WeekdayName[] {
33
+ if (input === undefined) return [];
34
+ const raw = (Array.isArray(input) ? input : String(input).split(","))
35
+ .map((value) => value.trim().toLowerCase())
36
+ .filter(Boolean);
37
+ const resolved = new Set<WeekdayName>();
38
+ for (const value of raw) {
39
+ if (value === "weekend" || value === "weekends") {
40
+ resolved.add("saturday");
41
+ resolved.add("sunday");
42
+ continue;
43
+ }
44
+ if (value === "weekday" || value === "weekdays") {
45
+ for (const day of WEEKDAY_NAMES) {
46
+ if (day !== "saturday" && day !== "sunday") resolved.add(day);
47
+ }
48
+ continue;
49
+ }
50
+ const match = WEEKDAY_ALIASES.get(value.replace(/s$/, ""));
51
+ if (!match) {
52
+ throw new Error(
53
+ `Unrecognized day of week: ${value}. Use full or 3-letter English day names.`,
54
+ );
55
+ }
56
+ resolved.add(match);
57
+ }
58
+ return WEEKDAY_NAMES.filter((day) => resolved.has(day));
59
+ }
60
+
61
+ /**
62
+ * The weekday the user sees for an event, which is the weekday in the calendar
63
+ * view's timezone — not UTC. A Sunday 5pm America/Los_Angeles meeting is Monday
64
+ * in UTC, so filtering on the raw ISO string silently misses it and instead
65
+ * deletes a Monday meeting the user never mentioned.
66
+ *
67
+ * All-day starts carry no instant, so their `YYYY-MM-DD` date IS the local day
68
+ * and must not be re-projected through a timezone.
69
+ */
70
+ export function eventWeekday(start: string, timezone: string): WeekdayName {
71
+ if (DATE_ONLY_RE.test(start)) {
72
+ const [year, month, day] = start.split("-").map(Number);
73
+ return WEEKDAY_NAMES[new Date(Date.UTC(year, month - 1, day)).getUTCDay()];
74
+ }
75
+ const parsed = new Date(start);
76
+ if (Number.isNaN(parsed.getTime())) {
77
+ throw new Error(`Invalid event start: ${start}`);
78
+ }
79
+ const label = new Intl.DateTimeFormat("en-US", {
80
+ timeZone: timezone,
81
+ weekday: "long",
82
+ })
83
+ .format(parsed)
84
+ .toLowerCase();
85
+ const match = WEEKDAY_NAMES.find((day) => day === label);
86
+ if (!match) throw new Error(`Unable to resolve weekday for ${start}`);
87
+ return match;
88
+ }
89
+
90
+ export function matchesWeekdays(
91
+ start: string,
92
+ timezone: string,
93
+ weekdays: readonly WeekdayName[],
94
+ ): boolean {
95
+ if (weekdays.length === 0) return true;
96
+ return weekdays.includes(eventWeekday(start, timezone));
97
+ }
98
+
99
+ /**
100
+ * Validate a caller-supplied IANA timezone instead of falling back to UTC.
101
+ * `normalizeTimezone` silently returns UTC for an unparseable zone, which on a
102
+ * destructive weekday filter would quietly reclassify every event and delete a
103
+ * different set of days than the caller named.
104
+ */
105
+ export function requireValidTimezone(timezone: string): string {
106
+ try {
107
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
108
+ } catch {
109
+ throw new Error(`Invalid IANA timezone: ${timezone}`);
110
+ }
111
+ return timezone;
112
+ }
@@ -0,0 +1 @@
1
+ export { default } from "../../../../_agent-native/google/add-account/auth-url.post.js";
@@ -0,0 +1 @@
1
+ export { default } from "../../../_agent-native/google/auth-url.post.js";
@@ -0,0 +1 @@
1
+ export { getGoogleAddAccountUrl as default } from "../../../../handlers/google-auth";
@@ -0,0 +1 @@
1
+ export { getGoogleAuthUrl as default } from "../../../handlers/google-auth";
@@ -168,6 +168,7 @@ export interface VideoPlayerHandle {
168
168
  play: () => Promise<void> | void;
169
169
  pause: () => void;
170
170
  seek: (ms: number) => void;
171
+ getCurrentOriginalMs: () => number;
171
172
  setSpeed: (rate: number) => void;
172
173
  toggleMute: () => void;
173
174
  toggleCaptions: () => void;
@@ -1159,6 +1160,17 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1159
1160
  play: requestPlay,
1160
1161
  pause: pauseVideo,
1161
1162
  seek: seekToVisibleMs,
1163
+ getCurrentOriginalMs: () => {
1164
+ const v = videoRef.current;
1165
+ const liveOriginalMs =
1166
+ v &&
1167
+ Number.isFinite(v.currentTime) &&
1168
+ v.currentTime >= 0 &&
1169
+ v.currentTime < 1e7
1170
+ ? Math.floor(v.currentTime * 1000)
1171
+ : currentMsRef.current;
1172
+ return liveOriginalMs;
1173
+ },
1162
1174
  setSpeed: applySpeed,
1163
1175
  toggleMute: () => {
1164
1176
  if (videoRef.current) {
@@ -159,6 +159,10 @@ function dayBucket(iso: string): string {
159
159
  }
160
160
  }
161
161
 
162
+ export function dictationsRefetchInterval(isActive: boolean): number | false {
163
+ return isActive ? 2_000 : false;
164
+ }
165
+
162
166
  function sourceMeta(
163
167
  source: string | undefined,
164
168
  t: ReturnType<typeof useT>,
@@ -694,10 +698,6 @@ function DownloadDesktopAppCard() {
694
698
 
695
699
  export default function DictateRoute() {
696
700
  const t = useT();
697
- const { data, isLoading, isError } = useActionQuery<
698
- { dictations: Dictation[] } | Dictation[] | undefined
699
- >("list-dictations", {}, { retry: false });
700
-
701
701
  const { isDesktopApp } = useDesktopPromo();
702
702
  const [filter, setFilter] = useState<SourceFilter>("all");
703
703
  const [listening, setListening] = useState(false);
@@ -706,6 +706,20 @@ export default function DictateRoute() {
706
706
  const [speechSupported, setSpeechSupported] = useState(false);
707
707
  const qc = useQueryClient();
708
708
  const createDictation = useActionMutation("create-dictation");
709
+ const { data, isLoading, isError } = useActionQuery<
710
+ { dictations: Dictation[] } | Dictation[] | undefined
711
+ >(
712
+ "list-dictations",
713
+ {},
714
+ {
715
+ retry: false,
716
+ // Action-backed mutations invalidate this query when a desktop-created
717
+ // dictation lands. Poll only while browser work is active or saving,
718
+ // rather than running a permanent interval over idle history.
719
+ refetchInterval: () =>
720
+ dictationsRefetchInterval(listening || createDictation.isPending),
721
+ },
722
+ );
709
723
 
710
724
  const recognitionRef = useRef<SpeechRecognitionLike | null>(null);
711
725
  const transcriptRef = useRef("");
@@ -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