@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -4
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +7 -3
- package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +13 -3
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +64 -23
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +69 -46
- package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +35 -6
- package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +24 -3
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +45 -6
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +38 -5
- package/corpus/templates/clips/changelog/2026-08-06-desktop-full-screen-recordings-now-normalize-microphone-leve.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +146 -45
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +185 -69
- package/corpus/templates/clips/server/jobs/brain-export.ts +13 -6
- package/corpus/templates/clips/server/jobs/builder-media-compression.ts +10 -3
- package/corpus/templates/clips/server/jobs/meeting-reminders.ts +10 -3
- package/corpus/templates/clips/server/jobs/poll-calendars.ts +23 -21
- package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +10 -3
- package/corpus/templates/clips/server/jobs/transactional-emails.ts +10 -3
- package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
- package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +15 -2
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +47 -4
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +82 -39
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +31 -9
- package/corpus/templates/mail/server/plugins/mail-jobs.ts +36 -17
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/dist/agent/production-agent.d.ts +6 -0
- package/dist/client/AssistantChat.js +73 -43
- package/dist/client/MultiTabAssistantChat.d.ts +3 -1
- package/dist/client/MultiTabAssistantChat.js +79 -92
- package/dist/client/browser-session-bridge.js +97 -54
- package/dist/client/db-admin/useAgentSync.js +34 -43
- package/dist/client/dynamic-suggestions.d.ts +3 -0
- package/dist/client/dynamic-suggestions.js +54 -52
- package/dist/client/hooks/index.d.ts +1 -0
- package/dist/client/hooks/index.js +1 -0
- package/dist/client/i18n.d.ts +7 -0
- package/dist/client/i18n.js +12 -3
- package/dist/client/integrations/useIntegrationStatus.d.ts +1 -1
- package/dist/client/integrations/useIntegrationStatus.js +9 -22
- package/dist/client/notifications/NotificationsBell.js +14 -9
- package/dist/client/progress/RunsTray.js +6 -5
- package/dist/client/settings/useBuilderStatus.js +95 -73
- package/dist/client/transcription/BuilderTranscriptionCta.js +27 -31
- package/dist/client/use-agent-engine-configured.js +21 -3
- package/dist/client/use-pausing-interval.d.ts +6 -0
- package/dist/client/use-pausing-interval.js +6 -0
- package/dist/client/use-poll-loop.d.ts +39 -0
- package/dist/client/use-poll-loop.js +73 -0
- package/dist/client/use-run-stuck-detection.js +41 -13
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/framework-tools.d.ts +126 -0
- package/dist/framework-tools.js +114 -0
- package/dist/integrations/google-docs-poller.js +26 -7
- package/dist/integrations/pending-tasks-retry-job.js +17 -23
- package/dist/integrations/plugin.js +16 -15
- package/dist/integrations/remote-push-delivery-job.js +16 -25
- package/dist/integrations/remote-retry-job.js +19 -23
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/oauth-tokens/google-refresh.js +12 -19
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/action-discovery.d.ts +27 -0
- package/dist/server/action-discovery.js +89 -0
- package/dist/server/agent-chat/action-filters-a2a.d.ts +11 -0
- package/dist/server/agent-chat/action-filters-a2a.js +17 -1
- package/dist/server/agent-chat/framework-prompts.d.ts +2 -0
- package/dist/server/agent-chat/plugin-options.d.ts +30 -0
- package/dist/server/agent-chat/prompt-resources.d.ts +4 -1
- package/dist/server/agent-chat/prompt-resources.js +8 -2
- package/dist/server/agent-chat-plugin.js +148 -100
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/interval-job.d.ts +25 -0
- package/dist/server/interval-job.js +19 -0
- package/dist/server/prompts/framework-core-compact.d.ts +5 -0
- package/dist/server/prompts/framework-core-compact.js +28 -12
- package/dist/server/prompts/framework-core.d.ts +5 -0
- package/dist/server/prompts/framework-core.js +33 -16
- package/dist/server/prompts/index.d.ts +1 -1
- package/dist/server/prompts/index.js +1 -1
- package/dist/server/prompts/shared-rules.d.ts +12 -4
- package/dist/server/prompts/shared-rules.js +14 -4
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/poll-engine.d.ts +49 -0
- package/dist/shared/poll-engine.js +148 -0
- package/dist/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
- package/dist/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
- package/dist/tracking/providers.js +10 -1
- package/docs/content/deployment.mdx +47 -13
- package/docs/content/key-concepts.mdx +13 -6
- package/package.json +2 -1
- package/src/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
- 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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
140
|
+
await processJobs();
|
|
142
141
|
} catch (err) {
|
|
143
|
-
console.error("[mail-jobs]
|
|
142
|
+
console.error("[mail-jobs] processJobs failed:", err);
|
|
144
143
|
}
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
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
|
-
|
|
2057
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2123
|
-
?
|
|
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
|
|
2127
|
-
// live stream and flicker tool cards back to older
|
|
2128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2217
|
-
|
|
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
|
-
|
|
900
|
-
let stopped = false;
|
|
901
|
-
let timer = null;
|
|
901
|
+
usePollLoop(async (signal) => {
|
|
902
902
|
const runsUrl = `${apiUrl.replace(/\/$/, "")}/runs/list?goalId=agent-team`;
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
|
941
|
+
return next;
|
|
925
942
|
});
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
(
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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)
|