@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
|
@@ -6,7 +6,7 @@ import { agentNativePath } from "../api-path.js";
|
|
|
6
6
|
import { DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from "../components/ui/dropdown-menu.js";
|
|
7
7
|
import { useFormatters, useT } from "../i18n.js";
|
|
8
8
|
import { useChangeVersion } from "../use-change-version.js";
|
|
9
|
-
import {
|
|
9
|
+
import { usePollLoop } from "../use-poll-loop.js";
|
|
10
10
|
import { cn } from "../utils.js";
|
|
11
11
|
const RUN_CHANGE_SETTLE_MS = 250;
|
|
12
12
|
function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showRecent, }) {
|
|
@@ -14,19 +14,20 @@ function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showR
|
|
|
14
14
|
const [runs, setRuns] = useState([]);
|
|
15
15
|
const includeRecent = showRecent ?? !hideWhenIdle;
|
|
16
16
|
const runsVersion = useChangeVersion("runs");
|
|
17
|
-
const refresh = useCallback(async () => {
|
|
17
|
+
const refresh = useCallback(async (signal) => {
|
|
18
18
|
try {
|
|
19
19
|
const query = new URLSearchParams({ limit: String(limit) });
|
|
20
20
|
if (!includeRecent)
|
|
21
21
|
query.set("active", "true");
|
|
22
|
-
const res = await fetch(agentNativePath(`/_agent-native/runs?${query.toString()}`));
|
|
22
|
+
const res = await fetch(agentNativePath(`/_agent-native/runs?${query.toString()}`), { signal });
|
|
23
23
|
if (!res.ok)
|
|
24
24
|
return;
|
|
25
25
|
const rows = (await res.json());
|
|
26
26
|
setRuns(rows);
|
|
27
27
|
}
|
|
28
28
|
catch {
|
|
29
|
-
//
|
|
29
|
+
// coercion-ok: `runs` keeps its last good value rather than being
|
|
30
|
+
// cleared to an empty tray; the next poll tick retries.
|
|
30
31
|
}
|
|
31
32
|
}, [includeRecent, limit]);
|
|
32
33
|
useEffect(() => {
|
|
@@ -38,7 +39,7 @@ function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showR
|
|
|
38
39
|
const timeout = window.setTimeout(() => void refresh(), RUN_CHANGE_SETTLE_MS);
|
|
39
40
|
return () => window.clearTimeout(timeout);
|
|
40
41
|
}, [refresh, runsVersion]);
|
|
41
|
-
|
|
42
|
+
usePollLoop(refresh, { intervalMs: pollMs, enabled: pollMs > 0 });
|
|
42
43
|
const dismissRun = useCallback(async (runId) => {
|
|
43
44
|
setRuns((current) => current.filter((run) => run.id !== runId));
|
|
44
45
|
try {
|
|
@@ -4,6 +4,7 @@ import { trackEvent } from "../analytics.js";
|
|
|
4
4
|
import { agentNativePath } from "../api-path.js";
|
|
5
5
|
import { getCallbackOrigin } from "../frame.js";
|
|
6
6
|
import { openMcpAppHostLink } from "../mcp-app-host.js";
|
|
7
|
+
import { usePollLoop } from "../use-poll-loop.js";
|
|
7
8
|
/**
|
|
8
9
|
* Fetches Builder connection status from the neutral connection-status route.
|
|
9
10
|
* The legacy /_agent-native/builder/status route remains available for older
|
|
@@ -80,6 +81,10 @@ export function useBuilderStatus({ enabled = true, } = {}) {
|
|
|
80
81
|
}
|
|
81
82
|
const POLL_INTERVAL_MS = 2000;
|
|
82
83
|
const POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
84
|
+
// Fallback timeout for callers of fetchStatus() with no external signal of
|
|
85
|
+
// their own (the initial status fetch, the popup-open branches in `start`).
|
|
86
|
+
// The connect-flow poll loop below gets its timeout from usePollLoop instead.
|
|
87
|
+
const STATUS_FETCH_ABORT_MS = 10_000;
|
|
83
88
|
const CALLBACK_SUCCESS_STATUS_RETRY_MS = 500;
|
|
84
89
|
const CALLBACK_SUCCESS_STATUS_RETRIES = 10;
|
|
85
90
|
const BUILDER_CONNECT_PARAM = "_an_connect";
|
|
@@ -388,7 +393,6 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
388
393
|
// still-good direct URL (desktop) or refresh a new one inside the popup
|
|
389
394
|
// gesture path (browser/editor embeds).
|
|
390
395
|
const statusConnectUrlAtRef = useRef(null);
|
|
391
|
-
const pollRef = useRef(null);
|
|
392
396
|
const connectStartedAtRef = useRef(null);
|
|
393
397
|
const mountedRef = useRef(true);
|
|
394
398
|
const notifiedConnectedRef = useRef(false);
|
|
@@ -396,25 +400,42 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
396
400
|
// caller passes an inline arrow function.
|
|
397
401
|
const onConnectedRef = useRef(onConnected);
|
|
398
402
|
onConnectedRef.current = onConnected;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
403
|
+
// Tracking identity for whichever click is currently connecting, read by
|
|
404
|
+
// the poll loop's timeout-failure event below.
|
|
405
|
+
const activeTrackingRef = useRef({
|
|
406
|
+
source: trackingSource,
|
|
407
|
+
flow: trackingFlow,
|
|
408
|
+
});
|
|
409
|
+
// Accepts an optional external `signal` so the connect-flow poll loop below
|
|
410
|
+
// can cancel this fetch via its own timeout instead of racing a second,
|
|
411
|
+
// separately-constructed AbortController. Callers with no signal of their
|
|
412
|
+
// own (the initial status fetch, the popup-open branches in `start`) keep
|
|
413
|
+
// the internal fallback timeout.
|
|
414
|
+
const fetchStatus = useCallback(async (signal) => {
|
|
406
415
|
if (!enabled)
|
|
407
416
|
return null;
|
|
408
417
|
const origin = getCallbackOrigin() || window.location.origin;
|
|
418
|
+
const ownController = !signal && typeof AbortController !== "undefined"
|
|
419
|
+
? new AbortController()
|
|
420
|
+
: null;
|
|
421
|
+
const timeoutId = ownController
|
|
422
|
+
? setTimeout(() => ownController.abort(), STATUS_FETCH_ABORT_MS)
|
|
423
|
+
: null;
|
|
409
424
|
try {
|
|
410
|
-
const r = await fetch(new URL(agentNativePath("/_agent-native/connection-status/builder"), origin).href);
|
|
425
|
+
const r = await fetch(new URL(agentNativePath("/_agent-native/connection-status/builder"), origin).href, { signal: signal ?? ownController?.signal });
|
|
411
426
|
if (!r.ok)
|
|
412
427
|
return null;
|
|
413
428
|
return (await r.json());
|
|
414
429
|
}
|
|
415
430
|
catch {
|
|
431
|
+
// coercion-ok: null means "status unknown this tick" and callers hold
|
|
432
|
+
// their previous state rather than rendering a disconnected Builder.
|
|
416
433
|
return null;
|
|
417
434
|
}
|
|
435
|
+
finally {
|
|
436
|
+
if (timeoutId)
|
|
437
|
+
clearTimeout(timeoutId);
|
|
438
|
+
}
|
|
418
439
|
}, [enabled]);
|
|
419
440
|
// Initial fetch + focus/visibility refresh so if the user completed the
|
|
420
441
|
// flow in another tab (or a downgraded same-tab nav) we notice it. Also
|
|
@@ -433,7 +454,6 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
433
454
|
setStatusResolved(false);
|
|
434
455
|
setStatusConnectUrl(null);
|
|
435
456
|
statusConnectUrlAtRef.current = null;
|
|
436
|
-
stopPoll();
|
|
437
457
|
return;
|
|
438
458
|
}
|
|
439
459
|
mountedRef.current = true;
|
|
@@ -501,17 +521,19 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
501
521
|
window.removeEventListener("focus", refresh);
|
|
502
522
|
document.removeEventListener("visibilitychange", onVisible);
|
|
503
523
|
window.removeEventListener("agent-engine:configured-changed", refresh);
|
|
504
|
-
stopPoll();
|
|
505
524
|
};
|
|
506
|
-
}, [enabled, fetchStatus
|
|
525
|
+
}, [enabled, fetchStatus]);
|
|
507
526
|
const start = useCallback((startOptions) => {
|
|
508
527
|
if (!enabled)
|
|
509
528
|
return;
|
|
510
|
-
stopPoll();
|
|
511
529
|
const started = Date.now();
|
|
512
530
|
const clickTrackingSource = startOptions?.trackingSource ?? trackingSource;
|
|
513
531
|
const clickTrackingFlow = startOptions?.trackingFlow ?? trackingFlow;
|
|
514
532
|
connectStartedAtRef.current = started;
|
|
533
|
+
activeTrackingRef.current = {
|
|
534
|
+
source: clickTrackingSource,
|
|
535
|
+
flow: clickTrackingFlow,
|
|
536
|
+
};
|
|
515
537
|
setConnecting(true);
|
|
516
538
|
setError(null);
|
|
517
539
|
// Open SYNCHRONOUSLY inside the caller's click handler — any await
|
|
@@ -580,7 +602,6 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
580
602
|
const openedByHost = await openBuilderConnectViaMcpHost(trackedHostUrl);
|
|
581
603
|
if (!mountedRef.current || openedByHost)
|
|
582
604
|
return;
|
|
583
|
-
stopPoll();
|
|
584
605
|
connectStartedAtRef.current = null;
|
|
585
606
|
setConnecting(false);
|
|
586
607
|
setError("Couldn't open Builder from this chat host. Open this app in a browser tab and try Connect Builder (free tier available) again.");
|
|
@@ -628,7 +649,6 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
628
649
|
catch {
|
|
629
650
|
// Ignore close failures.
|
|
630
651
|
}
|
|
631
|
-
stopPoll();
|
|
632
652
|
connectStartedAtRef.current = null;
|
|
633
653
|
setConnecting(false);
|
|
634
654
|
setError("Couldn't start Builder connect. Refresh this page and try again.");
|
|
@@ -639,7 +659,6 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
639
659
|
flow: clickTrackingFlow,
|
|
640
660
|
});
|
|
641
661
|
if (!navigateBuilderConnectPopup(opened, trackedFreshUrl)) {
|
|
642
|
-
stopPoll();
|
|
643
662
|
connectStartedAtRef.current = null;
|
|
644
663
|
setConnecting(false);
|
|
645
664
|
setError("Couldn't navigate the Builder popup. Allow popups and try again.");
|
|
@@ -647,66 +666,72 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
647
666
|
})();
|
|
648
667
|
}
|
|
649
668
|
}
|
|
650
|
-
pollRef.current = setInterval(async () => {
|
|
651
|
-
const s = await fetchStatus();
|
|
652
|
-
if (!mountedRef.current) {
|
|
653
|
-
stopPoll();
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
if (s?.configured) {
|
|
657
|
-
stopPoll();
|
|
658
|
-
setConfigured(true);
|
|
659
|
-
setEnvManaged(!!s.envManaged);
|
|
660
|
-
setBuilderEnabled(!!s.builderEnabled);
|
|
661
|
-
const nextConnectUrl = s.cliAuthUrl ?? s.connectUrl ?? null;
|
|
662
|
-
setStatusConnectUrl(nextConnectUrl);
|
|
663
|
-
statusConnectUrlAtRef.current = nextConnectUrl ? Date.now() : null;
|
|
664
|
-
const org = s.orgName ?? null;
|
|
665
|
-
setOrgName(org);
|
|
666
|
-
setConnecting(false);
|
|
667
|
-
connectStartedAtRef.current = null;
|
|
668
|
-
notifiedConnectedRef.current = true;
|
|
669
|
-
notifyAgentEngineConfiguredChanged("builder-connect");
|
|
670
|
-
try {
|
|
671
|
-
await onConnectedRef.current?.({ orgName: org });
|
|
672
|
-
}
|
|
673
|
-
catch {
|
|
674
|
-
// Consumer's callback failed; we've already flipped the UI state
|
|
675
|
-
// to connected. Swallow so we don't re-arm the flow.
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
else if (isCurrentConnectError(s?.connectError, started)) {
|
|
679
|
-
// OAuth callback ran but writeBuilderCredentials threw — surface the
|
|
680
|
-
// real error instead of letting the user wait 5 minutes for timeout.
|
|
681
|
-
stopPoll();
|
|
682
|
-
connectStartedAtRef.current = null;
|
|
683
|
-
setConnecting(false);
|
|
684
|
-
setError(`Couldn't save Builder credentials: ${s.connectError.message}. Try again or contact support.`);
|
|
685
|
-
}
|
|
686
|
-
else if (Date.now() - started > POLL_TIMEOUT_MS) {
|
|
687
|
-
stopPoll();
|
|
688
|
-
connectStartedAtRef.current = null;
|
|
689
|
-
setConnecting(false);
|
|
690
|
-
trackEvent("builder connect failed", {
|
|
691
|
-
feature: "builder",
|
|
692
|
-
stage: "client",
|
|
693
|
-
reason: "timeout",
|
|
694
|
-
source: clickTrackingSource,
|
|
695
|
-
flow: cleanTrackingParam(clickTrackingFlow) ??
|
|
696
|
-
inferBuilderConnectTrackingFlow(clickTrackingSource),
|
|
697
|
-
});
|
|
698
|
-
setError("Didn't hear back from Builder in 5 minutes. Allow popups and try again.");
|
|
699
|
-
}
|
|
700
|
-
}, POLL_INTERVAL_MS);
|
|
701
669
|
}, [
|
|
702
670
|
enabled,
|
|
703
671
|
fetchStatus,
|
|
704
672
|
popupUrl,
|
|
705
673
|
statusConnectUrl,
|
|
706
|
-
stopPoll,
|
|
707
674
|
trackingFlow,
|
|
708
675
|
trackingSource,
|
|
709
676
|
]);
|
|
677
|
+
// Connect-flow poll: while `connecting`, checks the server every 2s for the
|
|
678
|
+
// popup callback to have landed. `enabled` here folds in both the hook's
|
|
679
|
+
// own `enabled` option and the 5-minute overall cap (checked below and
|
|
680
|
+
// enforced by flipping `connecting` false, which then disables this loop).
|
|
681
|
+
usePollLoop(async (signal) => {
|
|
682
|
+
const started = connectStartedAtRef.current;
|
|
683
|
+
if (started == null)
|
|
684
|
+
return;
|
|
685
|
+
const s = await fetchStatus(signal);
|
|
686
|
+
if (!mountedRef.current)
|
|
687
|
+
return;
|
|
688
|
+
if (s?.configured) {
|
|
689
|
+
setConfigured(true);
|
|
690
|
+
setEnvManaged(!!s.envManaged);
|
|
691
|
+
setBuilderEnabled(!!s.builderEnabled);
|
|
692
|
+
const nextConnectUrl = s.cliAuthUrl ?? s.connectUrl ?? null;
|
|
693
|
+
setStatusConnectUrl(nextConnectUrl);
|
|
694
|
+
statusConnectUrlAtRef.current = nextConnectUrl ? Date.now() : null;
|
|
695
|
+
const org = s.orgName ?? null;
|
|
696
|
+
setOrgName(org);
|
|
697
|
+
setConnecting(false);
|
|
698
|
+
connectStartedAtRef.current = null;
|
|
699
|
+
notifiedConnectedRef.current = true;
|
|
700
|
+
notifyAgentEngineConfiguredChanged("builder-connect");
|
|
701
|
+
try {
|
|
702
|
+
await onConnectedRef.current?.({ orgName: org });
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// coercion-ok: the connection itself succeeded and the UI state is
|
|
706
|
+
// already flipped; re-arming the flow on a consumer callback failure
|
|
707
|
+
// would reconnect an account that is connected.
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
else if (isCurrentConnectError(s?.connectError, started)) {
|
|
711
|
+
// OAuth callback ran but writeBuilderCredentials threw \u2014 surface the
|
|
712
|
+
// real error instead of letting the user wait 5 minutes for timeout.
|
|
713
|
+
connectStartedAtRef.current = null;
|
|
714
|
+
setConnecting(false);
|
|
715
|
+
setError(`Couldn't save Builder credentials: ${s.connectError.message}. Try again or contact support.`);
|
|
716
|
+
}
|
|
717
|
+
else if (Date.now() - started > POLL_TIMEOUT_MS) {
|
|
718
|
+
connectStartedAtRef.current = null;
|
|
719
|
+
setConnecting(false);
|
|
720
|
+
const { source, flow } = activeTrackingRef.current;
|
|
721
|
+
trackEvent("builder connect failed", {
|
|
722
|
+
feature: "builder",
|
|
723
|
+
stage: "client",
|
|
724
|
+
reason: "timeout",
|
|
725
|
+
source,
|
|
726
|
+
flow: cleanTrackingParam(flow) ?? inferBuilderConnectTrackingFlow(source),
|
|
727
|
+
});
|
|
728
|
+
setError("Didn't hear back from Builder in 5 minutes. Allow popups and try again.");
|
|
729
|
+
}
|
|
730
|
+
}, {
|
|
731
|
+
intervalMs: POLL_INTERVAL_MS,
|
|
732
|
+
leading: false,
|
|
733
|
+
enabled: enabled && connecting,
|
|
734
|
+
});
|
|
710
735
|
// Popup-side fast path: the callback page broadcasts a message so we stop
|
|
711
736
|
// polling immediately rather than waiting for the next 2s tick.
|
|
712
737
|
//
|
|
@@ -714,11 +739,10 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
714
739
|
// AND on window.message (legacy path for environments without BC or for
|
|
715
740
|
// popups that still have opener access). Both paths are safe to have open
|
|
716
741
|
// simultaneously \u2014 the first one to fire wins and the error is deduplicated
|
|
717
|
-
// by the
|
|
742
|
+
// by the setConnecting(false) call, which is idempotent.
|
|
718
743
|
useEffect(() => {
|
|
719
744
|
let channel = null;
|
|
720
745
|
const handleError = (message) => {
|
|
721
|
-
stopPoll();
|
|
722
746
|
connectStartedAtRef.current = null;
|
|
723
747
|
setConnecting(false);
|
|
724
748
|
setError(`Couldn't save Builder credentials: ${message}.`);
|
|
@@ -754,14 +778,12 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
754
778
|
setOrgName(s.orgName ?? null);
|
|
755
779
|
}
|
|
756
780
|
if (connectError) {
|
|
757
|
-
stopPoll();
|
|
758
781
|
connectStartedAtRef.current = null;
|
|
759
782
|
setConnecting(false);
|
|
760
783
|
setError(`Couldn't save Builder credentials: ${connectError.message}. Try again or contact support.`);
|
|
761
784
|
}
|
|
762
785
|
return;
|
|
763
786
|
}
|
|
764
|
-
stopPoll();
|
|
765
787
|
setHasFetchedStatus(true);
|
|
766
788
|
setConfigured(true);
|
|
767
789
|
setEnvManaged(!!s.envManaged);
|
|
@@ -819,7 +841,7 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
819
841
|
channel?.close();
|
|
820
842
|
window.removeEventListener("message", handler);
|
|
821
843
|
};
|
|
822
|
-
}, [fetchStatus
|
|
844
|
+
}, [fetchStatus]);
|
|
823
845
|
return {
|
|
824
846
|
configured,
|
|
825
847
|
statusResolved,
|
|
@@ -10,12 +10,14 @@ import { IconBolt, IconLoader2 } from "@tabler/icons-react";
|
|
|
10
10
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
11
11
|
import { agentNativePath } from "../api-path.js";
|
|
12
12
|
import { openBuilderConnectPopup } from "../settings/useBuilderStatus.js";
|
|
13
|
+
import { usePollLoop } from "../use-poll-loop.js";
|
|
14
|
+
const POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
13
15
|
export function BuilderTranscriptionCta() {
|
|
14
16
|
const [configured, setConfigured] = useState(null);
|
|
15
17
|
const [connectUrl, setConnectUrl] = useState(null);
|
|
16
18
|
const [connecting, setConnecting] = useState(false);
|
|
17
19
|
const [error, setError] = useState(null);
|
|
18
|
-
const
|
|
20
|
+
const connectStartedAtRef = useRef(null);
|
|
19
21
|
const mountedRef = useRef(true);
|
|
20
22
|
useEffect(() => {
|
|
21
23
|
mountedRef.current = true;
|
|
@@ -37,46 +39,40 @@ export function BuilderTranscriptionCta() {
|
|
|
37
39
|
});
|
|
38
40
|
return () => {
|
|
39
41
|
mountedRef.current = false;
|
|
40
|
-
if (pollRef.current)
|
|
41
|
-
clearInterval(pollRef.current);
|
|
42
42
|
};
|
|
43
43
|
}, []);
|
|
44
44
|
const handleConnect = useCallback(() => {
|
|
45
|
-
|
|
46
|
-
clearInterval(pollRef.current);
|
|
45
|
+
connectStartedAtRef.current = Date.now();
|
|
47
46
|
setConnecting(true);
|
|
48
47
|
setError(null);
|
|
49
48
|
openBuilderConnectPopup({
|
|
50
49
|
url: connectUrl ?? undefined,
|
|
51
50
|
source: "builder_transcription_cta",
|
|
52
51
|
});
|
|
53
|
-
const start = Date.now();
|
|
54
|
-
pollRef.current = setInterval(async () => {
|
|
55
|
-
try {
|
|
56
|
-
const r = await fetch(agentNativePath("/_agent-native/builder/status"));
|
|
57
|
-
if (!r.ok)
|
|
58
|
-
return;
|
|
59
|
-
const s = (await r.json());
|
|
60
|
-
if (!mountedRef.current) {
|
|
61
|
-
clearInterval(pollRef.current);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (s.configured) {
|
|
65
|
-
clearInterval(pollRef.current);
|
|
66
|
-
setConfigured(true);
|
|
67
|
-
setConnecting(false);
|
|
68
|
-
}
|
|
69
|
-
else if (Date.now() - start > 5 * 60 * 1000) {
|
|
70
|
-
clearInterval(pollRef.current);
|
|
71
|
-
setConnecting(false);
|
|
72
|
-
setError("Didn't hear back from Builder. Allow popups and try again.");
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// transient — keep polling
|
|
77
|
-
}
|
|
78
|
-
}, 2000);
|
|
79
52
|
}, [connectUrl]);
|
|
53
|
+
usePollLoop(async (signal) => {
|
|
54
|
+
const started = connectStartedAtRef.current;
|
|
55
|
+
if (started == null)
|
|
56
|
+
return;
|
|
57
|
+
const r = await fetch(agentNativePath("/_agent-native/builder/status"), {
|
|
58
|
+
signal,
|
|
59
|
+
});
|
|
60
|
+
if (!r.ok || !mountedRef.current)
|
|
61
|
+
return;
|
|
62
|
+
const s = (await r.json());
|
|
63
|
+
if (!mountedRef.current)
|
|
64
|
+
return;
|
|
65
|
+
if (s.configured) {
|
|
66
|
+
connectStartedAtRef.current = null;
|
|
67
|
+
setConfigured(true);
|
|
68
|
+
setConnecting(false);
|
|
69
|
+
}
|
|
70
|
+
else if (Date.now() - started > POLL_TIMEOUT_MS) {
|
|
71
|
+
connectStartedAtRef.current = null;
|
|
72
|
+
setConnecting(false);
|
|
73
|
+
setError("Didn't hear back from Builder. Allow popups and try again.");
|
|
74
|
+
}
|
|
75
|
+
}, { intervalMs: 2000, leading: false, enabled: connecting });
|
|
80
76
|
// Already connected or still loading — render nothing
|
|
81
77
|
if (configured === null || configured)
|
|
82
78
|
return null;
|
|
@@ -95,6 +95,17 @@ export function useAgentEngineConfigured(enabled = true, options) {
|
|
|
95
95
|
let requestSeq = 0;
|
|
96
96
|
let retryTimer;
|
|
97
97
|
let retryAttempt = 0;
|
|
98
|
+
const scheduleRetry = (delay) => {
|
|
99
|
+
retryTimer = setTimeout(() => {
|
|
100
|
+
if (document.hidden) {
|
|
101
|
+
// Tab is backgrounded — keep the same backoff instead of hitting
|
|
102
|
+
// the network; the visibilitychange listener below recovers fast.
|
|
103
|
+
scheduleRetry(delay);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
void check();
|
|
107
|
+
}, delay);
|
|
108
|
+
};
|
|
98
109
|
const check = async (options) => {
|
|
99
110
|
const seq = ++requestSeq;
|
|
100
111
|
if (retryTimer !== undefined) {
|
|
@@ -114,9 +125,7 @@ export function useAgentEngineConfigured(enabled = true, options) {
|
|
|
114
125
|
// back short of a reload.
|
|
115
126
|
const delay = Math.min(RETRY_BASE_MS * 2 ** retryAttempt, RETRY_MAX_MS);
|
|
116
127
|
retryAttempt += 1;
|
|
117
|
-
|
|
118
|
-
void check();
|
|
119
|
-
}, delay);
|
|
128
|
+
scheduleRetry(delay);
|
|
120
129
|
};
|
|
121
130
|
const onConfiguredChanged = () => {
|
|
122
131
|
void check();
|
|
@@ -130,15 +139,24 @@ export function useAgentEngineConfigured(enabled = true, options) {
|
|
|
130
139
|
}
|
|
131
140
|
void check({ missingFallback: true });
|
|
132
141
|
};
|
|
142
|
+
const onVisibilityChange = () => {
|
|
143
|
+
if (!document.hidden && retryTimer !== undefined) {
|
|
144
|
+
clearTimeout(retryTimer);
|
|
145
|
+
retryTimer = undefined;
|
|
146
|
+
void check();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
133
149
|
void check();
|
|
134
150
|
window.addEventListener("agent-engine:configured-changed", onConfiguredChanged);
|
|
135
151
|
// A stale failed stream can arrive after a reconnect succeeds. Re-check the
|
|
136
152
|
// current status before pinning the composer in setup.
|
|
137
153
|
window.addEventListener("agent-chat:missing-api-key", onMissing);
|
|
154
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
138
155
|
return () => {
|
|
139
156
|
cancelled = true;
|
|
140
157
|
if (retryTimer !== undefined)
|
|
141
158
|
clearTimeout(retryTimer);
|
|
159
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
142
160
|
window.removeEventListener("agent-engine:configured-changed", onConfiguredChanged);
|
|
143
161
|
window.removeEventListener("agent-chat:missing-api-key", onMissing);
|
|
144
162
|
};
|
|
@@ -6,5 +6,11 @@
|
|
|
6
6
|
* Pass `pollMs=0` to disable. Pass `pauseWhenHidden=false` to keep the
|
|
7
7
|
* interval running even when the tab is hidden — the bell's browser-
|
|
8
8
|
* notification popup loop uses that to still reach backgrounded tabs.
|
|
9
|
+
*
|
|
10
|
+
* @deprecated Use `usePollLoop` instead — it has the same visibility
|
|
11
|
+
* semantics plus a built-in per-attempt timeout and settle-then-reschedule
|
|
12
|
+
* (rather than `setInterval`) scheduling, so a hung `callback` can't wedge
|
|
13
|
+
* the loop. No remaining callers in this codebase; new polling code should
|
|
14
|
+
* use `usePollLoop` directly.
|
|
9
15
|
*/
|
|
10
16
|
export declare function usePausingInterval(callback: () => void | Promise<void>, pollMs: number, pauseWhenHidden?: boolean): void;
|
|
@@ -7,6 +7,12 @@ import { useEffect } from "react";
|
|
|
7
7
|
* Pass `pollMs=0` to disable. Pass `pauseWhenHidden=false` to keep the
|
|
8
8
|
* interval running even when the tab is hidden — the bell's browser-
|
|
9
9
|
* notification popup loop uses that to still reach backgrounded tabs.
|
|
10
|
+
*
|
|
11
|
+
* @deprecated Use `usePollLoop` instead — it has the same visibility
|
|
12
|
+
* semantics plus a built-in per-attempt timeout and settle-then-reschedule
|
|
13
|
+
* (rather than `setInterval`) scheduling, so a hung `callback` can't wedge
|
|
14
|
+
* the loop. No remaining callers in this codebase; new polling code should
|
|
15
|
+
* use `usePollLoop` directly.
|
|
10
16
|
*/
|
|
11
17
|
export function usePausingInterval(callback, pollMs, pauseWhenHidden = true) {
|
|
12
18
|
useEffect(() => {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface UsePollLoopOptions {
|
|
2
|
+
/** Delay in ms between attempts while the tab is visible. */
|
|
3
|
+
intervalMs: number;
|
|
4
|
+
/** Per-attempt timeout. Default: `Math.max(timeoutFloorMs, intervalMs * 4)`. */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Floor used by the default `timeoutMs`. Default: 10_000. */
|
|
7
|
+
timeoutFloorMs?: number;
|
|
8
|
+
/** Called when an attempt throws or times out. Default: swallow. */
|
|
9
|
+
onError?: (err: unknown) => void;
|
|
10
|
+
/** Run the first attempt immediately on mount. Default: true. */
|
|
11
|
+
leading?: boolean;
|
|
12
|
+
/** Set to false to tear the loop down without unmounting the caller. Default: true. */
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* true: fully stop while the tab is hidden, resuming immediately when it
|
|
16
|
+
* becomes visible again. false (default): keep polling at a relaxed
|
|
17
|
+
* cadence (`Math.max(intervalMs, hiddenIntervalFloorMs)`) — use this for
|
|
18
|
+
* loops that must still reach a backgrounded tab (e.g. browser
|
|
19
|
+
* notifications).
|
|
20
|
+
*/
|
|
21
|
+
pauseWhenHidden?: boolean;
|
|
22
|
+
/** Floor for the relaxed hidden-tab cadence when `pauseWhenHidden` is false. Default: 10_000. */
|
|
23
|
+
hiddenIntervalFloorMs?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface UsePollLoopHandle {
|
|
26
|
+
/** Cancel the pending wait and run an attempt now. */
|
|
27
|
+
pollNow: () => void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Poll a network endpoint on a recurring cadence without hand-rolling a
|
|
31
|
+
* visibility check, an in-flight guard, or a request timeout — see
|
|
32
|
+
* `createPollEngine` for the underlying guarantees. `attempt` receives an
|
|
33
|
+
* `AbortSignal`; pass it to `fetch` for real cancellation on timeout.
|
|
34
|
+
*
|
|
35
|
+
* Supersedes `usePausingInterval`, which pauses/resumes on visibility but has
|
|
36
|
+
* no request timeout and schedules via `setInterval` rather than a
|
|
37
|
+
* settle-then-reschedule loop.
|
|
38
|
+
*/
|
|
39
|
+
export declare function usePollLoop(attempt: (signal: AbortSignal) => Promise<void>, options: UsePollLoopOptions): UsePollLoopHandle;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { createPollEngine } from "../shared/poll-engine.js";
|
|
3
|
+
const HIDDEN_INTERVAL_FLOOR_MS = 10_000;
|
|
4
|
+
function isDocumentHidden() {
|
|
5
|
+
return (typeof document !== "undefined" && document.visibilityState === "hidden");
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Poll a network endpoint on a recurring cadence without hand-rolling a
|
|
9
|
+
* visibility check, an in-flight guard, or a request timeout — see
|
|
10
|
+
* `createPollEngine` for the underlying guarantees. `attempt` receives an
|
|
11
|
+
* `AbortSignal`; pass it to `fetch` for real cancellation on timeout.
|
|
12
|
+
*
|
|
13
|
+
* Supersedes `usePausingInterval`, which pauses/resumes on visibility but has
|
|
14
|
+
* no request timeout and schedules via `setInterval` rather than a
|
|
15
|
+
* settle-then-reschedule loop.
|
|
16
|
+
*/
|
|
17
|
+
export function usePollLoop(attempt, options) {
|
|
18
|
+
const attemptRef = useRef(attempt);
|
|
19
|
+
attemptRef.current = attempt;
|
|
20
|
+
const pollNowRef = useRef(() => { });
|
|
21
|
+
const { intervalMs, timeoutMs, timeoutFloorMs, onError, leading, enabled = true, pauseWhenHidden = false, hiddenIntervalFloorMs = HIDDEN_INTERVAL_FLOOR_MS, } = options;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!enabled)
|
|
24
|
+
return;
|
|
25
|
+
const engine = createPollEngine((signal) => attemptRef.current(signal), {
|
|
26
|
+
intervalMs: pauseWhenHidden
|
|
27
|
+
? intervalMs
|
|
28
|
+
: () => isDocumentHidden()
|
|
29
|
+
? Math.max(intervalMs, hiddenIntervalFloorMs)
|
|
30
|
+
: intervalMs,
|
|
31
|
+
timeoutMs,
|
|
32
|
+
timeoutFloorMs,
|
|
33
|
+
onError,
|
|
34
|
+
leading,
|
|
35
|
+
});
|
|
36
|
+
pollNowRef.current = () => engine.pollNow();
|
|
37
|
+
// Mounting in an already-hidden tab (background restore, prerender) must
|
|
38
|
+
// not fire the leading attempt when pauseWhenHidden promises a full pause
|
|
39
|
+
// while hidden — the visibility handler below starts the loop instead.
|
|
40
|
+
if (!pauseWhenHidden || !isDocumentHidden())
|
|
41
|
+
engine.start();
|
|
42
|
+
const onVisibilityChange = () => {
|
|
43
|
+
if (isDocumentHidden()) {
|
|
44
|
+
if (pauseWhenHidden)
|
|
45
|
+
engine.stop();
|
|
46
|
+
else
|
|
47
|
+
engine.reschedule();
|
|
48
|
+
}
|
|
49
|
+
else if (pauseWhenHidden) {
|
|
50
|
+
engine.start();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
engine.pollNow();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
57
|
+
return () => {
|
|
58
|
+
engine.stop();
|
|
59
|
+
pollNowRef.current = () => { };
|
|
60
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
61
|
+
};
|
|
62
|
+
}, [
|
|
63
|
+
intervalMs,
|
|
64
|
+
timeoutMs,
|
|
65
|
+
timeoutFloorMs,
|
|
66
|
+
onError,
|
|
67
|
+
leading,
|
|
68
|
+
enabled,
|
|
69
|
+
pauseWhenHidden,
|
|
70
|
+
hiddenIntervalFloorMs,
|
|
71
|
+
]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
72
|
+
return { pollNow: () => pollNowRef.current() };
|
|
73
|
+
}
|