@agent-native/core 0.144.2 → 0.144.3

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 (70) hide show
  1. package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -4
  2. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +7 -3
  3. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +13 -3
  4. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +64 -23
  5. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +69 -46
  6. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +35 -6
  7. package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +24 -3
  8. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +45 -6
  9. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +38 -5
  10. package/corpus/templates/clips/desktop/src/app.tsx +146 -45
  11. package/corpus/templates/clips/server/jobs/brain-export.ts +13 -6
  12. package/corpus/templates/clips/server/jobs/builder-media-compression.ts +10 -3
  13. package/corpus/templates/clips/server/jobs/meeting-reminders.ts +10 -3
  14. package/corpus/templates/clips/server/jobs/poll-calendars.ts +23 -21
  15. package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +10 -3
  16. package/corpus/templates/clips/server/jobs/transactional-emails.ts +10 -3
  17. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
  18. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +15 -2
  19. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +82 -39
  20. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +31 -9
  21. package/corpus/templates/mail/server/plugins/mail-jobs.ts +36 -17
  22. package/dist/client/AssistantChat.js +73 -43
  23. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  24. package/dist/client/MultiTabAssistantChat.js +79 -92
  25. package/dist/client/browser-session-bridge.js +97 -54
  26. package/dist/client/db-admin/useAgentSync.js +34 -43
  27. package/dist/client/dynamic-suggestions.d.ts +3 -0
  28. package/dist/client/dynamic-suggestions.js +54 -52
  29. package/dist/client/hooks/index.d.ts +1 -0
  30. package/dist/client/hooks/index.js +1 -0
  31. package/dist/client/i18n.d.ts +7 -0
  32. package/dist/client/i18n.js +12 -3
  33. package/dist/client/integrations/useIntegrationStatus.d.ts +1 -1
  34. package/dist/client/integrations/useIntegrationStatus.js +9 -22
  35. package/dist/client/notifications/NotificationsBell.js +14 -9
  36. package/dist/client/progress/RunsTray.js +6 -5
  37. package/dist/client/settings/useBuilderStatus.js +95 -73
  38. package/dist/client/transcription/BuilderTranscriptionCta.js +27 -31
  39. package/dist/client/use-agent-engine-configured.js +21 -3
  40. package/dist/client/use-pausing-interval.d.ts +6 -0
  41. package/dist/client/use-pausing-interval.js +6 -0
  42. package/dist/client/use-poll-loop.d.ts +39 -0
  43. package/dist/client/use-poll-loop.js +73 -0
  44. package/dist/client/use-run-stuck-detection.js +41 -13
  45. package/dist/collab/routes.d.ts +1 -1
  46. package/dist/integrations/google-docs-poller.js +26 -7
  47. package/dist/integrations/pending-tasks-retry-job.js +17 -23
  48. package/dist/integrations/plugin.js +16 -15
  49. package/dist/integrations/remote-push-delivery-job.js +16 -25
  50. package/dist/integrations/remote-retry-job.js +19 -23
  51. package/dist/notifications/routes.d.ts +1 -1
  52. package/dist/oauth-tokens/google-refresh.js +12 -19
  53. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  54. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  55. package/dist/server/agent-chat-plugin.js +26 -29
  56. package/dist/server/index.d.ts +1 -0
  57. package/dist/server/index.js +1 -0
  58. package/dist/server/interval-job.d.ts +25 -0
  59. package/dist/server/interval-job.js +19 -0
  60. package/dist/server/realtime-token.d.ts +1 -1
  61. package/dist/shared/index.d.ts +1 -0
  62. package/dist/shared/index.js +1 -0
  63. package/dist/shared/poll-engine.d.ts +49 -0
  64. package/dist/shared/poll-engine.js +148 -0
  65. package/dist/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  66. package/dist/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
  67. package/dist/tracking/providers.js +10 -1
  68. package/package.json +4 -3
  69. package/src/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  70. package/src/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
@@ -1,14 +1,17 @@
1
- import { useEffect, useMemo, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { readClientAppState } from "./application-state.js";
3
3
  import { useChangeVersions } from "./use-change-version.js";
4
+ import { usePollLoop } from "./use-poll-loop.js";
4
5
  const SAFE_BROWSER_TAB_ID_RE = /^[A-Za-z0-9_-]{1,96}$/;
5
6
  const DEFAULT_MAX_SUGGESTIONS = 3;
7
+ const DEFAULT_POLL_MS = 30_000;
6
8
  export function normalizeAgentDynamicSuggestionsConfig(option) {
7
9
  if (option === false) {
8
10
  return {
9
11
  enabled: false,
10
12
  max: DEFAULT_MAX_SUGGESTIONS,
11
13
  includeStatic: true,
14
+ pollMs: DEFAULT_POLL_MS,
12
15
  };
13
16
  }
14
17
  if (option === true || option === undefined) {
@@ -16,6 +19,7 @@ export function normalizeAgentDynamicSuggestionsConfig(option) {
16
19
  enabled: true,
17
20
  max: DEFAULT_MAX_SUGGESTIONS,
18
21
  includeStatic: true,
22
+ pollMs: DEFAULT_POLL_MS,
19
23
  };
20
24
  }
21
25
  return {
@@ -25,6 +29,9 @@ export function normalizeAgentDynamicSuggestionsConfig(option) {
25
29
  : DEFAULT_MAX_SUGGESTIONS,
26
30
  includeStatic: option.includeStatic !== false,
27
31
  ...(option.getSuggestions ? { getSuggestions: option.getSuggestions } : {}),
32
+ pollMs: typeof option.pollMs === "number" && Number.isFinite(option.pollMs)
33
+ ? Math.max(0, Math.floor(option.pollMs))
34
+ : DEFAULT_POLL_MS,
28
35
  };
29
36
  }
30
37
  function normalizeBrowserTabId(browserTabId) {
@@ -263,64 +270,59 @@ export function useAgentDynamicSuggestionsResult(options) {
263
270
  const enabled = options.enabled !== false && config.enabled;
264
271
  const [context, setContext] = useState(null);
265
272
  const [isLoading, setIsLoading] = useState(false);
273
+ // Invalidates in-flight loads from a stale scope/browserTabId/enabled
274
+ // generation — shared between the leading load below and the safety-net
275
+ // poll so a slow response from an old generation can't clobber a newer one.
276
+ const loadGenerationRef = useRef(0);
277
+ const load = useCallback(async (showLoading) => {
278
+ const generation = loadGenerationRef.current;
279
+ if (showLoading)
280
+ setIsLoading(true);
281
+ try {
282
+ const [navigation, selection, pendingSelection, url] = await Promise.all([
283
+ readScopedAppState("navigation", browserTabId),
284
+ readScopedAppState("selection", browserTabId),
285
+ readScopedAppState("pending-selection-context", browserTabId),
286
+ readScopedAppState("__url__", browserTabId),
287
+ ]);
288
+ if (generation !== loadGenerationRef.current)
289
+ return;
290
+ setContext({ navigation, selection, pendingSelection, url, scope });
291
+ }
292
+ catch {
293
+ if (generation !== loadGenerationRef.current)
294
+ return;
295
+ setContext({
296
+ navigation: null,
297
+ selection: null,
298
+ pendingSelection: null,
299
+ url: null,
300
+ scope,
301
+ });
302
+ }
303
+ finally {
304
+ if (generation === loadGenerationRef.current && showLoading) {
305
+ setIsLoading(false);
306
+ }
307
+ }
308
+ }, [browserTabId, scope]);
266
309
  useEffect(() => {
310
+ loadGenerationRef.current += 1;
267
311
  if (!enabled) {
268
312
  setContext(null);
269
313
  setIsLoading(false);
270
314
  return;
271
315
  }
272
- let cancelled = false;
273
- const load = async (showLoading) => {
274
- if (showLoading)
275
- setIsLoading(true);
276
- try {
277
- const [navigation, selection, pendingSelection, url] = await Promise.all([
278
- readScopedAppState("navigation", browserTabId),
279
- readScopedAppState("selection", browserTabId),
280
- readScopedAppState("pending-selection-context", browserTabId),
281
- readScopedAppState("__url__", browserTabId),
282
- ]);
283
- if (cancelled)
284
- return;
285
- setContext({
286
- navigation,
287
- selection,
288
- pendingSelection,
289
- url,
290
- scope,
291
- });
292
- }
293
- catch {
294
- if (cancelled)
295
- return;
296
- setContext({
297
- navigation: null,
298
- selection: null,
299
- pendingSelection: null,
300
- url: null,
301
- scope,
302
- });
303
- }
304
- finally {
305
- if (!cancelled && showLoading)
306
- setIsLoading(false);
307
- }
308
- };
309
316
  void load(true);
310
- const interval = setInterval(() => {
311
- // The useEffect deps already include appStateVersion, so app-state
312
- // changes trigger an immediate event-driven refresh above. This
313
- // interval is only a slow safety net for updates that don't bump
314
- // that version — skip ticks while the tab isn't visible.
315
- if (document.hidden)
316
- return;
317
- void load(false);
318
- }, 30_000);
319
- return () => {
320
- cancelled = true;
321
- clearInterval(interval);
322
- };
323
- }, [appStateVersion, browserTabId, enabled, scope, scopeKey]);
317
+ }, [appStateVersion, enabled, load, scopeKey, config]);
318
+ // Slow safety net for app-state updates that don't bump `appStateVersion`
319
+ // (the effect above already handles the event-driven case). `pollMs <= 0`
320
+ // disables it.
321
+ usePollLoop(() => load(false), {
322
+ intervalMs: config.pollMs,
323
+ leading: false,
324
+ enabled: enabled && config.pollMs > 0,
325
+ });
324
326
  const suggestions = useMemo(() => {
325
327
  if (!enabled) {
326
328
  return options.staticSuggestions
@@ -10,3 +10,4 @@ export { createAgentNativeQueryClient } from "../create-query-client.js";
10
10
  export { AppProviders, type AppProvidersProps } from "../app-providers.js";
11
11
  export { usePinchZoom, type UsePinchZoomOptions } from "../use-pinch-zoom.js";
12
12
  export { useAvatarUrl, uploadAvatar, invalidateAvatarCache, } from "../use-avatar.js";
13
+ export { usePollLoop, type UsePollLoopOptions, type UsePollLoopHandle, } from "../use-poll-loop.js";
@@ -10,3 +10,4 @@ export { createAgentNativeQueryClient } from "../create-query-client.js";
10
10
  export { AppProviders } from "../app-providers.js";
11
11
  export { usePinchZoom } from "../use-pinch-zoom.js";
12
12
  export { useAvatarUrl, uploadAvatar, invalidateAvatarCache, } from "../use-avatar.js";
13
+ export { usePollLoop, } from "../use-poll-loop.js";
@@ -14,6 +14,12 @@ export interface AgentNativeI18nCatalog {
14
14
  sourceLocale?: LocaleCode;
15
15
  messages?: LocaleMessages;
16
16
  loadMessages?: (locale: LocaleCode) => Promise<LocaleMessages | null>;
17
+ /**
18
+ * Locales this app actually ships translations for. Defaults to every
19
+ * framework-supported locale when omitted, which only matches apps whose
20
+ * `loadMessages` covers all of them.
21
+ */
22
+ supportedLocales?: readonly LocaleCode[];
17
23
  }
18
24
  export interface AgentNativeI18nProviderProps {
19
25
  children: React.ReactNode;
@@ -31,6 +37,7 @@ interface LocaleContextValue {
31
37
  metadata: LocaleMetadata;
32
38
  setPreference: (preference: LocalePreference) => Promise<void>;
33
39
  loading: boolean;
40
+ supportedLocales: readonly LocaleCode[];
34
41
  }
35
42
  declare global {
36
43
  var __AGENT_NATIVE_LOCALE_CONTEXT__: React.Context<LocaleContextValue | null> | undefined;
@@ -155,6 +155,7 @@ export function AgentNativeI18nProvider({ children, catalog, initialLocale, init
155
155
  const sourceLocale = catalog?.sourceLocale ?? DEFAULT_LOCALE;
156
156
  const sourceMessages = catalog?.messages ?? {};
157
157
  const loadMessages = catalog?.loadMessages;
158
+ const supportedLocales = catalog?.supportedLocales ?? SUPPORTED_LOCALES;
158
159
  const hydration = readHydrationPayload();
159
160
  const initialState = useMemo(() => resolveInitialState({ initialLocale, initialPreference }), [initialLocale, initialPreference]);
160
161
  const [preference, setPreferenceState] = useState(initialState.preference);
@@ -305,7 +306,15 @@ export function AgentNativeI18nProvider({ children, catalog, initialLocale, init
305
306
  metadata: LOCALE_METADATA[locale],
306
307
  setPreference,
307
308
  loading,
308
- }), [loading, locale, preference, setPreference, sourceLocale]);
309
+ supportedLocales,
310
+ }), [
311
+ loading,
312
+ locale,
313
+ preference,
314
+ setPreference,
315
+ sourceLocale,
316
+ supportedLocales,
317
+ ]);
309
318
  return (_jsx(LocaleContext.Provider, { value: context, children: _jsx(I18nextProvider, { i18n: i18n, children: children }) }));
310
319
  }
311
320
  export function useLocale() {
@@ -472,7 +481,7 @@ export function useFormatters() {
472
481
  }), [locale]);
473
482
  }
474
483
  export function LanguagePicker({ className, includeSystem = true, label, variant = "select", }) {
475
- const { locale, preference, setPreference } = useLocale();
484
+ const { locale, preference, setPreference, supportedLocales } = useLocale();
476
485
  const [open, setOpen] = useState(false);
477
486
  const copy = LANGUAGE_PICKER_COPY[locale] ?? LANGUAGE_PICKER_COPY[DEFAULT_LOCALE];
478
487
  const resolvedLabel = label ?? copy.label;
@@ -486,7 +495,7 @@ export function LanguagePicker({ className, includeSystem = true, label, variant
486
495
  },
487
496
  ]
488
497
  : []),
489
- ...SUPPORTED_LOCALES.map((code) => ({
498
+ ...supportedLocales.map((code) => ({
490
499
  value: code,
491
500
  label: `${LOCALE_METADATA[code].nativeName} (${code})`,
492
501
  description: code,
@@ -10,5 +10,5 @@ export interface IntegrationStatus {
10
10
  export declare function useIntegrationStatus(): {
11
11
  statuses: IntegrationStatus[];
12
12
  loading: boolean;
13
- refetch: () => Promise<void>;
13
+ refetch: (signal?: AbortSignal) => Promise<void>;
14
14
  };
@@ -1,37 +1,24 @@
1
- import { useState, useEffect, useCallback, useRef } from "react";
1
+ import { useState, useCallback } from "react";
2
2
  import { agentNativePath } from "../api-path.js";
3
+ import { usePollLoop } from "../use-poll-loop.js";
3
4
  export function useIntegrationStatus() {
4
5
  const [statuses, setStatuses] = useState([]);
5
6
  const [loading, setLoading] = useState(true);
6
- const mountedRef = useRef(true);
7
- const fetchStatuses = useCallback(async () => {
7
+ const fetchStatuses = useCallback(async (signal) => {
8
8
  try {
9
- const res = await fetch(agentNativePath("/_agent-native/integrations/status"));
9
+ const res = await fetch(agentNativePath("/_agent-native/integrations/status"), { signal });
10
10
  if (!res.ok) {
11
- if (mountedRef.current)
12
- setLoading(false);
11
+ setLoading(false);
13
12
  return;
14
13
  }
15
14
  const data = await res.json();
16
- if (mountedRef.current) {
17
- setStatuses(Array.isArray(data) ? data : []);
18
- setLoading(false);
19
- }
15
+ setStatuses(Array.isArray(data) ? data : []);
16
+ setLoading(false);
20
17
  }
21
18
  catch {
22
- if (mountedRef.current) {
23
- setLoading(false);
24
- }
19
+ setLoading(false);
25
20
  }
26
21
  }, []);
27
- useEffect(() => {
28
- mountedRef.current = true;
29
- fetchStatuses();
30
- const interval = setInterval(fetchStatuses, 30000);
31
- return () => {
32
- mountedRef.current = false;
33
- clearInterval(interval);
34
- };
35
- }, [fetchStatuses]);
22
+ usePollLoop(fetchStatuses, { intervalMs: 30_000 });
36
23
  return { statuses, loading, refetch: fetchStatuses };
37
24
  }
@@ -3,7 +3,7 @@ import { IconBell, IconBellRinging, IconLoader2, IconX, } from "@tabler/icons-re
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { agentNativePath, appPath } from "../api-path.js";
5
5
  import { Popover, PopoverContent, PopoverTrigger, } from "../components/ui/popover.js";
6
- import { usePausingInterval } from "../use-pausing-interval.js";
6
+ import { usePollLoop } from "../use-poll-loop.js";
7
7
  const POLL_MS_DEFAULT = 10_000;
8
8
  const SUPPORTS_NOTIFICATION = typeof window !== "undefined" && "Notification" in window;
9
9
  /**
@@ -47,10 +47,10 @@ export function NotificationsBell({ pollMs = POLL_MS_DEFAULT, className, browser
47
47
  // the popup loop — no second /count request), and pop Notification() for
48
48
  // any new ids. When off, we fetch just /count. The unread-list branch also
49
49
  // opts out of visibility pause so popups still fire for backgrounded tabs.
50
- const refresh = useCallback(async () => {
50
+ const refresh = useCallback(async (signal) => {
51
51
  if (browserNotifications) {
52
52
  try {
53
- const res = await fetch(agentNativePath("/_agent-native/notifications?unread=true&limit=20"));
53
+ const res = await fetch(agentNativePath("/_agent-native/notifications?unread=true&limit=20"), { signal });
54
54
  if (!res.ok)
55
55
  return;
56
56
  const rows = (await res.json());
@@ -74,19 +74,21 @@ export function NotificationsBell({ pollMs = POLL_MS_DEFAULT, className, browser
74
74
  new Notification(n.title, { body: n.body, tag: n.id });
75
75
  }
76
76
  catch {
77
- // Safari / restricted contexts may throw even when permission
78
- // claims to be granted — silent no-op.
77
+ // coercion-ok: Safari / restricted contexts may throw even when
78
+ // permission claims to be granted; one failed OS notification
79
+ // must not abort the unread sweep around it.
79
80
  }
80
81
  }
81
82
  seenIdsRef.current = seen;
82
83
  }
83
84
  catch {
84
- // best-effort
85
+ // coercion-ok: the bell renders from the rows it already has; a
86
+ // failed sweep is retried by the next poll tick.
85
87
  }
86
88
  return;
87
89
  }
88
90
  try {
89
- const res = await fetch(agentNativePath("/_agent-native/notifications/count"));
91
+ const res = await fetch(agentNativePath("/_agent-native/notifications/count"), { signal });
90
92
  if (!res.ok)
91
93
  return;
92
94
  const data = (await res.json());
@@ -96,8 +98,11 @@ export function NotificationsBell({ pollMs = POLL_MS_DEFAULT, className, browser
96
98
  // best-effort
97
99
  }
98
100
  }, [browserNotifications]);
99
- usePausingInterval(refresh, pollMs,
100
- /* pauseWhenHidden */ !browserNotifications);
101
+ usePollLoop(refresh, {
102
+ intervalMs: pollMs,
103
+ enabled: pollMs > 0,
104
+ pauseWhenHidden: !browserNotifications,
105
+ });
101
106
  useEffect(() => {
102
107
  if (!open)
103
108
  return;
@@ -6,7 +6,7 @@ import { agentNativePath } from "../api-path.js";
6
6
  import { DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from "../components/ui/dropdown-menu.js";
7
7
  import { useFormatters, useT } from "../i18n.js";
8
8
  import { useChangeVersion } from "../use-change-version.js";
9
- import { usePausingInterval } from "../use-pausing-interval.js";
9
+ import { usePollLoop } from "../use-poll-loop.js";
10
10
  import { cn } from "../utils.js";
11
11
  const RUN_CHANGE_SETTLE_MS = 250;
12
12
  function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showRecent, }) {
@@ -14,19 +14,20 @@ function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showR
14
14
  const [runs, setRuns] = useState([]);
15
15
  const includeRecent = showRecent ?? !hideWhenIdle;
16
16
  const runsVersion = useChangeVersion("runs");
17
- const refresh = useCallback(async () => {
17
+ const refresh = useCallback(async (signal) => {
18
18
  try {
19
19
  const query = new URLSearchParams({ limit: String(limit) });
20
20
  if (!includeRecent)
21
21
  query.set("active", "true");
22
- const res = await fetch(agentNativePath(`/_agent-native/runs?${query.toString()}`));
22
+ const res = await fetch(agentNativePath(`/_agent-native/runs?${query.toString()}`), { signal });
23
23
  if (!res.ok)
24
24
  return;
25
25
  const rows = (await res.json());
26
26
  setRuns(rows);
27
27
  }
28
28
  catch {
29
- // best-effort
29
+ // coercion-ok: `runs` keeps its last good value rather than being
30
+ // cleared to an empty tray; the next poll tick retries.
30
31
  }
31
32
  }, [includeRecent, limit]);
32
33
  useEffect(() => {
@@ -38,7 +39,7 @@ function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showR
38
39
  const timeout = window.setTimeout(() => void refresh(), RUN_CHANGE_SETTLE_MS);
39
40
  return () => window.clearTimeout(timeout);
40
41
  }, [refresh, runsVersion]);
41
- usePausingInterval(refresh, pollMs);
42
+ usePollLoop(refresh, { intervalMs: pollMs, enabled: pollMs > 0 });
42
43
  const dismissRun = useCallback(async (runId) => {
43
44
  setRuns((current) => current.filter((run) => run.id !== runId));
44
45
  try {