@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
@@ -17,6 +17,7 @@ import { callAction } from "./use-action.js";
17
17
  import { useChangeVersion } from "./use-change-version.js";
18
18
  import { CHAT_MODEL_SELECTION_CHANGED_EVENT } from "./use-chat-models.js";
19
19
  import { useChatThreads, } from "./use-chat-threads.js";
20
+ import { usePollLoop } from "./use-poll-loop.js";
20
21
  import { cn } from "./utils.js";
21
22
  /** The single path that hands a queued send to a mounted chat ref. */
22
23
  function deliverPendingSend(ref, send) {
@@ -275,6 +276,7 @@ function chatTabStatusFromAgentTeamStatus(status) {
275
276
  return isActiveAgentTeamStatus(status) ? "running" : "completed";
276
277
  }
277
278
  const STALE_THREAD_THRESHOLD_MS = 12 * 60 * 60 * 1000;
279
+ const DEFAULT_AGENT_TEAM_POLL_MS = 3000;
278
280
  const DEFAULT_THREAD_URL_PARAM = "thread";
279
281
  const THREAD_URL_CHANGED_EVENT = "agent-chat:url-thread-changed";
280
282
  const hasOwn = Object.prototype.hasOwnProperty;
@@ -373,7 +375,7 @@ function runToAgentTeamTabInfo(run) {
373
375
  status: run.status ?? "unknown",
374
376
  };
375
377
  }
376
- export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderOverlay, contentHidden = false, apiUrl = agentNativePath("/_agent-native/agent-chat"), storageKey, restoreActiveThread = true, browserTabId, threadUrlSync = false, scope = null, ...props }) {
378
+ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderOverlay, contentHidden = false, apiUrl = agentNativePath("/_agent-native/agent-chat"), storageKey, restoreActiveThread = true, browserTabId, threadUrlSync = false, scope = null, agentTeamPollMs = DEFAULT_AGENT_TEAM_POLL_MS, ...props }) {
377
379
  const { enabled: threadUrlSyncEnabled, paramName: threadUrlParamName, routeThreadId, getPath: getThreadPath, navigate: navigateThreadUrl, } = resolveThreadUrlSync(threadUrlSync);
378
380
  const threadRouteControlsActiveThread = threadUrlSyncEnabled &&
379
381
  threadUrlSync !== true &&
@@ -896,107 +898,92 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
896
898
  });
897
899
  }
898
900
  }, [isLoading, openTabIds, activeThreadId, createThread, writeThreadUrl]);
899
- useEffect(() => {
900
- let stopped = false;
901
- let timer = null;
901
+ usePollLoop(async (signal) => {
902
902
  const runsUrl = `${apiUrl.replace(/\/$/, "")}/runs/list?goalId=agent-team`;
903
- async function hydrateAgentTeamTabs() {
904
- try {
905
- const res = await fetch(runsUrl);
906
- if (res.ok) {
907
- const data = (await res.json());
908
- const infos = Array.isArray(data.runs)
909
- ? data.runs
910
- .map(runToAgentTeamTabInfo)
911
- .filter((info) => Boolean(info))
912
- : [];
913
- setSubAgentStatuses((prev) => {
914
- let changed = false;
915
- const next = {};
916
- for (const info of infos) {
917
- next[info.threadId] = info.status;
918
- if (prev[info.threadId] !== info.status)
919
- changed = true;
920
- }
921
- if (Object.keys(prev).length !== Object.keys(next).length) {
903
+ try {
904
+ const res = await fetch(runsUrl, { signal });
905
+ if (res.ok) {
906
+ const data = (await res.json());
907
+ const infos = Array.isArray(data.runs)
908
+ ? data.runs
909
+ .map(runToAgentTeamTabInfo)
910
+ .filter((info) => Boolean(info))
911
+ : [];
912
+ setSubAgentStatuses((prev) => {
913
+ let changed = false;
914
+ const next = {};
915
+ for (const info of infos) {
916
+ next[info.threadId] = info.status;
917
+ if (prev[info.threadId] !== info.status)
922
918
  changed = true;
919
+ }
920
+ if (Object.keys(prev).length !== Object.keys(next).length) {
921
+ changed = true;
922
+ }
923
+ return changed ? next : prev;
924
+ });
925
+ const openSet = new Set(openTabIdsRef.current);
926
+ const candidates = infos.filter((info) => !dismissedSubAgentTabsRef.current.has(info.threadId) &&
927
+ (openSet.has(info.parentThreadId) || openSet.has(info.threadId)));
928
+ if (candidates.length > 0) {
929
+ const shouldRefreshThreads = candidates.some((info) => !openSet.has(info.threadId) && openSet.has(info.parentThreadId));
930
+ const candidateParents = new Map(candidates.map((info) => [info.threadId, info.parentThreadId]));
931
+ setParentMap((prev) => {
932
+ let next = prev;
933
+ for (const info of candidates) {
934
+ if (next[info.threadId] === info.parentThreadId)
935
+ continue;
936
+ next =
937
+ next === prev
938
+ ? { ...prev, [info.threadId]: info.parentThreadId }
939
+ : { ...next, [info.threadId]: info.parentThreadId };
923
940
  }
924
- return changed ? next : prev;
941
+ return next;
925
942
  });
926
- const openSet = new Set(openTabIdsRef.current);
927
- const candidates = infos.filter((info) => !dismissedSubAgentTabsRef.current.has(info.threadId) &&
928
- (openSet.has(info.parentThreadId) || openSet.has(info.threadId)));
929
- if (candidates.length > 0) {
930
- const shouldRefreshThreads = candidates.some((info) => !openSet.has(info.threadId) && openSet.has(info.parentThreadId));
931
- const candidateParents = new Map(candidates.map((info) => [info.threadId, info.parentThreadId]));
932
- setParentMap((prev) => {
933
- let next = prev;
934
- for (const info of candidates) {
935
- if (next[info.threadId] === info.parentThreadId)
936
- continue;
937
- next =
938
- next === prev
939
- ? { ...prev, [info.threadId]: info.parentThreadId }
940
- : { ...next, [info.threadId]: info.parentThreadId };
941
- }
942
- return next;
943
- });
944
- setSubAgentNames((prev) => {
945
- let next = prev;
946
- for (const info of candidates) {
947
- if (!info.name || next[info.threadId] === info.name)
948
- continue;
949
- next =
950
- next === prev
951
- ? { ...prev, [info.threadId]: info.name }
952
- : { ...next, [info.threadId]: info.name };
953
- }
954
- return next;
955
- });
956
- setOpenTabIds((prev) => {
957
- let next = prev;
958
- for (const info of candidates) {
959
- if (next.includes(info.threadId))
960
- continue;
961
- const parentIdx = next.indexOf(info.parentThreadId);
962
- if (parentIdx === -1)
963
- continue;
964
- if (next === prev)
965
- next = [...prev];
966
- let insertIdx = parentIdx + 1;
967
- while (insertIdx < next.length) {
968
- const siblingParent = parentMapRef.current[next[insertIdx]] ||
969
- candidateParents.get(next[insertIdx]);
970
- if (siblingParent !== info.parentThreadId)
971
- break;
972
- insertIdx++;
973
- }
974
- next.splice(insertIdx, 0, info.threadId);
943
+ setSubAgentNames((prev) => {
944
+ let next = prev;
945
+ for (const info of candidates) {
946
+ if (!info.name || next[info.threadId] === info.name)
947
+ continue;
948
+ next =
949
+ next === prev
950
+ ? { ...prev, [info.threadId]: info.name }
951
+ : { ...next, [info.threadId]: info.name };
952
+ }
953
+ return next;
954
+ });
955
+ setOpenTabIds((prev) => {
956
+ let next = prev;
957
+ for (const info of candidates) {
958
+ if (next.includes(info.threadId))
959
+ continue;
960
+ const parentIdx = next.indexOf(info.parentThreadId);
961
+ if (parentIdx === -1)
962
+ continue;
963
+ if (next === prev)
964
+ next = [...prev];
965
+ let insertIdx = parentIdx + 1;
966
+ while (insertIdx < next.length) {
967
+ const siblingParent = parentMapRef.current[next[insertIdx]] ||
968
+ candidateParents.get(next[insertIdx]);
969
+ if (siblingParent !== info.parentThreadId)
970
+ break;
971
+ insertIdx++;
975
972
  }
976
- return next;
977
- });
978
- if (shouldRefreshThreads) {
979
- refreshThreads();
973
+ next.splice(insertIdx, 0, info.threadId);
980
974
  }
975
+ return next;
976
+ });
977
+ if (shouldRefreshThreads) {
978
+ refreshThreads();
981
979
  }
982
980
  }
983
981
  }
984
- catch {
985
- // Best effort: task cards and manual history still work if this poll fails.
986
- }
987
- finally {
988
- if (!stopped) {
989
- timer = setTimeout(hydrateAgentTeamTabs, 3000);
990
- }
991
- }
992
982
  }
993
- hydrateAgentTeamTabs();
994
- return () => {
995
- stopped = true;
996
- if (timer)
997
- clearTimeout(timer);
998
- };
999
- }, [apiUrl, refreshThreads]);
983
+ catch {
984
+ // Best effort: task cards and manual history still work if this poll fails.
985
+ }
986
+ }, { intervalMs: agentTeamPollMs, pauseWhenHidden: true });
1000
987
  // Focus the composer when switching tabs
1001
988
  useEffect(() => {
1002
989
  if (!activeThreadId)
@@ -1,11 +1,27 @@
1
+ import { createPollEngine } from "../shared/poll-engine.js";
1
2
  import { agentNativePath } from "./api-path.js";
2
3
  import { announceAgentNativeFrameReady, defaultAgentNativeHostCommands, requestAgentNativeHostActions, requestAgentNativeHostContext, runAgentNativeHostAction, sendAgentNativeHostCommand, } from "./host-bridge.js";
3
4
  const DEFAULT_ENDPOINT = "/_agent-native/browser-sessions";
4
5
  const DEFAULT_HEARTBEAT_MS = 5_000;
5
6
  const DEFAULT_POLL_MS = 500;
7
+ const REQUEST_ABORT_MIN_MS = 10_000;
8
+ // Relaxed cadence floor applied while the tab is hidden — mirrors
9
+ // use-poll-loop.ts's `pauseWhenHidden: false` default so a backgrounded tab
10
+ // keeps heartbeating (the server-side session TTL still needs refreshing)
11
+ // instead of going fully silent.
12
+ const HIDDEN_INTERVAL_FLOOR_MS = 10_000;
13
+ function isDocumentHidden() {
14
+ return (typeof document !== "undefined" && document.visibilityState === "hidden");
15
+ }
6
16
  function browserSessionId() {
7
17
  return `browser-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
8
18
  }
19
+ // Bounds a single request so a hung fetch can't leave the caller waiting
20
+ // forever on a wedged connection.
21
+ function requestAbortMs(options) {
22
+ const cadence = Math.min(options.pollMs ?? DEFAULT_POLL_MS, options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
23
+ return Math.max(REQUEST_ABORT_MIN_MS, cadence * 4);
24
+ }
9
25
  function messageError(error) {
10
26
  return error instanceof Error ? error : new Error(String(error));
11
27
  }
@@ -35,26 +51,48 @@ async function readJsonResponse(response) {
35
51
  return body;
36
52
  }
37
53
  async function postJson(options, path, body) {
38
- const response = await fetchImpl(options)(endpointPath(options, path), {
39
- method: "POST",
40
- credentials: "include",
41
- headers: {
42
- "Content-Type": "application/json",
43
- "X-Agent-Native-CSRF": "1",
44
- },
45
- body: JSON.stringify(body ?? {}),
46
- });
47
- return readJsonResponse(response);
54
+ const controller = typeof AbortController === "undefined" ? null : new AbortController();
55
+ const timeoutId = controller
56
+ ? setTimeout(() => controller.abort(), requestAbortMs(options))
57
+ : null;
58
+ try {
59
+ const response = await fetchImpl(options)(endpointPath(options, path), {
60
+ method: "POST",
61
+ credentials: "include",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ "X-Agent-Native-CSRF": "1",
65
+ },
66
+ body: JSON.stringify(body ?? {}),
67
+ ...(controller ? { signal: controller.signal } : {}),
68
+ });
69
+ return readJsonResponse(response);
70
+ }
71
+ finally {
72
+ if (timeoutId)
73
+ clearTimeout(timeoutId);
74
+ }
48
75
  }
49
76
  async function deleteJson(options, path) {
50
- const response = await fetchImpl(options)(endpointPath(options, path), {
51
- method: "DELETE",
52
- credentials: "include",
53
- headers: {
54
- "X-Agent-Native-CSRF": "1",
55
- },
56
- });
57
- await readJsonResponse(response);
77
+ const controller = typeof AbortController === "undefined" ? null : new AbortController();
78
+ const timeoutId = controller
79
+ ? setTimeout(() => controller.abort(), requestAbortMs(options))
80
+ : null;
81
+ try {
82
+ const response = await fetchImpl(options)(endpointPath(options, path), {
83
+ method: "DELETE",
84
+ credentials: "include",
85
+ headers: {
86
+ "X-Agent-Native-CSRF": "1",
87
+ },
88
+ ...(controller ? { signal: controller.signal } : {}),
89
+ });
90
+ await readJsonResponse(response);
91
+ }
92
+ finally {
93
+ if (timeoutId)
94
+ clearTimeout(timeoutId);
95
+ }
58
96
  }
59
97
  function hostRequestOptions(options) {
60
98
  const { endpoint: _endpoint, sessionId: _sessionId, session: _session, getContext: _getContext, actions: _actions, commands: _commands, origin: _origin, label: _label, heartbeatMs: _heartbeatMs, pollMs: _pollMs, ttlMs: _ttlMs, fetch: _fetch, ...hostOptions } = options;
@@ -224,10 +262,7 @@ export function createAgentNativeBrowserSessionBridge(options = {}) {
224
262
  let currentSessionId = options.sessionId ?? null;
225
263
  let fallbackSessionId = null;
226
264
  let started = false;
227
- let heartbeatTimer;
228
- let pollTimer;
229
- let refreshing = false;
230
- let polling = false;
265
+ let onVisibility;
231
266
  async function refreshRegistration() {
232
267
  const direct = hasDirectHost(options);
233
268
  const hostOptions = hostRequestOptions(options);
@@ -256,17 +291,6 @@ export function createAgentNativeBrowserSessionBridge(options = {}) {
256
291
  });
257
292
  return body.session;
258
293
  }
259
- async function heartbeat() {
260
- if (refreshing)
261
- return;
262
- refreshing = true;
263
- try {
264
- await refreshRegistration();
265
- }
266
- finally {
267
- refreshing = false;
268
- }
269
- }
270
294
  async function claimOnce() {
271
295
  if (!currentSessionId) {
272
296
  await refreshRegistration();
@@ -286,17 +310,27 @@ export function createAgentNativeBrowserSessionBridge(options = {}) {
286
310
  }
287
311
  return request;
288
312
  }
289
- async function poll() {
290
- if (polling)
291
- return;
292
- polling = true;
293
- try {
294
- await claimOnce();
295
- }
296
- finally {
297
- polling = false;
298
- }
299
- }
313
+ // This is a plain factory function, not a React component, so it can't use
314
+ // the usePollLoop hook — createPollEngine gives each loop the same
315
+ // never-overlaps/never-stalls guarantees imperatively. The relaxed hidden
316
+ // cadence + visibilitychange wiring below mirrors what use-poll-loop.ts
317
+ // does internally for `pauseWhenHidden: false`.
318
+ const heartbeatEngine = createPollEngine(() => refreshRegistration().then(() => { }), {
319
+ intervalMs: () => {
320
+ const base = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
321
+ return isDocumentHidden()
322
+ ? Math.max(base, HIDDEN_INTERVAL_FLOOR_MS)
323
+ : base;
324
+ },
325
+ });
326
+ const pollEngine = createPollEngine(() => claimOnce().then(() => { }), {
327
+ intervalMs: () => {
328
+ const base = options.pollMs ?? DEFAULT_POLL_MS;
329
+ return isDocumentHidden()
330
+ ? Math.max(base, HIDDEN_INTERVAL_FLOOR_MS)
331
+ : base;
332
+ },
333
+ });
300
334
  const bridge = {
301
335
  get sessionId() {
302
336
  return currentSessionId;
@@ -308,22 +342,31 @@ export function createAgentNativeBrowserSessionBridge(options = {}) {
308
342
  if (!hasDirectHost(options)) {
309
343
  announceAgentNativeFrameReady(hostRequestOptions(options));
310
344
  }
311
- void heartbeat();
312
- void poll();
313
- heartbeatTimer = setInterval(() => void heartbeat(), options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
314
- pollTimer = setInterval(() => void poll(), options.pollMs ?? DEFAULT_POLL_MS);
345
+ heartbeatEngine.start();
346
+ pollEngine.start();
347
+ onVisibility = () => {
348
+ if (isDocumentHidden()) {
349
+ heartbeatEngine.reschedule();
350
+ pollEngine.reschedule();
351
+ }
352
+ else {
353
+ heartbeatEngine.pollNow();
354
+ pollEngine.pollNow();
355
+ }
356
+ };
357
+ document.addEventListener("visibilitychange", onVisibility);
315
358
  return bridge;
316
359
  },
317
360
  stop() {
318
361
  if (!started)
319
362
  return;
320
363
  started = false;
321
- if (heartbeatTimer)
322
- clearInterval(heartbeatTimer);
323
- if (pollTimer)
324
- clearInterval(pollTimer);
325
- heartbeatTimer = undefined;
326
- pollTimer = undefined;
364
+ heartbeatEngine.stop();
365
+ pollEngine.stop();
366
+ if (onVisibility) {
367
+ document.removeEventListener("visibilitychange", onVisibility);
368
+ onVisibility = undefined;
369
+ }
327
370
  if (currentSessionId) {
328
371
  void deleteJson(options, `/${encodePathSegment(currentSessionId)}`).catch(() => { });
329
372
  }
@@ -16,6 +16,7 @@ import { useEffect, useRef } from "react";
16
16
  import { removeAgentChatContextItem, setAgentChatContextItem, } from "../agent-chat.js";
17
17
  import { agentNativePath } from "../api-path.js";
18
18
  import { deleteClientAppState, readClientAppState, setClientAppState, } from "../application-state.js";
19
+ import { usePollLoop } from "../use-poll-loop.js";
19
20
  const NAVIGATION_PATH = agentNativePath("/_agent-native/application-state/navigation");
20
21
  const NAVIGATE_PATH = agentNativePath("/_agent-native/application-state/navigate");
21
22
  const POLL_INTERVAL_MS = 1500;
@@ -137,50 +138,40 @@ export function useDbAdminAgentSync({ table, mode, enabled = true, }) {
137
138
  export function useNavigateConsumer(onNavigate, enabled = true) {
138
139
  const handlerRef = useRef(onNavigate);
139
140
  handlerRef.current = onNavigate;
141
+ const mountedRef = useRef(true);
140
142
  useEffect(() => {
141
- if (!enabled)
142
- return;
143
- let active = true;
144
- let timer;
145
- const tick = async () => {
146
- if (!active)
147
- return;
148
- try {
149
- const res = await fetch(NAVIGATE_PATH, {
150
- method: "GET",
151
- credentials: "include",
152
- headers: headers(),
153
- });
154
- if (active && res.ok) {
155
- const data = (await res.json());
156
- if (data &&
157
- data.view === "database" &&
158
- typeof data.table === "string" &&
159
- data.table) {
160
- const target = data.table;
161
- // Clear the one-shot command before acting so it fires once.
162
- fetch(NAVIGATE_PATH, {
163
- method: "DELETE",
164
- credentials: "include",
165
- headers: headers({ "X-Agent-Native-CSRF": "1" }),
166
- }).catch(() => { });
167
- handlerRef.current(target);
168
- }
169
- }
170
- }
171
- catch {
172
- // Ignore transient errors; the next tick retries.
173
- }
174
- finally {
175
- if (active)
176
- timer = setTimeout(tick, POLL_INTERVAL_MS);
177
- }
178
- };
179
- timer = setTimeout(tick, POLL_INTERVAL_MS);
143
+ mountedRef.current = true;
180
144
  return () => {
181
- active = false;
182
- if (timer)
183
- clearTimeout(timer);
145
+ mountedRef.current = false;
184
146
  };
185
- }, [enabled]);
147
+ }, []);
148
+ usePollLoop(async (signal) => {
149
+ const res = await fetch(NAVIGATE_PATH, {
150
+ method: "GET",
151
+ credentials: "include",
152
+ headers: headers(),
153
+ signal,
154
+ });
155
+ if (!mountedRef.current || !res.ok)
156
+ return;
157
+ const data = (await res.json());
158
+ if (data &&
159
+ data.view === "database" &&
160
+ typeof data.table === "string" &&
161
+ data.table) {
162
+ const target = data.table;
163
+ // Clear the one-shot command before acting so it fires once.
164
+ fetch(NAVIGATE_PATH, {
165
+ method: "DELETE",
166
+ credentials: "include",
167
+ headers: headers({ "X-Agent-Native-CSRF": "1" }),
168
+ }).catch(() => { });
169
+ handlerRef.current(target);
170
+ }
171
+ }, {
172
+ intervalMs: POLL_INTERVAL_MS,
173
+ leading: false,
174
+ pauseWhenHidden: true,
175
+ enabled,
176
+ });
186
177
  }
@@ -15,6 +15,8 @@ export interface AgentDynamicSuggestionsConfig {
15
15
  includeStatic?: boolean;
16
16
  /** Optional app-specific deterministic suggestion builder. */
17
17
  getSuggestions?: (context: AgentDynamicSuggestionContext) => string[];
18
+ /** Safety-net refresh cadence in ms, for updates that don't bump app-state version. Default 30_000. */
19
+ pollMs?: number;
18
20
  }
19
21
  export type AgentDynamicSuggestionsOption = boolean | AgentDynamicSuggestionsConfig;
20
22
  export interface AgentDynamicSuggestionsResult {
@@ -33,6 +35,7 @@ interface NormalizedAgentDynamicSuggestionsConfig {
33
35
  max: number;
34
36
  includeStatic: boolean;
35
37
  getSuggestions?: (context: AgentDynamicSuggestionContext) => string[];
38
+ pollMs: number;
36
39
  }
37
40
  export declare function normalizeAgentDynamicSuggestionsConfig(option?: AgentDynamicSuggestionsOption): NormalizedAgentDynamicSuggestionsConfig;
38
41
  export declare function buildDynamicAgentSuggestions(context: AgentDynamicSuggestionContext): string[];