@agent-native/core 0.0.0-beta-20260819181349 → 0.0.0-beta-20260819184657

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 (143) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/components/player/video-player.tsx +12 -0
  3. package/corpus/templates/clips/app/routes/_app.dictate.tsx +18 -4
  4. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +114 -20
  5. package/corpus/templates/clips/app/routes/share.$shareId.tsx +83 -78
  6. package/corpus/templates/mail/actions/view-screen.ts +1 -1
  7. package/corpus/templates/mail/server/handlers/google-auth.ts +20 -22
  8. package/corpus/templates/mail/server/lib/gmail-query.ts +5 -2
  9. package/corpus/templates/plan/app/pages/PlansPage.tsx +0 -12
  10. package/corpus/templates/plan/changelog/2026-08-19-plan-prompts-now-guide-you-to-connect-ai-before-sending-and-.md +6 -0
  11. package/dist/a2a/task-store.d.ts +1 -0
  12. package/dist/a2a/task-store.js +1 -1
  13. package/dist/agent/observational-memory/store.d.ts +1 -0
  14. package/dist/agent/observational-memory/store.js +1 -1
  15. package/dist/agent/run-store.d.ts +1 -0
  16. package/dist/agent/run-store.js +1 -1
  17. package/dist/agent/tool-approval-store.d.ts +1 -0
  18. package/dist/agent/tool-approval-store.js +1 -1
  19. package/dist/application-state/store.d.ts +1 -0
  20. package/dist/application-state/store.js +1 -1
  21. package/dist/browser-sessions/store.d.ts +1 -0
  22. package/dist/browser-sessions/store.js +1 -1
  23. package/dist/checkpoints/store.d.ts +1 -0
  24. package/dist/checkpoints/store.js +1 -1
  25. package/dist/client/EnvironmentBadge.d.ts +7 -13
  26. package/dist/client/EnvironmentBadge.js +43 -92
  27. package/dist/client/agent-chat-adapter.js +22 -10
  28. package/dist/client/app-providers.js +4 -4
  29. package/dist/client/chat/message-components.d.ts +4 -0
  30. package/dist/client/chat/message-components.js +28 -1
  31. package/dist/client/chat/run-recovery.js +26 -0
  32. package/dist/client/sse-event-processor.js +13 -8
  33. package/dist/coding-tools/sandbox/executions-store.d.ts +1 -0
  34. package/dist/coding-tools/sandbox/executions-store.js +1 -1
  35. package/dist/collab/awareness-store.d.ts +1 -0
  36. package/dist/collab/awareness-store.js +1 -1
  37. package/dist/collab/awareness.d.ts +2 -2
  38. package/dist/collab/routes.d.ts +1 -1
  39. package/dist/collab/storage.d.ts +1 -0
  40. package/dist/collab/storage.js +1 -1
  41. package/dist/collab/struct-routes.d.ts +1 -1
  42. package/dist/db/client.js +3 -3
  43. package/dist/db/ddl-guard.js +8 -0
  44. package/dist/db/migration-runtime.d.ts +23 -0
  45. package/dist/db/migration-runtime.js +41 -0
  46. package/dist/db/migrations.d.ts +1 -2
  47. package/dist/db/migrations.js +2 -24
  48. package/dist/email-catalog/log.d.ts +1 -0
  49. package/dist/email-catalog/log.js +1 -1
  50. package/dist/guards/index.d.ts +1 -0
  51. package/dist/guards/index.js +1 -0
  52. package/dist/guards/release-schema-complete.d.ts +22 -0
  53. package/dist/guards/release-schema-complete.js +144 -0
  54. package/dist/integrations/a2a-continuations-store.d.ts +1 -0
  55. package/dist/integrations/a2a-continuations-store.js +1 -1
  56. package/dist/integrations/awaiting-input-store.d.ts +1 -0
  57. package/dist/integrations/awaiting-input-store.js +1 -1
  58. package/dist/integrations/config-store.d.ts +1 -0
  59. package/dist/integrations/config-store.js +1 -1
  60. package/dist/integrations/controls-store.d.ts +1 -0
  61. package/dist/integrations/controls-store.js +1 -1
  62. package/dist/integrations/identity-links-store.d.ts +1 -0
  63. package/dist/integrations/identity-links-store.js +1 -1
  64. package/dist/integrations/installations-store.d.ts +1 -0
  65. package/dist/integrations/installations-store.js +1 -1
  66. package/dist/integrations/remote-commands-store.d.ts +1 -0
  67. package/dist/integrations/remote-commands-store.js +1 -1
  68. package/dist/integrations/remote-devices-store.d.ts +1 -0
  69. package/dist/integrations/remote-devices-store.js +1 -1
  70. package/dist/integrations/remote-push-store.d.ts +1 -0
  71. package/dist/integrations/remote-push-store.js +1 -1
  72. package/dist/integrations/remote-run-events-store.d.ts +1 -0
  73. package/dist/integrations/remote-run-events-store.js +1 -1
  74. package/dist/integrations/scope-store.d.ts +1 -0
  75. package/dist/integrations/scope-store.js +1 -1
  76. package/dist/integrations/thread-mapping-store.d.ts +1 -0
  77. package/dist/integrations/thread-mapping-store.js +1 -1
  78. package/dist/integrations/usage-budget-store.d.ts +1 -0
  79. package/dist/integrations/usage-budget-store.js +1 -1
  80. package/dist/jobs/run-history.d.ts +1 -0
  81. package/dist/jobs/run-history.js +1 -1
  82. package/dist/jobs/scheduler-health.d.ts +1 -0
  83. package/dist/jobs/scheduler-health.js +1 -1
  84. package/dist/mcp/approval-store.d.ts +1 -0
  85. package/dist/mcp/approval-store.js +1 -1
  86. package/dist/mcp/connect-store.d.ts +1 -0
  87. package/dist/mcp/connect-store.js +1 -1
  88. package/dist/mcp/oauth-store.d.ts +1 -0
  89. package/dist/mcp/oauth-store.js +1 -1
  90. package/dist/notifications/routes.d.ts +3 -3
  91. package/dist/notifications/store.d.ts +1 -0
  92. package/dist/notifications/store.js +1 -1
  93. package/dist/oauth-tokens/store.d.ts +1 -0
  94. package/dist/oauth-tokens/store.js +1 -1
  95. package/dist/observability/routes.d.ts +6 -6
  96. package/dist/progress/routes.d.ts +1 -1
  97. package/dist/progress/store.d.ts +1 -0
  98. package/dist/progress/store.js +1 -1
  99. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  100. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  101. package/dist/provider-api/corpus-jobs-store.d.ts +1 -0
  102. package/dist/provider-api/corpus-jobs-store.js +1 -1
  103. package/dist/provider-api/custom-registry.d.ts +1 -0
  104. package/dist/provider-api/custom-registry.js +1 -1
  105. package/dist/provider-api/quota-governor.d.ts +1 -0
  106. package/dist/provider-api/quota-governor.js +1 -1
  107. package/dist/provider-api/staged-datasets-store.d.ts +1 -0
  108. package/dist/provider-api/staged-datasets-store.js +1 -1
  109. package/dist/resources/handlers.d.ts +1 -1
  110. package/dist/resources/store.d.ts +1 -0
  111. package/dist/resources/store.js +1 -1
  112. package/dist/search/index.js +3 -0
  113. package/dist/secrets/routes.d.ts +9 -9
  114. package/dist/secrets/storage.d.ts +1 -0
  115. package/dist/secrets/storage.js +1 -1
  116. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  117. package/dist/server/agent-teams-run-queue.d.ts +1 -0
  118. package/dist/server/agent-teams-run-queue.js +1 -1
  119. package/dist/server/auth-marketing.js +3 -1
  120. package/dist/server/auth.d.ts +1 -0
  121. package/dist/server/auth.js +1 -1
  122. package/dist/server/embed-session.d.ts +1 -0
  123. package/dist/server/embed-session.js +1 -1
  124. package/dist/server/identity-sso-store.d.ts +1 -0
  125. package/dist/server/identity-sso-store.js +1 -1
  126. package/dist/server/onboarding-html.js +121 -1
  127. package/dist/server/poll.d.ts +2 -0
  128. package/dist/server/poll.js +19 -1
  129. package/dist/server/release-migrations.js +5 -0
  130. package/dist/server/release-schema.d.ts +46 -0
  131. package/dist/server/release-schema.js +307 -0
  132. package/dist/settings/store.d.ts +1 -0
  133. package/dist/settings/store.js +1 -1
  134. package/dist/shared/environment-lanes.d.ts +28 -0
  135. package/dist/shared/environment-lanes.js +61 -0
  136. package/dist/styles/agent-native.css +17 -15
  137. package/dist/triggers/routes.d.ts +11 -0
  138. package/dist/triggers/routes.js +10 -0
  139. package/dist/usage/alerts-store.d.ts +1 -0
  140. package/dist/usage/alerts-store.js +1 -1
  141. package/dist/usage/store.d.ts +1 -0
  142. package/dist/usage/store.js +1 -1
  143. package/package.json +3 -3
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8458
34
+ - template files: 8459
@@ -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",
@@ -70,7 +70,7 @@ async function fetchEmailList(
70
70
 
71
71
  const gmailQuery = buildGmailEmailSearchQuery({ view, q: search });
72
72
  const effectiveQuery =
73
- view === "all" && !search ? "" : gmailQuery || "in:inbox -in:sent";
73
+ view === "all" && !search ? "" : gmailQuery || "in:inbox";
74
74
  const { messages } = await listGmailMessages(
75
75
  effectiveQuery,
76
76
  50,
@@ -69,17 +69,25 @@ function googleOAuthErrorPayload(
69
69
  error?.name === "OAuthAccountOwnedByOtherUserError"
70
70
  ) {
71
71
  const account = error.accountId || "This Google account";
72
+ const message = `${account} is connected to another login. Sign out, then sign in with ${account}.`;
72
73
  return {
73
- message: `${account} is already connected to another login. Sign out, then sign in with the login that originally connected it.`,
74
+ message,
74
75
  code: "account_owner_mismatch",
75
76
  accountId: error.accountId,
76
77
  };
77
78
  }
78
79
 
79
80
  const msg = error?.message || "Unknown error";
81
+ const statusCode = Number(error?.statusCode || error?.status || 0);
80
82
  const isPermission =
83
+ error?.oauthErrorCode === "access_denied" ||
84
+ error?.oauthErrorCode === "forbidden" ||
85
+ error?.oauthErrorCode === "insufficient_scope" ||
81
86
  msg.includes("Insufficient Permission") ||
82
- msg.includes("insufficient_scope");
87
+ msg.includes("insufficient_scope") ||
88
+ /insufficient authentication scopes/i.test(msg) ||
89
+ (statusCode === 403 &&
90
+ /\b(?:scope|permission|forbidden|access denied)\b/i.test(msg));
83
91
  return {
84
92
  message: isPermission
85
93
  ? "This account wasn't granted the required permissions. Make sure you check all the permission boxes on the consent screen. If the app is in testing mode, add this email as a test user in Google Cloud Console."
@@ -212,16 +220,11 @@ export const handleGoogleCallback = defineEventHandler(
212
220
  if (googleError) {
213
221
  const errorDesc =
214
222
  (query.error_description as string | undefined) || googleError;
215
- const isPermission =
216
- googleError === "access_denied" ||
217
- errorDesc.includes("Insufficient Permission");
218
- const userMessage = isPermission
219
- ? "Access was denied. Make sure to check all the permission boxes on the consent screen. If the app is in testing mode, add this email as a test user in Google Cloud Console."
220
- : `Connection failed: ${errorDesc}`;
221
- return googleOAuthErrorResponse(event, new Error(userMessage), {
222
- desktop,
223
- flowId,
224
- });
223
+ return googleOAuthErrorResponse(
224
+ event,
225
+ Object.assign(new Error(errorDesc), { oauthErrorCode: googleError }),
226
+ { desktop, flowId },
227
+ );
225
228
  }
226
229
 
227
230
  const code = query.code as string;
@@ -442,16 +445,11 @@ export const handleGoogleAddAccountCallback = defineEventHandler(
442
445
  if (googleError) {
443
446
  const errorDesc =
444
447
  (query.error_description as string | undefined) || googleError;
445
- const isPermission =
446
- googleError === "access_denied" ||
447
- errorDesc.includes("Insufficient Permission");
448
- const userMessage = isPermission
449
- ? "Access was denied. Make sure to check all the permission boxes on the consent screen. If the app is in testing mode, add this email as a test user in Google Cloud Console."
450
- : `Connection failed: ${errorDesc}`;
451
- return googleOAuthErrorResponse(event, new Error(userMessage), {
452
- desktop,
453
- flowId,
454
- });
448
+ return googleOAuthErrorResponse(
449
+ event,
450
+ Object.assign(new Error(errorDesc), { oauthErrorCode: googleError }),
451
+ { desktop, flowId },
452
+ );
455
453
  }
456
454
 
457
455
  const { redirectUri, owner: stateOwner } = state;
@@ -6,8 +6,11 @@ import { isSelfAddressedThread } from "@shared/self-notes.js";
6
6
  import type { EmailMessage } from "@shared/types.js";
7
7
 
8
8
  export const VIEW_QUERIES: Record<string, string> = {
9
- inbox: "in:inbox -in:sent",
10
- unread: "is:unread in:inbox -in:sent",
9
+ // Keep sent replies in a matching Gmail thread. The local thread filter
10
+ // below excludes sent-only threads while preserving received inbox threads
11
+ // whose latest message was sent by the user.
12
+ inbox: "in:inbox",
13
+ unread: "is:unread in:inbox",
11
14
  starred: "is:starred",
12
15
  sent: "in:sent",
13
16
  drafts: "in:drafts",
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  SIDEBAR_STATE_CHANGE_EVENT,
3
- BuilderSetupCard,
4
3
  sendToAgentChat,
5
4
  setAgentChatContextItem,
6
5
  useAgentEngineConfigured,
@@ -8466,23 +8465,12 @@ function CreatePlanDialog({
8466
8465
  </DialogDescription>
8467
8466
  </DialogHeader>
8468
8467
  <div className="grid gap-3">
8469
- {canCreate && agentMissing ? (
8470
- <BuilderSetupCard
8471
- fullWidth
8472
- onConnected={() =>
8473
- window.dispatchEvent(
8474
- new Event("agent-engine:configured-changed"),
8475
- )
8476
- }
8477
- />
8478
- ) : null}
8479
8468
  <div className="rounded-xl border border-border bg-background p-2 shadow-sm">
8480
8469
  <PromptComposer
8481
8470
  autoFocus
8482
8471
  disabled={composerLocked}
8483
8472
  attachmentsEnabled={false}
8484
8473
  showModelSelector={false}
8485
- modelStatusChecksEnabled={false}
8486
8474
  placeholder={t("plansPage.create.placeholder")}
8487
8475
  draftScope="plans:create-plan"
8488
8476
  initialText={promptSeed}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-19
4
+ ---
5
+
6
+ Plan prompts now guide you to connect AI before sending and offer a clear retry after setup.
@@ -1,6 +1,7 @@
1
1
  import type { Task, Message, TaskState, Artifact } from "./types.js";
2
2
  export declare const MAX_A2A_IDEMPOTENCY_KEY_CHARS = 128;
3
3
  export declare const A2A_PERSONAL_OWNER_SCOPE = "__personal__";
4
+ export declare function ensureTable(): Promise<void>;
4
5
  export interface A2AApprovalRecord {
5
6
  id: string;
6
7
  taskId: string;
@@ -5,7 +5,7 @@ let _initPromise;
5
5
  export const MAX_A2A_IDEMPOTENCY_KEY_CHARS = 128;
6
6
  const A2A_IDEMPOTENCY_INDEX = "idx_a2a_tasks_owner_scope_idempotency";
7
7
  export const A2A_PERSONAL_OWNER_SCOPE = "__personal__";
8
- async function ensureTable() {
8
+ export async function ensureTable() {
9
9
  if (!_initPromise) {
10
10
  _initPromise = (async () => {
11
11
  const client = getDbExec();
@@ -12,6 +12,7 @@
12
12
  * runtime even before the migration plugin is registered.
13
13
  */
14
14
  import type { ObservationalMemoryEntry, ObservationalMemoryOwner, ObservationalMemoryTier } from "./types.js";
15
+ export declare function ensureTable(): Promise<void>;
15
16
  /** Reset the cached ensureTable promise — test-only seam. */
16
17
  export declare function __resetObservationalMemoryTableCache(): void;
17
18
  export interface InsertObservationalMemoryInput extends ObservationalMemoryOwner {
@@ -14,7 +14,7 @@
14
14
  import { getDbExec, isPostgres } from "../../db/client.js";
15
15
  import { ensureTableExists, ensureIndexExists } from "../../db/ddl-guard.js";
16
16
  let tableReady = null;
17
- async function ensureTable() {
17
+ export async function ensureTable() {
18
18
  if (tableReady)
19
19
  return tableReady;
20
20
  tableReady = (async () => {
@@ -217,6 +217,7 @@ export declare const IN_FLIGHT_RUN_STALE_GRACE_MS: number;
217
217
  * is reaped like any other stale run.
218
218
  */
219
219
  export declare const IN_FLIGHT_GRACE_MAX_LIVENESS_GAP_MS = 120000;
220
+ export declare function ensureRunTables(): Promise<void>;
220
221
  /**
221
222
  * Persist a zombie tool-call completion to the ledger. Called by the detached
222
223
  * promise continuation after `Promise.race` abandons it. Best-effort — never