@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,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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
if (timer)
|
|
183
|
-
clearTimeout(timer);
|
|
145
|
+
mountedRef.current = false;
|
|
184
146
|
};
|
|
185
|
-
}, [
|
|
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[];
|
|
@@ -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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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";
|
package/dist/client/i18n.d.ts
CHANGED
|
@@ -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;
|
package/dist/client/i18n.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
...
|
|
498
|
+
...supportedLocales.map((code) => ({
|
|
490
499
|
value: code,
|
|
491
500
|
label: `${LOCALE_METADATA[code].nativeName} (${code})`,
|
|
492
501
|
description: code,
|
|
@@ -1,37 +1,24 @@
|
|
|
1
|
-
import { useState,
|
|
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
|
|
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
|
-
|
|
12
|
-
setLoading(false);
|
|
11
|
+
setLoading(false);
|
|
13
12
|
return;
|
|
14
13
|
}
|
|
15
14
|
const data = await res.json();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
setLoading(false);
|
|
19
|
-
}
|
|
15
|
+
setStatuses(Array.isArray(data) ? data : []);
|
|
16
|
+
setLoading(false);
|
|
20
17
|
}
|
|
21
18
|
catch {
|
|
22
|
-
|
|
23
|
-
setLoading(false);
|
|
24
|
-
}
|
|
19
|
+
setLoading(false);
|
|
25
20
|
}
|
|
26
21
|
}, []);
|
|
27
|
-
|
|
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 {
|
|
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
|
|
78
|
-
// claims to be granted
|
|
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
|
-
//
|
|
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
|
-
|
|
100
|
-
|
|
101
|
+
usePollLoop(refresh, {
|
|
102
|
+
intervalMs: pollMs,
|
|
103
|
+
enabled: pollMs > 0,
|
|
104
|
+
pauseWhenHidden: !browserNotifications,
|
|
105
|
+
});
|
|
101
106
|
useEffect(() => {
|
|
102
107
|
if (!open)
|
|
103
108
|
return;
|