@agent-native/core 0.77.4 → 0.77.7

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 (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +31 -0
  3. package/corpus/core/package.json +3 -3
  4. package/corpus/core/src/agent/production-agent.ts +35 -0
  5. package/corpus/core/src/agent/run-store.ts +2 -0
  6. package/corpus/core/src/agent/thread-data-builder.ts +21 -2
  7. package/corpus/core/src/cli/create.ts +35 -1
  8. package/corpus/core/src/cli/skills.ts +8 -0
  9. package/corpus/core/src/client/AgentPanel.tsx +57 -25
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +31 -1
  11. package/corpus/core/src/client/analytics.ts +24 -2
  12. package/corpus/core/src/client/changelog/Changelog.tsx +22 -10
  13. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +405 -35
  14. package/corpus/core/src/client/use-agent-engine-configured.ts +29 -15
  15. package/corpus/core/src/client/use-chat-threads.ts +104 -55
  16. package/corpus/core/src/server/analytics.ts +27 -3
  17. package/corpus/core/src/sharing/access.ts +35 -13
  18. package/corpus/core/src/sharing/actions/share-resource.ts +32 -9
  19. package/corpus/core/src/sharing/actions/unshare-resource.ts +25 -2
  20. package/corpus/core/src/sharing/registry.ts +9 -0
  21. package/corpus/core/src/vite/client.ts +3 -0
  22. package/corpus/templates/analytics/actions/data-source-status.ts +25 -14
  23. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +2 -2
  24. package/corpus/templates/analytics/app/lib/data-source-status.ts +47 -6
  25. package/corpus/templates/analytics/app/lib/data-sources.ts +14 -3
  26. package/corpus/templates/analytics/app/pages/DataSources.tsx +24 -5
  27. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +15 -43
  28. package/corpus/templates/analytics/changelog/2026-06-25-fixed-hubspot-data-source-status-so-private-app-tokens-legac.md +6 -0
  29. package/corpus/templates/analytics/netlify.toml +1 -0
  30. package/corpus/templates/analytics/server/lib/credential-keys.ts +3 -1
  31. package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +12 -3
  32. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +5 -2
  33. package/corpus/templates/assets/changelog/2026-06-25-create-now-returns-from-agent-settings-to-the-asset-chat-eve.md +6 -0
  34. package/corpus/templates/assets/netlify.toml +1 -0
  35. package/corpus/templates/brain/netlify.toml +1 -0
  36. package/corpus/templates/calendar/app/i18n-data.ts +12 -0
  37. package/corpus/templates/calendar/app/lib/clipboard.ts +58 -0
  38. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +15 -9
  39. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +6 -2
  40. package/corpus/templates/calendar/changelog/2026-06-25-booking-link-copy-buttons-no-longer-fail-when-clipboard-perm.md +6 -0
  41. package/corpus/templates/calendar/netlify.toml +1 -0
  42. package/corpus/templates/chat/netlify.toml +1 -0
  43. package/corpus/templates/clips/AGENTS.md +9 -7
  44. package/corpus/templates/clips/actions/add-vocabulary-term.ts +2 -2
  45. package/corpus/templates/clips/actions/archive-recording.ts +5 -2
  46. package/corpus/templates/clips/actions/clear-edits.ts +5 -2
  47. package/corpus/templates/clips/actions/create-folder.ts +7 -2
  48. package/corpus/templates/clips/actions/delete-folder.ts +7 -4
  49. package/corpus/templates/clips/actions/delete-recording-permanent.ts +5 -2
  50. package/corpus/templates/clips/actions/finalize-recording.ts +5 -2
  51. package/corpus/templates/clips/actions/import-loom-recording.ts +2 -1
  52. package/corpus/templates/clips/actions/list-notifications.ts +9 -5
  53. package/corpus/templates/clips/actions/list-organization-state.ts +2 -1
  54. package/corpus/templates/clips/actions/list-recordings.ts +4 -1
  55. package/corpus/templates/clips/actions/move-recording.ts +5 -1
  56. package/corpus/templates/clips/actions/rename-folder.ts +6 -3
  57. package/corpus/templates/clips/actions/request-transcript.ts +5 -2
  58. package/corpus/templates/clips/actions/restore-recording.ts +5 -2
  59. package/corpus/templates/clips/actions/save-browser-diagnostics.ts +21 -8
  60. package/corpus/templates/clips/actions/set-chapters.ts +5 -2
  61. package/corpus/templates/clips/actions/set-thumbnail.ts +5 -2
  62. package/corpus/templates/clips/actions/split-recording.ts +5 -2
  63. package/corpus/templates/clips/actions/stitch-recordings.ts +6 -2
  64. package/corpus/templates/clips/actions/trash-recording.ts +5 -2
  65. package/corpus/templates/clips/actions/undo-edit.ts +5 -2
  66. package/corpus/templates/clips/actions/view-screen.ts +2 -1
  67. package/corpus/templates/clips/app/components/library/recording-card.tsx +24 -7
  68. package/corpus/templates/clips/app/i18n/ar-SA.ts +14 -0
  69. package/corpus/templates/clips/app/i18n/de-DE.ts +14 -0
  70. package/corpus/templates/clips/app/i18n/en-US.ts +14 -0
  71. package/corpus/templates/clips/app/i18n/es-ES.ts +14 -0
  72. package/corpus/templates/clips/app/i18n/fr-FR.ts +14 -0
  73. package/corpus/templates/clips/app/i18n/hi-IN.ts +14 -0
  74. package/corpus/templates/clips/app/i18n/ja-JP.ts +14 -0
  75. package/corpus/templates/clips/app/i18n/ko-KR.ts +14 -0
  76. package/corpus/templates/clips/app/i18n/pt-BR.ts +14 -0
  77. package/corpus/templates/clips/app/i18n/zh-CN.ts +14 -0
  78. package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +5 -1
  79. package/corpus/templates/clips/app/root.tsx +2 -2
  80. package/corpus/templates/clips/app/routes/_app.extensions.$id.tsx +4 -1
  81. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +4 -1
  82. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +12 -5
  83. package/corpus/templates/clips/changelog/2026-06-25-chrome-extension-recordings-now-upload-large-capture-bursts.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-06-25-console-diagnostics-keep-more-useful-non-secret-details-when.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-06-25-fixed-owner-recognition-so-clips-created-from-desktop-and-we.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-06-25-localized-clips-settings-changelog-and-extension-sidebar-lab.md +6 -0
  87. package/corpus/templates/clips/chrome-extension/package.json +3 -0
  88. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  89. package/corpus/templates/clips/chrome-extension/src/background.ts +85 -9
  90. package/corpus/templates/clips/chrome-extension/src/content-script.ts +55 -0
  91. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +79 -3
  92. package/corpus/templates/clips/chrome-extension/src/options.ts +10 -0
  93. package/corpus/templates/clips/chrome-extension/src/overlay.ts +6 -0
  94. package/corpus/templates/clips/chrome-extension/src/permission.ts +8 -1
  95. package/corpus/templates/clips/chrome-extension/src/popup.ts +65 -39
  96. package/corpus/templates/clips/chrome-extension/src/sentry.ts +206 -0
  97. package/corpus/templates/clips/chrome-extension/vite.config.ts +47 -0
  98. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +4 -2
  99. package/corpus/templates/clips/desktop/src/styles.css +15 -1
  100. package/corpus/templates/clips/netlify.toml +1 -0
  101. package/corpus/templates/clips/server/db/index.ts +1 -0
  102. package/corpus/templates/clips/server/lib/google-calendar-oauth.ts +35 -23
  103. package/corpus/templates/clips/server/lib/public-agent-context.ts +8 -1
  104. package/corpus/templates/clips/server/lib/recordings.ts +18 -2
  105. package/corpus/templates/clips/server/lib/slack-oauth.ts +9 -6
  106. package/corpus/templates/clips/server/plugins/onboarding-calendar.ts +6 -2
  107. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +2 -2
  108. package/corpus/templates/clips/server/routes/api/recordings/[recordingId]/thumbnail.post.ts +5 -2
  109. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -2
  110. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +5 -2
  111. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +5 -2
  112. package/corpus/templates/clips/shared/browser-diagnostics.ts +14 -4
  113. package/corpus/templates/content/netlify.toml +1 -0
  114. package/corpus/templates/design/netlify.toml +1 -0
  115. package/corpus/templates/dispatch/netlify.toml +1 -0
  116. package/corpus/templates/forms/netlify.toml +1 -0
  117. package/corpus/templates/macros/netlify.toml +1 -0
  118. package/corpus/templates/mail/app/components/email/EmailList.tsx +7 -3
  119. package/corpus/templates/mail/app/components/email/EmailThread.tsx +4 -1
  120. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +10 -2
  121. package/corpus/templates/mail/app/hooks/use-compose-state.ts +15 -5
  122. package/corpus/templates/mail/app/hooks/use-google-auth.ts +47 -9
  123. package/corpus/templates/mail/changelog/2026-06-25-account-avatars-stay-stable-while-mail-refreshes-google-acco.md +6 -0
  124. package/corpus/templates/mail/changelog/2026-06-25-mail-background-draft-saves-and-thread-prefetches-no-longer.md +6 -0
  125. package/corpus/templates/mail/netlify.toml +1 -0
  126. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +8 -0
  127. package/corpus/templates/plan/netlify.toml +1 -0
  128. package/corpus/templates/slides/netlify.toml +1 -0
  129. package/corpus/templates/videos/netlify.toml +1 -0
  130. package/dist/agent/production-agent.d.ts.map +1 -1
  131. package/dist/agent/production-agent.js +31 -0
  132. package/dist/agent/production-agent.js.map +1 -1
  133. package/dist/agent/run-store.d.ts +2 -0
  134. package/dist/agent/run-store.d.ts.map +1 -1
  135. package/dist/agent/run-store.js +2 -0
  136. package/dist/agent/run-store.js.map +1 -1
  137. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  138. package/dist/agent/thread-data-builder.js +23 -2
  139. package/dist/agent/thread-data-builder.js.map +1 -1
  140. package/dist/cli/create.d.ts.map +1 -1
  141. package/dist/cli/create.js +28 -1
  142. package/dist/cli/create.js.map +1 -1
  143. package/dist/cli/skills.d.ts +1 -1
  144. package/dist/cli/skills.d.ts.map +1 -1
  145. package/dist/cli/skills.js +8 -0
  146. package/dist/cli/skills.js.map +1 -1
  147. package/dist/client/AgentPanel.d.ts +8 -2
  148. package/dist/client/AgentPanel.d.ts.map +1 -1
  149. package/dist/client/AgentPanel.js +29 -15
  150. package/dist/client/AgentPanel.js.map +1 -1
  151. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  152. package/dist/client/MultiTabAssistantChat.js +4 -4
  153. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  154. package/dist/client/analytics.d.ts.map +1 -1
  155. package/dist/client/analytics.js +22 -2
  156. package/dist/client/analytics.js.map +1 -1
  157. package/dist/client/changelog/Changelog.d.ts +7 -2
  158. package/dist/client/changelog/Changelog.d.ts.map +1 -1
  159. package/dist/client/changelog/Changelog.js +9 -7
  160. package/dist/client/changelog/Changelog.js.map +1 -1
  161. package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
  162. package/dist/client/extensions/ExtensionsSidebarSection.js +339 -11
  163. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  164. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  165. package/dist/client/use-agent-engine-configured.js +23 -13
  166. package/dist/client/use-agent-engine-configured.js.map +1 -1
  167. package/dist/client/use-chat-threads.d.ts +4 -0
  168. package/dist/client/use-chat-threads.d.ts.map +1 -1
  169. package/dist/client/use-chat-threads.js +49 -4
  170. package/dist/client/use-chat-threads.js.map +1 -1
  171. package/dist/collab/routes.d.ts +1 -1
  172. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  173. package/dist/observability/routes.d.ts +7 -7
  174. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  175. package/dist/server/analytics.d.ts +4 -0
  176. package/dist/server/analytics.d.ts.map +1 -1
  177. package/dist/server/analytics.js +19 -3
  178. package/dist/server/analytics.js.map +1 -1
  179. package/dist/sharing/access.d.ts.map +1 -1
  180. package/dist/sharing/access.js +26 -12
  181. package/dist/sharing/access.js.map +1 -1
  182. package/dist/sharing/actions/share-resource.js +20 -9
  183. package/dist/sharing/actions/share-resource.js.map +1 -1
  184. package/dist/sharing/actions/unshare-resource.js +13 -2
  185. package/dist/sharing/actions/unshare-resource.js.map +1 -1
  186. package/dist/sharing/registry.d.ts +9 -0
  187. package/dist/sharing/registry.d.ts.map +1 -1
  188. package/dist/sharing/registry.js.map +1 -1
  189. package/dist/vite/client.d.ts.map +1 -1
  190. package/dist/vite/client.js +1 -0
  191. package/dist/vite/client.js.map +1 -1
  192. package/package.json +3 -3
@@ -26,21 +26,13 @@ export function useAgentEngineConfigured(
26
26
  ): UseAgentEngineConfiguredResult {
27
27
  const [state, setState] = useState<AgentEngineConfiguredState>("unknown");
28
28
 
29
- // Mid-run adapter signal that no key is usable — honored even when disabled.
30
29
  useEffect(() => {
31
- const onMissing = () => setState("missing");
32
- window.addEventListener("agent-chat:missing-api-key", onMissing);
33
- return () =>
34
- window.removeEventListener("agent-chat:missing-api-key", onMissing);
35
- }, []);
36
-
37
- useEffect(() => {
38
- if (!enabled) {
39
- setState("configured");
40
- return;
41
- }
42
30
  let cancelled = false;
43
- const check = async () => {
31
+ const check = async (options?: { missingFallback?: boolean }) => {
32
+ if (!enabled) {
33
+ setState("configured");
34
+ return;
35
+ }
44
36
  const [envKeys, builderStatus, engineStatus] = await Promise.all([
45
37
  fetch(agentNativePath("/_agent-native/env-status"))
46
38
  .then((r) => (r.ok ? r.json() : null))
@@ -55,6 +47,7 @@ export function useAgentEngineConfigured(
55
47
  if (cancelled) return;
56
48
  // All three failed — likely a flaky network; keep the current state.
57
49
  if (envKeys == null && builderStatus == null && engineStatus == null) {
50
+ if (options?.missingFallback) setState("missing");
58
51
  return;
59
52
  }
60
53
  const keys = (envKeys ?? []) as Array<{
@@ -68,11 +61,32 @@ export function useAgentEngineConfigured(
68
61
  engineStatus?.configured === true;
69
62
  setState(anyConfigured ? "configured" : "missing");
70
63
  };
64
+ const onConfiguredChanged = () => {
65
+ void check();
66
+ };
67
+ const onMissing = () => {
68
+ if (!enabled) {
69
+ setState("configured");
70
+ return;
71
+ }
72
+ void check({ missingFallback: true });
73
+ };
74
+
71
75
  void check();
72
- window.addEventListener("agent-engine:configured-changed", check);
76
+ window.addEventListener(
77
+ "agent-engine:configured-changed",
78
+ onConfiguredChanged,
79
+ );
80
+ // A stale failed stream can arrive after a reconnect succeeds. Re-check the
81
+ // current status before pinning the composer in setup.
82
+ window.addEventListener("agent-chat:missing-api-key", onMissing);
73
83
  return () => {
74
84
  cancelled = true;
75
- window.removeEventListener("agent-engine:configured-changed", check);
85
+ window.removeEventListener(
86
+ "agent-engine:configured-changed",
87
+ onConfiguredChanged,
88
+ );
89
+ window.removeEventListener("agent-chat:missing-api-key", onMissing);
76
90
  };
77
91
  }, [enabled]);
78
92
 
@@ -74,6 +74,7 @@ export interface UseChatThreadsOptions {
74
74
 
75
75
  const ACTIVE_THREAD_KEY = "agent-chat-active-thread";
76
76
  const THREADS_UPDATED_EVENT = "agent-chat:threads-updated";
77
+ const THREADS_PAGE_SIZE = 50;
77
78
 
78
79
  function emitThreadsUpdated() {
79
80
  if (typeof window === "undefined") return;
@@ -215,6 +216,10 @@ export function useChatThreads(
215
216
  }
216
217
 
217
218
  const [threads, setThreads] = useState<ChatThreadSummary[]>([]);
219
+ const [hasMoreThreads, setHasMoreThreads] = useState(false);
220
+ const [isLoadingMoreThreads, setIsLoadingMoreThreads] = useState(false);
221
+ const [threadsLoadError, setThreadsLoadError] = useState<string | null>(null);
222
+ const nextThreadsOffsetRef = useRef(0);
218
223
  const threadsRef = useRef<ChatThreadSummary[]>(threads);
219
224
  threadsRef.current = threads;
220
225
 
@@ -414,66 +419,106 @@ export function useChatThreads(
414
419
  threads,
415
420
  ]);
416
421
 
417
- const fetchThreads = useCallback(async () => {
418
- try {
419
- const res = await fetch(`${apiUrl}/threads`);
420
- if (!res.ok) return;
421
- const data = await res.json();
422
- setThreads((prev) => {
423
- const loaded = (data.threads ?? []) as ChatThreadSummary[];
424
- const loadedIds = new Set(loaded.map((t) => t.id));
425
- // Preserve any optimistic threads we've created this session that
426
- // haven't shown up in the server list yet — the server only learns
427
- // about a thread when the user actually sends a message and the
428
- // agent run's `persistSubmittedUserMessage` writes the row.
429
- const optimisticOnly = prev.filter(
430
- (t) => newlyCreatedRef.current.has(t.id) && !loadedIds.has(t.id),
422
+ const fetchThreads = useCallback(
423
+ async (options?: { append?: boolean }) => {
424
+ try {
425
+ const offset = options?.append ? nextThreadsOffsetRef.current : 0;
426
+ const res = await fetch(
427
+ offset > 0
428
+ ? `${apiUrl}/threads?offset=${offset}`
429
+ : `${apiUrl}/threads`,
431
430
  );
432
- // Reconcile each server thread against our local copy. If the local
433
- // copy has a newer updatedAt or higher messageCount, keep those
434
- // fields the server probably hasn't observed the user's latest
435
- // send yet, and naively replacing makes the recent-chats list
436
- // visibly jump back to older timestamps right after a send.
437
- const merged = loaded.map((server) => {
438
- const local = prev.find((t) => t.id === server.id);
439
- if (!local) return server;
440
- const next = { ...server };
441
- if (local.updatedAt > server.updatedAt) {
442
- next.updatedAt = local.updatedAt;
443
- }
444
- if (userRenamedThreadIdsRef.current.has(server.id) && local.title) {
445
- next.title = local.title;
446
- }
447
- if (pendingPinnedAtRef.current.has(server.id)) {
448
- next.pinnedAt = pendingPinnedAtRef.current.get(server.id) ?? null;
449
- }
450
- if (pendingArchivedAtRef.current.has(server.id)) {
451
- next.archivedAt =
452
- pendingArchivedAtRef.current.get(server.id) ?? null;
453
- }
454
- if (local.messageCount > server.messageCount) {
455
- next.messageCount = local.messageCount;
456
- if (local.preview) next.preview = local.preview;
457
- if (local.title) next.title = local.title;
431
+ if (!res.ok) {
432
+ if (!options?.append) {
433
+ setThreadsLoadError("Could not load chat history.");
458
434
  }
459
- // Preserve optimistic scope: when the server creates the row
460
- // on first message it does so without scope, and the next PUT
461
- // (saveThreadData) writes the local scope back. In the brief
462
- // window between those, the server list returns scope: null
463
- // while the user is clearly working inside a deck — keep the
464
- // local value so the tab bar doesn't blink unscoped.
465
- if (local.scope && !server.scope) {
466
- next.scope = local.scope;
435
+ return;
436
+ }
437
+ const data = await res.json();
438
+ setThreadsLoadError(null);
439
+ const loaded = (data.threads ?? []) as ChatThreadSummary[];
440
+ if (!options?.append) {
441
+ nextThreadsOffsetRef.current = loaded.length;
442
+ } else {
443
+ nextThreadsOffsetRef.current += loaded.length;
444
+ }
445
+ setHasMoreThreads(loaded.length >= THREADS_PAGE_SIZE);
446
+ setThreads((prev) => {
447
+ const loadedIds = new Set(loaded.map((t) => t.id));
448
+ // Preserve any optimistic threads we've created this session that
449
+ // haven't shown up in the server list yet — the server only learns
450
+ // about a thread when the user actually sends a message and the
451
+ // agent run's `persistSubmittedUserMessage` writes the row.
452
+ const optimisticOnly = prev.filter(
453
+ (t) => newlyCreatedRef.current.has(t.id) && !loadedIds.has(t.id),
454
+ );
455
+ // Reconcile each server thread against our local copy. If the local
456
+ // copy has a newer updatedAt or higher messageCount, keep those
457
+ // fields — the server probably hasn't observed the user's latest
458
+ // send yet, and naively replacing makes the recent-chats list
459
+ // visibly jump back to older timestamps right after a send.
460
+ const merged = loaded.map((server) => {
461
+ const local = prev.find((t) => t.id === server.id);
462
+ if (!local) return server;
463
+ const next = { ...server };
464
+ if (local.updatedAt > server.updatedAt) {
465
+ next.updatedAt = local.updatedAt;
466
+ }
467
+ if (userRenamedThreadIdsRef.current.has(server.id) && local.title) {
468
+ next.title = local.title;
469
+ }
470
+ if (pendingPinnedAtRef.current.has(server.id)) {
471
+ next.pinnedAt = pendingPinnedAtRef.current.get(server.id) ?? null;
472
+ }
473
+ if (pendingArchivedAtRef.current.has(server.id)) {
474
+ next.archivedAt =
475
+ pendingArchivedAtRef.current.get(server.id) ?? null;
476
+ }
477
+ if (local.messageCount > server.messageCount) {
478
+ next.messageCount = local.messageCount;
479
+ if (local.preview) next.preview = local.preview;
480
+ if (local.title) next.title = local.title;
481
+ }
482
+ // Preserve optimistic scope: when the server creates the row
483
+ // on first message it does so without scope, and the next PUT
484
+ // (saveThreadData) writes the local scope back. In the brief
485
+ // window between those, the server list returns scope: null
486
+ // while the user is clearly working inside a deck — keep the
487
+ // local value so the tab bar doesn't blink unscoped.
488
+ if (local.scope && !server.scope) {
489
+ next.scope = local.scope;
490
+ }
491
+ return next;
492
+ });
493
+ if (options?.append) {
494
+ const existingIds = new Set(prev.map((t) => t.id));
495
+ return sortThreadSummaries([
496
+ ...prev,
497
+ ...merged.filter((t) => !existingIds.has(t.id)),
498
+ ]);
467
499
  }
468
- return next;
500
+ return [...optimisticOnly, ...merged];
469
501
  });
470
- return [...optimisticOnly, ...merged];
471
- });
472
- return data.threads as ChatThreadSummary[];
473
- } catch {
474
- return undefined;
502
+ return data.threads as ChatThreadSummary[];
503
+ } catch {
504
+ if (!options?.append) {
505
+ setThreadsLoadError("Could not load chat history.");
506
+ }
507
+ return undefined;
508
+ }
509
+ },
510
+ [apiUrl],
511
+ );
512
+
513
+ const loadMoreThreads = useCallback(async (): Promise<void> => {
514
+ if (isLoadingMoreThreads || !hasMoreThreads) return;
515
+ setIsLoadingMoreThreads(true);
516
+ try {
517
+ await fetchThreads({ append: true });
518
+ } finally {
519
+ setIsLoadingMoreThreads(false);
475
520
  }
476
- }, [apiUrl]);
521
+ }, [fetchThreads, hasMoreThreads, isLoadingMoreThreads]);
477
522
 
478
523
  // Initial load: load threads from server, then reconcile against the
479
524
  // saved active thread.
@@ -1158,10 +1203,14 @@ export function useChatThreads(
1158
1203
  saveThreadData,
1159
1204
  generateTitle,
1160
1205
  searchThreads,
1206
+ loadMoreThreads,
1161
1207
  getThreadShareState,
1162
1208
  createThreadShareLink,
1163
1209
  revokeThreadShareLink,
1164
1210
  refreshThreads,
1211
+ hasMoreThreads,
1212
+ isLoadingMoreThreads,
1213
+ threadsLoadError,
1165
1214
  isNewThread,
1166
1215
  };
1167
1216
  }
@@ -3,6 +3,10 @@
3
3
  * Supported environment variables:
4
4
  * - `GA_MEASUREMENT_ID` — Google Analytics 4 measurement ID
5
5
  *
6
+ * Netlify configuration-file env vars are build-time only for serverless
7
+ * functions, so the Vite plugin also bakes this public value into SSR bundles
8
+ * as `__AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__`.
9
+ *
6
10
  * Amplitude and Sentry are initialized client-side via their npm packages
7
11
  * (see `packages/core/src/client/analytics.ts`). Only GA requires script
8
12
  * tag injection because the gtag.js loader must be a `<script src>`.
@@ -17,16 +21,36 @@
17
21
  * ```
18
22
  */
19
23
 
24
+ declare const __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__: string | undefined;
25
+
26
+ function normalizeMeasurementId(value: string | undefined): string | null {
27
+ const trimmed = value?.trim();
28
+ return trimmed ? trimmed : null;
29
+ }
30
+
31
+ function getGaMeasurementId(): string | null {
32
+ return (
33
+ normalizeMeasurementId(process.env.GA_MEASUREMENT_ID) ||
34
+ normalizeMeasurementId(
35
+ typeof __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__ === "string"
36
+ ? __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__
37
+ : undefined,
38
+ )
39
+ );
40
+ }
41
+
20
42
  function getGaScript(): string | null {
21
- const id = process.env.GA_MEASUREMENT_ID;
43
+ const id = getGaMeasurementId();
22
44
  if (!id) return null;
45
+ const srcId = encodeURIComponent(id);
46
+ const jsId = JSON.stringify(id);
23
47
  return (
24
- `<script async src="https://www.googletagmanager.com/gtag/js?id=${id}"></script>` +
48
+ `<script async src="https://www.googletagmanager.com/gtag/js?id=${srcId}"></script>` +
25
49
  `<script>` +
26
50
  `window.dataLayer=window.dataLayer||[];` +
27
51
  `function gtag(){dataLayer.push(arguments);}` +
28
52
  `gtag('js',new Date());` +
29
- `gtag('config','${id}');` +
53
+ `gtag('config',${jsId});` +
30
54
  `if(typeof sessionStorage!=='undefined'&&sessionStorage.getItem('__an_signin')){` +
31
55
  `sessionStorage.removeItem('__an_signin');` +
32
56
  `gtag('event','sign_in');` +
@@ -71,6 +71,15 @@ export function resolveRegisteredAccessContext(
71
71
  return reg?.resolveAccessContext ? reg.resolveAccessContext(ctx) : ctx;
72
72
  }
73
73
 
74
+ function normalizeEmailForAccess(email: string | undefined): string | null {
75
+ const normalized = email?.trim().toLowerCase();
76
+ return normalized || null;
77
+ }
78
+
79
+ function emailColumnMatches(column: any, email: string): SQL {
80
+ return sql`lower(${column}) = ${email}`;
81
+ }
82
+
74
83
  /**
75
84
  * Build a Drizzle `WHERE` clause that admits rows the current user can see.
76
85
  * Pass the ownable resource table and its shares table; optional min role
@@ -103,15 +112,16 @@ export function accessFilter(
103
112
  const reg = findRegistrationByTable(resourceTable);
104
113
  const ctx = resolveRegisteredAccessContext(reg, rawCtx);
105
114
  const { userEmail, orgId } = ctx;
115
+ const normalizedUserEmail = normalizeEmailForAccess(userEmail);
106
116
  const publicAllowed = reg?.allowPublic !== false;
107
117
  const includePublic = (options.includePublic ?? false) && publicAllowed;
108
118
  const clauses: SQL[] = [];
109
119
 
110
- if (userEmail) {
120
+ if (normalizedUserEmail) {
111
121
  clauses.push(
112
122
  and(
113
- eq(resourceTable.ownerEmail, userEmail),
114
- ownerScopeFilter(resourceTable, ctx),
123
+ emailColumnMatches(resourceTable.ownerEmail, normalizedUserEmail),
124
+ ownerScopeFilter(reg, resourceTable, ctx),
115
125
  )!,
116
126
  );
117
127
  }
@@ -128,13 +138,13 @@ export function accessFilter(
128
138
  );
129
139
  }
130
140
  }
131
- if (userEmail) {
141
+ if (normalizedUserEmail) {
132
142
  const shareScope = restrictedShareScopeSql(reg, resourceTable, ctx);
133
143
  clauses.push(
134
144
  sql`exists (select 1 from ${sharesTable}
135
145
  where ${sharesTable.resourceId} = ${resourceTable.id}
136
146
  and ${sharesTable.principalType} = 'user'
137
- and ${sharesTable.principalId} = ${userEmail}
147
+ and lower(${sharesTable.principalId}) = ${normalizedUserEmail}
138
148
  and ${shareScope}
139
149
  and ${minRoleSql(minRole)})`,
140
150
  );
@@ -154,7 +164,12 @@ export function accessFilter(
154
164
  return or(...clauses) ?? sql`1=0`;
155
165
  }
156
166
 
157
- function ownerScopeFilter(resourceTable: any, ctx: AccessContext): SQL {
167
+ function ownerScopeFilter(
168
+ reg: ShareableResourceRegistration | undefined,
169
+ resourceTable: any,
170
+ ctx: AccessContext,
171
+ ): SQL {
172
+ if (reg?.ownerAccessIgnoresOrg === true) return sql`1=1`;
158
173
  if (ctx.orgId) {
159
174
  // Rows created before org-scoping, or in solo mode, have no org_id. Keep
160
175
  // them manageable by their owner after the owner joins or switches into an
@@ -167,7 +182,12 @@ function ownerScopeFilter(resourceTable: any, ctx: AccessContext): SQL {
167
182
  return sql`${resourceTable.orgId} IS NULL`;
168
183
  }
169
184
 
170
- function ownerMatchesActiveScope(resource: any, ctx: AccessContext): boolean {
185
+ function ownerMatchesActiveScope(
186
+ reg: ShareableResourceRegistration | undefined,
187
+ resource: any,
188
+ ctx: AccessContext,
189
+ ): boolean {
190
+ if (reg?.ownerAccessIgnoresOrg === true) return true;
171
191
  const resourceOrgId = resource?.orgId ?? null;
172
192
  if (!resourceOrgId) return true;
173
193
  return ctx.orgId === resourceOrgId;
@@ -233,11 +253,12 @@ export async function resolveAccess(
233
253
  if (!resource) return null;
234
254
 
235
255
  const { userEmail, orgId } = ctx;
256
+ const normalizedUserEmail = normalizeEmailForAccess(userEmail);
236
257
 
237
258
  if (
238
- userEmail &&
239
- resource.ownerEmail === userEmail &&
240
- ownerMatchesActiveScope(resource, ctx)
259
+ normalizedUserEmail &&
260
+ normalizeEmailForAccess(resource.ownerEmail) === normalizedUserEmail &&
261
+ ownerMatchesActiveScope(reg, resource, ctx)
241
262
  ) {
242
263
  return { role: "owner", resource };
243
264
  }
@@ -265,16 +286,17 @@ async function highestShareRole(
265
286
  resource: any,
266
287
  ): Promise<ShareRole | null> {
267
288
  const { userEmail, orgId } = ctx;
268
- if (!userEmail && !orgId) return null;
289
+ const normalizedUserEmail = normalizeEmailForAccess(userEmail);
290
+ if (!normalizedUserEmail && !orgId) return null;
269
291
  if (!explicitSharesAllowedForResource(reg, resource, ctx)) return null;
270
292
  const db = reg.getDb() as any;
271
293
 
272
294
  const principalClauses: ReturnType<typeof and>[] = [];
273
- if (userEmail) {
295
+ if (normalizedUserEmail) {
274
296
  principalClauses.push(
275
297
  and(
276
298
  eq(reg.sharesTable.principalType, "user"),
277
- eq(reg.sharesTable.principalId, userEmail),
299
+ emailColumnMatches(reg.sharesTable.principalId, normalizedUserEmail),
278
300
  ),
279
301
  );
280
302
  }
@@ -1,4 +1,4 @@
1
- import { and, eq } from "drizzle-orm";
1
+ import { and, eq, sql, type SQL } from "drizzle-orm";
2
2
  import { z } from "zod";
3
3
 
4
4
  import { defineAction } from "../../action.js";
@@ -89,6 +89,25 @@ function nanoid(size = 12): string {
89
89
  return id;
90
90
  }
91
91
 
92
+ function normalizePrincipalId(
93
+ principalType: "user" | "org",
94
+ principalId: string,
95
+ ): string {
96
+ return principalType === "user"
97
+ ? principalId.trim().toLowerCase()
98
+ : principalId;
99
+ }
100
+
101
+ function principalIdMatches(
102
+ sharesTable: any,
103
+ principalType: "user" | "org",
104
+ principalId: string,
105
+ ): SQL {
106
+ return principalType === "user"
107
+ ? sql`lower(${sharesTable.principalId}) = ${principalId}`
108
+ : eq(sharesTable.principalId, principalId);
109
+ }
110
+
92
111
  /**
93
112
  * Returns true if the given email is either an active member of `orgId` or
94
113
  * has a pending invitation to `orgId`. Used by resources whose registration
@@ -163,6 +182,10 @@ export default defineAction({
163
182
  );
164
183
  const actor = getRequestUserEmail();
165
184
  if (!actor) throw new ForbiddenError("Not signed in");
185
+ const principalId = normalizePrincipalId(
186
+ args.principalType,
187
+ args.principalId,
188
+ );
166
189
  const beforeExtensionTargets = await getExtensionShareChangeTargets(
167
190
  args.resourceType,
168
191
  args.resourceId,
@@ -176,10 +199,10 @@ export default defineAction({
176
199
  );
177
200
  }
178
201
  if (args.principalType === "user") {
179
- const ok = await isOrgMemberOrInvited(resourceOrgId, args.principalId);
202
+ const ok = await isOrgMemberOrInvited(resourceOrgId, principalId);
180
203
  if (!ok) {
181
204
  throw new ForbiddenError(
182
- `${args.principalId} is not in your organization. Invite them to the organization first, then share.`,
205
+ `${principalId} is not in your organization. Invite them to the organization first, then share.`,
183
206
  );
184
207
  }
185
208
  } else if (args.principalType === "org") {
@@ -187,7 +210,7 @@ export default defineAction({
187
210
  // extension code in the viewer's auth context — the same threat
188
211
  // model that blocks public + cross-org user shares. Pin org-
189
212
  // principal shares to the resource's own org.
190
- if (args.principalId !== resourceOrgId) {
213
+ if (principalId !== resourceOrgId) {
191
214
  throw new ForbiddenError(
192
215
  `${reg.displayName} can only be shared with its own organization, not a different one.`,
193
216
  );
@@ -203,7 +226,7 @@ export default defineAction({
203
226
  and(
204
227
  eq(reg.sharesTable.resourceId, args.resourceId),
205
228
  eq(reg.sharesTable.principalType, args.principalType),
206
- eq(reg.sharesTable.principalId, args.principalId),
229
+ principalIdMatches(reg.sharesTable, args.principalType, principalId),
207
230
  ),
208
231
  );
209
232
 
@@ -225,7 +248,7 @@ export default defineAction({
225
248
  id,
226
249
  resourceId: args.resourceId,
227
250
  principalType: args.principalType,
228
- principalId: args.principalId,
251
+ principalId,
229
252
  role: args.role,
230
253
  createdBy: actor,
231
254
  createdAt: new Date().toISOString(),
@@ -240,7 +263,7 @@ export default defineAction({
240
263
  args.notify !== false &&
241
264
  args.principalType === "user" &&
242
265
  isEmailConfigured() &&
243
- !isSyntheticQaEmail(args.principalId)
266
+ !isSyntheticQaEmail(principalId)
244
267
  ) {
245
268
  try {
246
269
  const titleCol = reg.titleColumn ?? "title";
@@ -268,12 +291,12 @@ export default defineAction({
268
291
  heading: "You've been given access",
269
292
  paragraphs: [
270
293
  `${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,
271
- `Use the button below to open it. If prompted, sign in with ${emailStrong(args.principalId)}.`,
294
+ `Use the button below to open it. If prompted, sign in with ${emailStrong(principalId)}.`,
272
295
  ],
273
296
  cta: { label: `Open ${reg.displayName}`, url: notificationUrl },
274
297
  footer: `You received this because ${actor} granted you ${args.role} access.`,
275
298
  });
276
- await sendEmail({ to: args.principalId, subject, html, text });
299
+ await sendEmail({ to: principalId, subject, html, text });
277
300
  } catch (err) {
278
301
  console.error(
279
302
  "[share-resource] failed to send share notification:",
@@ -1,4 +1,4 @@
1
- import { and, eq } from "drizzle-orm";
1
+ import { and, eq, sql, type SQL } from "drizzle-orm";
2
2
  import { z } from "zod";
3
3
 
4
4
  import { defineAction } from "../../action.js";
@@ -9,6 +9,25 @@ import {
9
9
  notifyExtensionShareChanged,
10
10
  } from "./extension-change.js";
11
11
 
12
+ function normalizePrincipalId(
13
+ principalType: "user" | "org",
14
+ principalId: string,
15
+ ): string {
16
+ return principalType === "user"
17
+ ? principalId.trim().toLowerCase()
18
+ : principalId;
19
+ }
20
+
21
+ function principalIdMatches(
22
+ sharesTable: any,
23
+ principalType: "user" | "org",
24
+ principalId: string,
25
+ ): SQL {
26
+ return principalType === "user"
27
+ ? sql`lower(${sharesTable.principalId}) = ${principalId}`
28
+ : eq(sharesTable.principalId, principalId);
29
+ }
30
+
12
31
  export default defineAction({
13
32
  description:
14
33
  "Revoke a previously granted share. Owner or admin role required.",
@@ -28,13 +47,17 @@ export default defineAction({
28
47
  args.resourceId,
29
48
  );
30
49
  const db = reg.getDb() as any;
50
+ const principalId = normalizePrincipalId(
51
+ args.principalType,
52
+ args.principalId,
53
+ );
31
54
  await db
32
55
  .delete(reg.sharesTable)
33
56
  .where(
34
57
  and(
35
58
  eq(reg.sharesTable.resourceId, args.resourceId),
36
59
  eq(reg.sharesTable.principalType, args.principalType),
37
- eq(reg.sharesTable.principalId, args.principalId),
60
+ principalIdMatches(reg.sharesTable, args.principalType, principalId),
38
61
  ),
39
62
  );
40
63
  await notifyExtensionShareChanged(
@@ -78,6 +78,15 @@ export interface ShareableResourceRegistration {
78
78
  userEmail?: string;
79
79
  orgId?: string;
80
80
  };
81
+ /**
82
+ * When true, direct ownership is recognized by owner_email regardless of the
83
+ * caller's active org. Use this only for resource types where the template's
84
+ * own actions already treat owner_email as the cross-org authority and list
85
+ * views add their own org filters.
86
+ *
87
+ * Default: `false`.
88
+ */
89
+ ownerAccessIgnoresOrg?: boolean;
81
90
  }
82
91
 
83
92
  // Stash the registry on globalThis so it survives SSR bundle duplication.
@@ -1843,6 +1843,9 @@ function createAgentNativeConfig(
1843
1843
  define: {
1844
1844
  ...(userConfig.define ?? {}),
1845
1845
  ...(options.define ?? {}),
1846
+ __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__: JSON.stringify(
1847
+ process.env.GA_MEASUREMENT_ID?.trim() || "",
1848
+ ),
1846
1849
  // Framework route warmup controls how SSR `.data` routes are fetched:
1847
1850
  // ordinary fetches keep them CDN-cacheable, while native prefetch headers
1848
1851
  // can be refused before the CDN/origin sees the request. Keep this value
@@ -151,32 +151,43 @@ export default defineAction({
151
151
  );
152
152
  const visibleKeys = new Set(results.map((result) => result.key));
153
153
  const providers = credentialProviderConfigs
154
- .filter((provider) =>
155
- provider.requiredKeys.every((key) => visibleKeys.has(key)),
156
- )
154
+ .filter((provider) => {
155
+ const requiredMode = provider.requiredMode ?? "all";
156
+ return requiredMode === "any"
157
+ ? provider.requiredKeys.some((key) => visibleKeys.has(key))
158
+ : provider.requiredKeys.every((key) => visibleKeys.has(key));
159
+ })
157
160
  .map((provider) => {
158
161
  const optionalKeys = provider.optionalKeys ?? [];
159
- const missingRequiredKeys = provider.requiredKeys.filter(
160
- (key) => !configuredKeys.has(key),
161
- );
162
+ const requiredMode = provider.requiredMode ?? "all";
163
+ const hasRequiredCredentials =
164
+ requiredMode === "any"
165
+ ? provider.requiredKeys.some((key) => configuredKeys.has(key))
166
+ : provider.requiredKeys.every((key) => configuredKeys.has(key));
167
+ const missingRequiredKeys = hasRequiredCredentials
168
+ ? []
169
+ : provider.requiredKeys.filter((key) => !configuredKeys.has(key));
162
170
  const configuredProviderKeys = [
163
171
  ...provider.requiredKeys,
164
172
  ...optionalKeys,
165
173
  ].filter((key) => configuredKeys.has(key));
174
+ const workspaceConnection =
175
+ workspaceProviderStatusById.get(provider.provider) ??
176
+ summarizeWorkspaceConnections(
177
+ provider.provider,
178
+ workspace.connections,
179
+ workspace.grants,
180
+ );
166
181
  return {
167
182
  provider: provider.provider,
168
183
  label: provider.label,
169
- configured: missingRequiredKeys.length === 0,
184
+ configured:
185
+ hasRequiredCredentials ||
186
+ workspaceConnection.grantState === "connected",
170
187
  configuredKeys: configuredProviderKeys,
171
188
  missingRequiredKeys,
172
189
  optionalKeys,
173
- workspaceConnection:
174
- workspaceProviderStatusById.get(provider.provider) ??
175
- summarizeWorkspaceConnections(
176
- provider.provider,
177
- workspace.connections,
178
- workspace.grants,
179
- ),
190
+ workspaceConnection,
180
191
  };
181
192
  });
182
193
  return {