@agent-native/core 0.144.2 → 0.145.0

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 (104) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -4
  3. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +7 -3
  4. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +13 -3
  5. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +64 -23
  6. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +69 -46
  7. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +35 -6
  8. package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +24 -3
  9. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +45 -6
  10. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +38 -5
  11. package/corpus/templates/clips/changelog/2026-08-06-desktop-full-screen-recordings-now-normalize-microphone-leve.md +6 -0
  12. package/corpus/templates/clips/desktop/src/app.tsx +146 -45
  13. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +185 -69
  14. package/corpus/templates/clips/server/jobs/brain-export.ts +13 -6
  15. package/corpus/templates/clips/server/jobs/builder-media-compression.ts +10 -3
  16. package/corpus/templates/clips/server/jobs/meeting-reminders.ts +10 -3
  17. package/corpus/templates/clips/server/jobs/poll-calendars.ts +23 -21
  18. package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +10 -3
  19. package/corpus/templates/clips/server/jobs/transactional-emails.ts +10 -3
  20. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
  21. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +15 -2
  22. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +47 -4
  23. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  24. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +82 -39
  25. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +31 -9
  26. package/corpus/templates/mail/server/plugins/mail-jobs.ts +36 -17
  27. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  28. package/dist/agent/production-agent.d.ts +6 -0
  29. package/dist/client/AssistantChat.js +73 -43
  30. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  31. package/dist/client/MultiTabAssistantChat.js +79 -92
  32. package/dist/client/browser-session-bridge.js +97 -54
  33. package/dist/client/db-admin/useAgentSync.js +34 -43
  34. package/dist/client/dynamic-suggestions.d.ts +3 -0
  35. package/dist/client/dynamic-suggestions.js +54 -52
  36. package/dist/client/hooks/index.d.ts +1 -0
  37. package/dist/client/hooks/index.js +1 -0
  38. package/dist/client/i18n.d.ts +7 -0
  39. package/dist/client/i18n.js +12 -3
  40. package/dist/client/integrations/useIntegrationStatus.d.ts +1 -1
  41. package/dist/client/integrations/useIntegrationStatus.js +9 -22
  42. package/dist/client/notifications/NotificationsBell.js +14 -9
  43. package/dist/client/progress/RunsTray.js +6 -5
  44. package/dist/client/settings/useBuilderStatus.js +95 -73
  45. package/dist/client/transcription/BuilderTranscriptionCta.js +27 -31
  46. package/dist/client/use-agent-engine-configured.js +21 -3
  47. package/dist/client/use-pausing-interval.d.ts +6 -0
  48. package/dist/client/use-pausing-interval.js +6 -0
  49. package/dist/client/use-poll-loop.d.ts +39 -0
  50. package/dist/client/use-poll-loop.js +73 -0
  51. package/dist/client/use-run-stuck-detection.js +41 -13
  52. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  53. package/dist/framework-tools.d.ts +126 -0
  54. package/dist/framework-tools.js +114 -0
  55. package/dist/integrations/google-docs-poller.js +26 -7
  56. package/dist/integrations/pending-tasks-retry-job.js +17 -23
  57. package/dist/integrations/plugin.js +16 -15
  58. package/dist/integrations/remote-push-delivery-job.js +16 -25
  59. package/dist/integrations/remote-retry-job.js +19 -23
  60. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  61. package/dist/notifications/routes.d.ts +3 -3
  62. package/dist/oauth-tokens/google-refresh.js +12 -19
  63. package/dist/observability/routes.d.ts +3 -3
  64. package/dist/progress/routes.d.ts +1 -1
  65. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  66. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  67. package/dist/resources/handlers.d.ts +1 -1
  68. package/dist/secrets/routes.d.ts +3 -3
  69. package/dist/server/action-discovery.d.ts +27 -0
  70. package/dist/server/action-discovery.js +89 -0
  71. package/dist/server/agent-chat/action-filters-a2a.d.ts +11 -0
  72. package/dist/server/agent-chat/action-filters-a2a.js +17 -1
  73. package/dist/server/agent-chat/framework-prompts.d.ts +2 -0
  74. package/dist/server/agent-chat/plugin-options.d.ts +30 -0
  75. package/dist/server/agent-chat/prompt-resources.d.ts +4 -1
  76. package/dist/server/agent-chat/prompt-resources.js +8 -2
  77. package/dist/server/agent-chat-plugin.js +148 -100
  78. package/dist/server/index.d.ts +1 -0
  79. package/dist/server/index.js +1 -0
  80. package/dist/server/interval-job.d.ts +25 -0
  81. package/dist/server/interval-job.js +19 -0
  82. package/dist/server/prompts/framework-core-compact.d.ts +5 -0
  83. package/dist/server/prompts/framework-core-compact.js +28 -12
  84. package/dist/server/prompts/framework-core.d.ts +5 -0
  85. package/dist/server/prompts/framework-core.js +33 -16
  86. package/dist/server/prompts/index.d.ts +1 -1
  87. package/dist/server/prompts/index.js +1 -1
  88. package/dist/server/prompts/shared-rules.d.ts +12 -4
  89. package/dist/server/prompts/shared-rules.js +14 -4
  90. package/dist/server/realtime-token.d.ts +1 -1
  91. package/dist/shared/index.d.ts +1 -0
  92. package/dist/shared/index.js +1 -0
  93. package/dist/shared/poll-engine.d.ts +49 -0
  94. package/dist/shared/poll-engine.js +148 -0
  95. package/dist/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  96. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
  97. package/dist/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
  98. package/dist/tracking/providers.js +10 -1
  99. package/docs/content/deployment.mdx +47 -13
  100. package/docs/content/key-concepts.mdx +13 -6
  101. package/package.json +2 -1
  102. package/src/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  103. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
  104. package/src/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
@@ -1,5 +1,6 @@
1
1
  import { registerEvent } from "@agent-native/core/event-bus";
2
2
  import { listOAuthAccounts } from "@agent-native/core/oauth-tokens";
3
+ import { startIntervalJob } from "@agent-native/core/server/interval-job";
3
4
  import { z } from "zod";
4
5
 
5
6
  import { processAutomations } from "../lib/automation-engine.js";
@@ -18,6 +19,10 @@ import {
18
19
 
19
20
  const INTERVAL_MS = 60_000; // 1 minute
20
21
  const WATCH_RENEW_INTERVAL_MS = 12 * 60 * 60_000;
22
+ // Backstop for the whole tick (job sends + Gmail watch renewal, both outbound
23
+ // network calls with no timeout of their own), reported through the job's
24
+ // onError so a wedged tick is loud rather than silent.
25
+ const TICK_ABORT_MS = Math.max(10_000, INTERVAL_MS * 4);
21
26
  let lastWatchRenewalAt = 0;
22
27
  // Vite's dev server initializes Nitro plugins more than once during boot
23
28
  // (initial load + post-init). Module-scope flag ensures the "skipping" log
@@ -124,24 +129,38 @@ export default () => {
124
129
  return;
125
130
  }
126
131
 
127
- setInterval(async () => {
128
- try {
129
- await processJobs();
130
- } catch (err) {
131
- console.error("[mail-jobs] processJobs failed:", err);
132
- }
133
- try {
134
- await processAutomations();
135
- } catch (err) {
136
- console.error("[mail-jobs] processAutomations failed:", err);
137
- }
138
- if (Date.now() - lastWatchRenewalAt > WATCH_RENEW_INTERVAL_MS) {
139
- lastWatchRenewalAt = Date.now();
132
+ // The overlap guard and the hung-tick timeout both come from
133
+ // startIntervalJob rather than a module-level `running` flag here: these
134
+ // are outbound Google calls with no timeout of their own, and releasing the
135
+ // guard on the timeout while a hung call kept running is what let a tick
136
+ // overlap the next one and send duplicate mail.
137
+ startIntervalJob(
138
+ async () => {
140
139
  try {
141
- await renewAllWatches();
140
+ await processJobs();
142
141
  } catch (err) {
143
- console.error("[mail-jobs] renewAllWatches failed:", err);
142
+ console.error("[mail-jobs] processJobs failed:", err);
144
143
  }
145
- }
146
- }, INTERVAL_MS);
144
+ try {
145
+ await processAutomations();
146
+ } catch (err) {
147
+ console.error("[mail-jobs] processAutomations failed:", err);
148
+ }
149
+ if (Date.now() - lastWatchRenewalAt > WATCH_RENEW_INTERVAL_MS) {
150
+ lastWatchRenewalAt = Date.now();
151
+ try {
152
+ await renewAllWatches();
153
+ } catch (err) {
154
+ console.error("[mail-jobs] renewAllWatches failed:", err);
155
+ }
156
+ }
157
+ },
158
+ {
159
+ intervalMs: INTERVAL_MS,
160
+ timeoutMs: TICK_ABORT_MS,
161
+ leading: false,
162
+ onError: (err) =>
163
+ console.error("[mail-jobs] tick exceeded time budget:", err),
164
+ },
165
+ );
147
166
  };
@@ -43,7 +43,7 @@ export default createAgentChatPlugin({
43
43
  // Customer and product activity data belongs to Analytics. Keep raw DB
44
44
  // tools out of both the interactive and A2A Slides agent surfaces so the
45
45
  // agent cannot bypass the Analytics data dictionary with local SQL.
46
- databaseTools: "off",
46
+ frameworkTools: { database: "off" },
47
47
  // Enable sandboxed JavaScript execution so Slides agents can fetch,
48
48
  // paginate, and reduce provider data through providerFetch() without us
49
49
  // hardcoding one action per Google Drive endpoint.
@@ -196,6 +196,12 @@ export interface ActionEntry {
196
196
  * `needsApproval` option. See `packages/core/docs/content/actions.mdx`.
197
197
  */
198
198
  needsApproval?: boolean | ((args: any, ctx?: import("../action.js").ActionRunContext) => boolean | Promise<boolean>);
199
+ /** Which framework tool group contributed this action. Set by the framework,
200
+ * never by an app: apps own their action names, and a tagged action is one
201
+ * the app can switch off wholesale through `frameworkTools`. Tagged actions
202
+ * are also excluded from the DEFAULT first-request tool list — they stay
203
+ * reachable through `tool-search` unless named in `initialToolNames`. */
204
+ frameworkGroup?: import("../framework-tools.js").FrameworkToolGroup;
199
205
  }
200
206
  /** @deprecated Use `ActionEntry` instead */
201
207
  export type ScriptEntry = ActionEntry;
@@ -7,6 +7,7 @@ import { AssistantRuntimeProvider, useLocalRuntime, useThreadRuntime, useThread,
7
7
  import { CompositeAttachmentAdapter } from "@assistant-ui/react";
8
8
  import { IconArrowUp, IconClock, IconMessage, IconX, IconPlayerStopFilled, IconTerminal, IconAlertTriangle, IconChevronDown, IconRefresh, } from "@tabler/icons-react";
9
9
  import { useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect, forwardRef, useImperativeHandle, } from "react";
10
+ import { createPollEngine } from "../shared/poll-engine.js";
10
11
  import { clearPendingTurnIfMatches, clearActiveRunIfMatches, getPendingTurn, getActiveRunActivityTool, getActiveRun, resolveReconnectAfterSeq, setActiveRun, updateActiveRunActivity, updateActiveRunSeq, } from "./active-run-state.js";
11
12
  import { activeRunLooksAlive, createAgentChatAdapter, } from "./agent-chat-adapter.js";
12
13
  import { appendAgentChatContextToMessage, formatAgentChatContextItemsForPrompt, getAgentChatContextState, isAgentChatSubmitCancelled, normalizeAgentChatContextItem, publishAgentChatContextItems, refreshAgentChatContext, reportAgentChatSubmitResult, subscribeAgentChatContext, } from "./agent-chat.js";
@@ -98,6 +99,13 @@ export function createUserMessageRunConfig(references, requestMode, recoveryActi
98
99
  return options;
99
100
  }
100
101
  const PENDING_SELECTION_KEY = "pending-selection-context";
102
+ // Bounds an in-flight poll fetch so its own boolean in-flight guard is
103
+ // guaranteed to release even if the server never responds. Mirrors
104
+ // use-db-sync.ts's getPollAbortMs.
105
+ const POLL_ABORT_MIN_MS = 10_000;
106
+ function getPollAbortMs(interval) {
107
+ return Math.max(POLL_ABORT_MIN_MS, interval * 4);
108
+ }
101
109
  const ACTIVE_RUN_CLEAR_TIMEOUT_MS = 5_000;
102
110
  const ACTIVE_RUN_STUCK_THRESHOLD_MS = 90_000;
103
111
  const BACKGROUND_ACTIVE_RUN_STUCK_THRESHOLD_MS = 13 * 60_000;
@@ -1836,7 +1844,11 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1836
1844
  }
1837
1845
  return repo;
1838
1846
  }, [threadRuntime]);
1839
- const refreshThreadFromServer = useCallback(async () => {
1847
+ // Accepts an optional external `signal` so poll loops (e.g. the reconnect
1848
+ // thread-poll engine below) can cancel this fetch via their own timeout
1849
+ // instead of racing a second, separately-constructed AbortController.
1850
+ // Callers with no signal of their own keep the internal fallback timeout.
1851
+ const refreshThreadFromServer = useCallback(async (signal) => {
1840
1852
  if (loadHistoryRepository) {
1841
1853
  try {
1842
1854
  const repo = await loadHistoryRepository();
@@ -1845,13 +1857,21 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1845
1857
  return importThreadData(repo);
1846
1858
  }
1847
1859
  catch {
1860
+ // coercion-ok: callers treat null as "keep the thread already
1861
+ // rendered", the same handling an absent repo needs.
1848
1862
  return null;
1849
1863
  }
1850
1864
  }
1851
1865
  if (!threadId)
1852
1866
  return null;
1867
+ const ownAbort = !signal && typeof AbortController !== "undefined"
1868
+ ? new AbortController()
1869
+ : null;
1870
+ const ownAbortTimer = ownAbort
1871
+ ? setTimeout(() => ownAbort.abort(), getPollAbortMs(2000))
1872
+ : null;
1853
1873
  try {
1854
- const refreshRes = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`);
1874
+ const refreshRes = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`, { signal: signal ?? ownAbort?.signal });
1855
1875
  if (!refreshRes.ok)
1856
1876
  return null;
1857
1877
  const refreshData = await refreshRes.json();
@@ -1860,8 +1880,14 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1860
1880
  return importThreadData(refreshData.threadData);
1861
1881
  }
1862
1882
  catch {
1883
+ // coercion-ok: an aborted or failed refresh keeps the thread already
1884
+ // rendered; the next poll tick retries.
1863
1885
  return null;
1864
1886
  }
1887
+ finally {
1888
+ if (ownAbortTimer)
1889
+ clearTimeout(ownAbortTimer);
1890
+ }
1865
1891
  }, [apiUrl, importThreadData, loadHistoryRepository, threadId]);
1866
1892
  const exportCleanThreadRepo = useCallback(() => ensureMessageMetadata(normalizeThreadRepository(threadRuntime.export())), [threadRuntime]);
1867
1893
  const exportPersistableThreadRepo = useCallback(() => withLastAssistantRunDuration(exportCleanThreadRepo(), showRunningInUI ? null : lastChatRunDurationMs), [exportCleanThreadRepo, lastChatRunDurationMs, showRunningInUI]);
@@ -2026,37 +2052,38 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2026
2052
  reconnectAbortRef.current = abortCtrl;
2027
2053
  let reconnectTerminalReason = null;
2028
2054
  const reconnectStuckThresholdMs = activeRunStuckThresholdMs(runInfo);
2029
- const watchdog = setInterval(async () => {
2030
- try {
2031
- const res = await fetch(`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`);
2032
- if (!res.ok) {
2033
- abortCtrl.abort();
2034
- clearInterval(watchdog);
2035
- return;
2036
- }
2037
- const info = (await res.json());
2038
- // SINGLE-READER OWNERSHIP: if the adapter runtime came alive while
2039
- // this reader was attached (user sent a message, adapter adopted the
2040
- // thread's run), this reader is now the duplicate — kill it. The
2041
- // false→true runtime-transition effect also unwinds the UI state;
2042
- // this covers the reader itself.
2043
- if (isRuntimeRunningRef.current) {
2044
- abortCtrl.abort();
2045
- clearInterval(watchdog);
2046
- return;
2047
- }
2048
- if (isReplayableTerminalRun(info)) {
2049
- return;
2050
- }
2051
- if (info.status !== "running" || activeRunLooksStale(info)) {
2052
- abortCtrl.abort();
2053
- clearInterval(watchdog);
2054
- }
2055
+ // This lives inside a useCallback, not component render, so it can't
2056
+ // use the usePollLoop hook (Rules of Hooks) — createPollEngine gives
2057
+ // the same never-overlaps/never-stalls guarantees imperatively.
2058
+ const watchdog = createPollEngine(async (signal) => {
2059
+ if (document.hidden)
2060
+ return;
2061
+ const res = await fetch(`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`, { signal });
2062
+ if (!res.ok) {
2063
+ abortCtrl.abort();
2064
+ watchdog.stop();
2065
+ return;
2055
2066
  }
2056
- catch {
2057
- // Network blip — keep polling.
2067
+ const info = (await res.json());
2068
+ // SINGLE-READER OWNERSHIP: if the adapter runtime came alive while
2069
+ // this reader was attached (user sent a message, adapter adopted the
2070
+ // thread's run), this reader is now the duplicate — kill it. The
2071
+ // false→true runtime-transition effect also unwinds the UI state;
2072
+ // this covers the reader itself.
2073
+ if (isRuntimeRunningRef.current) {
2074
+ abortCtrl.abort();
2075
+ watchdog.stop();
2076
+ return;
2058
2077
  }
2059
- }, 1000);
2078
+ if (isReplayableTerminalRun(info)) {
2079
+ return;
2080
+ }
2081
+ if (info.status !== "running" || activeRunLooksStale(info)) {
2082
+ abortCtrl.abort();
2083
+ watchdog.stop();
2084
+ }
2085
+ }, { intervalMs: 1000 });
2086
+ watchdog.start();
2060
2087
  let reconnectTimedOut = false;
2061
2088
  // Idle deadline, NOT a total-duration cap. A long-but-healthy run (image
2062
2089
  // generation emits `activity` heartbeats every few seconds) keeps
@@ -2082,7 +2109,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2082
2109
  }
2083
2110
  reconnectTimedOut = true;
2084
2111
  abortCtrl.abort();
2085
- clearInterval(watchdog);
2112
+ watchdog.stop();
2086
2113
  clearInterval(idleCheck);
2087
2114
  }, 1000);
2088
2115
  const streamReconnect = async () => {
@@ -2119,18 +2146,23 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2119
2146
  return "unknown";
2120
2147
  }
2121
2148
  };
2122
- const threadPollInterval = afterSeq > 0
2123
- ? window.setInterval(() => {
2149
+ const threadPollEngine = afterSeq > 0
2150
+ ? createPollEngine(async (signal) => {
2151
+ if (document.hidden)
2152
+ return;
2124
2153
  if (reconnectRunIdRef.current !== runId)
2125
2154
  return;
2126
- // Adapter took over mid-poll — skip imports that would race the
2127
- // live stream and flicker tool cards back to older snapshots.
2128
- if (isRuntimeRunningRef.current || isAutoResumingRef.current) {
2155
+ // Adapter took over mid-poll — skip imports that would race
2156
+ // the live stream and flicker tool cards back to older
2157
+ // snapshots.
2158
+ if (isRuntimeRunningRef.current ||
2159
+ isAutoResumingRef.current) {
2129
2160
  return;
2130
2161
  }
2131
- void refreshThreadFromServer();
2132
- }, 2000)
2162
+ await refreshThreadFromServer(signal);
2163
+ }, { intervalMs: 2000, leading: false })
2133
2164
  : undefined;
2165
+ threadPollEngine?.start();
2134
2166
  try {
2135
2167
  const content = [];
2136
2168
  latestContent = content;
@@ -2213,10 +2245,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2213
2245
  }
2214
2246
  }
2215
2247
  finally {
2216
- if (threadPollInterval !== undefined) {
2217
- window.clearInterval(threadPollInterval);
2218
- }
2219
- clearInterval(watchdog);
2248
+ threadPollEngine?.stop();
2249
+ watchdog.stop();
2220
2250
  clearInterval(idleCheck);
2221
2251
  }
2222
2252
  // A newer reader, live adapter, stop action, or component unmount took
@@ -66,5 +66,7 @@ export type MultiTabAssistantChatProps = Omit<AssistantChatProps, "tabId" | "thr
66
66
  scope?: ChatThreadScope | null;
67
67
  /** @deprecated Scope context is now rendered in the composer. */
68
68
  showScopeBadge?: boolean;
69
+ /** Cadence for hydrating agent-team sub-agent tab status. Default: 3000. */
70
+ agentTeamPollMs?: number;
69
71
  };
70
- export declare function MultiTabAssistantChat({ showTabBar, renderHeader, renderOverlay, contentHidden, apiUrl, storageKey, restoreActiveThread, browserTabId, threadUrlSync, scope, ...props }: MultiTabAssistantChatProps): React.JSX.Element;
72
+ export declare function MultiTabAssistantChat({ showTabBar, renderHeader, renderOverlay, contentHidden, apiUrl, storageKey, restoreActiveThread, browserTabId, threadUrlSync, scope, agentTeamPollMs, ...props }: MultiTabAssistantChatProps): React.JSX.Element;
@@ -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)