@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
package/corpus/README.md
CHANGED
|
@@ -166,10 +166,24 @@ async function testConnection(
|
|
|
166
166
|
return res.json();
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// Bounds the GitHub status fetch so a hang can't leave the connect poll's
|
|
170
|
+
// `inFlight` guard stuck and stall the interval forever.
|
|
171
|
+
const GITHUB_OAUTH_STATUS_ABORT_MS = 10_000;
|
|
172
|
+
|
|
169
173
|
async function fetchGitHubOAuthStatus(): Promise<GitHubOAuthStatus> {
|
|
170
|
-
const
|
|
171
|
-
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const abortTimer = setTimeout(
|
|
176
|
+
() => controller.abort(),
|
|
177
|
+
GITHUB_OAUTH_STATUS_ABORT_MS,
|
|
172
178
|
);
|
|
179
|
+
let res: Response;
|
|
180
|
+
try {
|
|
181
|
+
res = await fetch(agentNativePath("/_agent-native/oauth/github/status"), {
|
|
182
|
+
signal: controller.signal,
|
|
183
|
+
});
|
|
184
|
+
} finally {
|
|
185
|
+
clearTimeout(abortTimer);
|
|
186
|
+
}
|
|
173
187
|
if (!res.ok) {
|
|
174
188
|
const data = await res.json().catch(() => ({}));
|
|
175
189
|
throw new Error(data.error || "Failed to load GitHub status");
|
|
@@ -374,8 +388,18 @@ function GitHubOAuthView({
|
|
|
374
388
|
},
|
|
375
389
|
onSuccess: () => {
|
|
376
390
|
const startedAt = Date.now();
|
|
377
|
-
|
|
378
|
-
|
|
391
|
+
let inFlight = false;
|
|
392
|
+
const pollId = window.setInterval(async () => {
|
|
393
|
+
if (document.hidden || inFlight) return;
|
|
394
|
+
inFlight = true;
|
|
395
|
+
try {
|
|
396
|
+
await queryClient.invalidateQueries({
|
|
397
|
+
queryKey: ["github-oauth-status"],
|
|
398
|
+
});
|
|
399
|
+
onSaved();
|
|
400
|
+
} finally {
|
|
401
|
+
inFlight = false;
|
|
402
|
+
}
|
|
379
403
|
if (Date.now() - startedAt > 120_000) {
|
|
380
404
|
window.clearInterval(pollId);
|
|
381
405
|
}
|
|
@@ -15,6 +15,11 @@ declare global {
|
|
|
15
15
|
|
|
16
16
|
let running = false;
|
|
17
17
|
const DEFAULT_MAX_REPORTS_PER_SWEEP = 5;
|
|
18
|
+
// Bounds snapshot/panel/render work per subscription. Originally only applied
|
|
19
|
+
// in serverless mode to fit the function's execution limit; it now also
|
|
20
|
+
// backstops the long-running in-process cron, where a hung query or render
|
|
21
|
+
// call would otherwise leave `running` stuck forever instead of just
|
|
22
|
+
// delaying the next sweep.
|
|
18
23
|
const SERVERLESS_REPORT_DELIVERY_BUDGET_MS = 220_000;
|
|
19
24
|
const SERVERLESS_MAX_REPORTS_PER_SWEEP = 1;
|
|
20
25
|
|
|
@@ -131,9 +136,8 @@ export async function runDashboardReportsOnce(): Promise<{
|
|
|
131
136
|
remaining = batch.length >= sweepLimit ? 1 : 0;
|
|
132
137
|
for (const sub of batch) {
|
|
133
138
|
processed++;
|
|
134
|
-
const deliveryDeadlineAt =
|
|
135
|
-
|
|
136
|
-
: undefined;
|
|
139
|
+
const deliveryDeadlineAt =
|
|
140
|
+
Date.now() + SERVERLESS_REPORT_DELIVERY_BUDGET_MS;
|
|
137
141
|
const retryAt = dashboardReportRetryAt(sub);
|
|
138
142
|
try {
|
|
139
143
|
const result = await runWithRequestContext(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { runDashboardReportsOnce } from "../jobs/dashboard-report";
|
|
2
2
|
import { isProductionServerlessRuntime } from "../lib/production-serverless-runtime";
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const DEFAULT_INTERVAL_MS = 60_000;
|
|
5
5
|
let skippingLogged = false;
|
|
6
6
|
|
|
7
7
|
declare global {
|
|
@@ -15,6 +15,15 @@ function platformSchedulerOwnsReports(): boolean {
|
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function intervalMs(): number {
|
|
19
|
+
const raw = process.env.DASHBOARD_REPORT_INTERVAL_MS?.trim();
|
|
20
|
+
if (!raw) return DEFAULT_INTERVAL_MS;
|
|
21
|
+
const parsed = Number.parseInt(raw, 10);
|
|
22
|
+
return Number.isFinite(parsed) && parsed >= 10_000
|
|
23
|
+
? parsed
|
|
24
|
+
: DEFAULT_INTERVAL_MS;
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
export default function registerDashboardReportJobs(): void {
|
|
19
28
|
const isProd = process.env.NODE_ENV === "production";
|
|
20
29
|
const flag =
|
|
@@ -36,13 +45,14 @@ export default function registerDashboardReportJobs(): void {
|
|
|
36
45
|
return;
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
const ms = intervalMs();
|
|
39
49
|
setInterval(() => {
|
|
40
50
|
runDashboardReportsOnce().catch((err) =>
|
|
41
51
|
console.error("[dashboard-report] interval failed:", err),
|
|
42
52
|
);
|
|
43
|
-
},
|
|
53
|
+
}, ms);
|
|
44
54
|
|
|
45
55
|
console.log(
|
|
46
|
-
`[dashboard-report] Recurring dashboard report sweep every ${
|
|
56
|
+
`[dashboard-report] Recurring dashboard report sweep every ${ms / 1000}s.`,
|
|
47
57
|
);
|
|
48
58
|
}
|
|
@@ -73,6 +73,11 @@ function getSetupSteps(t: CalendarT) {
|
|
|
73
73
|
];
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
const STATUS_POLL_INTERVAL_MS = 2000;
|
|
77
|
+
// Bounds each status poll so a hung fetch can't leave the in-flight guard
|
|
78
|
+
// permanently stuck and stall the interval forever.
|
|
79
|
+
const STATUS_POLL_ABORT_MS = Math.max(10_000, STATUS_POLL_INTERVAL_MS * 4);
|
|
80
|
+
|
|
76
81
|
interface GoogleConnectBannerProps {
|
|
77
82
|
variant?: "banner" | "hero";
|
|
78
83
|
}
|
|
@@ -99,6 +104,8 @@ export function GoogleConnectBanner({
|
|
|
99
104
|
const isBuilderFrame = useMemo(() => isInBuilderFrame(), []);
|
|
100
105
|
const authPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
101
106
|
const addAccountPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
107
|
+
const authPollInFlightRef = useRef(false);
|
|
108
|
+
const addAccountPollInFlightRef = useRef(false);
|
|
102
109
|
const {
|
|
103
110
|
isDesktopGoogleAuth,
|
|
104
111
|
isGoogleDesktopAuthPending,
|
|
@@ -165,21 +172,38 @@ export function GoogleConnectBanner({
|
|
|
165
172
|
|
|
166
173
|
if (authPollRef.current) clearInterval(authPollRef.current);
|
|
167
174
|
authPollRef.current = setInterval(async () => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
if (document.hidden || authPollInFlightRef.current) return;
|
|
176
|
+
authPollInFlightRef.current = true;
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const abortTimer = setTimeout(
|
|
179
|
+
() => controller.abort(),
|
|
180
|
+
STATUS_POLL_ABORT_MS,
|
|
181
|
+
);
|
|
182
|
+
try {
|
|
183
|
+
const res = await fetch(
|
|
184
|
+
agentNativePath("/_agent-native/google/status"),
|
|
185
|
+
{ signal: controller.signal },
|
|
186
|
+
)
|
|
187
|
+
// coercion-ok: a failed probe and a not-yet-connected response both
|
|
188
|
+
// mean "stay on the connect banner"; this loop only ever acts on an
|
|
189
|
+
// observed success, and credential errors surface via authUrl.error.
|
|
190
|
+
.catch(() => null);
|
|
191
|
+
if (res?.ok) {
|
|
192
|
+
const data = await res.json();
|
|
193
|
+
if (data.connected) {
|
|
194
|
+
if (authPollRef.current) {
|
|
195
|
+
clearInterval(authPollRef.current);
|
|
196
|
+
authPollRef.current = null;
|
|
197
|
+
}
|
|
198
|
+
setDismissed(true);
|
|
199
|
+
window.location.reload();
|
|
177
200
|
}
|
|
178
|
-
setDismissed(true);
|
|
179
|
-
window.location.reload();
|
|
180
201
|
}
|
|
202
|
+
} finally {
|
|
203
|
+
clearTimeout(abortTimer);
|
|
204
|
+
authPollInFlightRef.current = false;
|
|
181
205
|
}
|
|
182
|
-
},
|
|
206
|
+
}, STATUS_POLL_INTERVAL_MS);
|
|
183
207
|
}, [wantAuthUrl, authUrl.data, isBuilderFrame]);
|
|
184
208
|
|
|
185
209
|
// When auth URL fails with missing credentials, show wizard
|
|
@@ -242,20 +266,37 @@ export function GoogleConnectBanner({
|
|
|
242
266
|
const prevCount = accounts.length;
|
|
243
267
|
if (addAccountPollRef.current) clearInterval(addAccountPollRef.current);
|
|
244
268
|
addAccountPollRef.current = setInterval(async () => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
if (document.hidden || addAccountPollInFlightRef.current) return;
|
|
270
|
+
addAccountPollInFlightRef.current = true;
|
|
271
|
+
const controller = new AbortController();
|
|
272
|
+
const abortTimer = setTimeout(
|
|
273
|
+
() => controller.abort(),
|
|
274
|
+
STATUS_POLL_ABORT_MS,
|
|
275
|
+
);
|
|
276
|
+
try {
|
|
277
|
+
const res = await fetch(
|
|
278
|
+
agentNativePath("/_agent-native/google/status"),
|
|
279
|
+
{ signal: controller.signal },
|
|
280
|
+
)
|
|
281
|
+
// coercion-ok: a failed probe and a not-yet-connected response both
|
|
282
|
+
// mean "stay on the connect banner"; this loop only ever acts on an
|
|
283
|
+
// observed success, and credential errors surface via authUrl.error.
|
|
284
|
+
.catch(() => null);
|
|
285
|
+
if (res?.ok) {
|
|
286
|
+
const data = await res.json();
|
|
287
|
+
if (data.accounts?.length > prevCount) {
|
|
288
|
+
if (addAccountPollRef.current) {
|
|
289
|
+
clearInterval(addAccountPollRef.current);
|
|
290
|
+
addAccountPollRef.current = null;
|
|
291
|
+
}
|
|
292
|
+
window.location.reload();
|
|
254
293
|
}
|
|
255
|
-
window.location.reload();
|
|
256
294
|
}
|
|
295
|
+
} finally {
|
|
296
|
+
clearTimeout(abortTimer);
|
|
297
|
+
addAccountPollInFlightRef.current = false;
|
|
257
298
|
}
|
|
258
|
-
},
|
|
299
|
+
}, STATUS_POLL_INTERVAL_MS);
|
|
259
300
|
// accounts.length is captured into prevCount above; including it in deps
|
|
260
301
|
// would tear down and recreate the interval whenever the count changes.
|
|
261
302
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -37,6 +37,14 @@ interface DesktopGlobals {
|
|
|
37
37
|
electronAPI?: unknown;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const DESKTOP_AUTH_POLL_INTERVAL_MS = 1500;
|
|
41
|
+
// Bounds each poll tick's fetches so a hung request can't leave
|
|
42
|
+
// pollInFlightRef stuck and stall the interval forever.
|
|
43
|
+
const DESKTOP_AUTH_POLL_ABORT_MS = Math.max(
|
|
44
|
+
10_000,
|
|
45
|
+
DESKTOP_AUTH_POLL_INTERVAL_MS * 4,
|
|
46
|
+
);
|
|
47
|
+
|
|
40
48
|
function bodyError(
|
|
41
49
|
body: any,
|
|
42
50
|
raw: string | undefined,
|
|
@@ -188,6 +196,7 @@ export function useGoogleAddAccountUrl(enabled = false) {
|
|
|
188
196
|
export function useGoogleDesktopAuth(options: DesktopAuthOptions = {}) {
|
|
189
197
|
const { onError, onSuccess, timeoutMs = 120_000 } = options;
|
|
190
198
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
199
|
+
const pollInFlightRef = useRef(false);
|
|
191
200
|
const [isPending, setIsPending] = useState(false);
|
|
192
201
|
const isDesktopGoogleAuth = useMemo(() => {
|
|
193
202
|
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
@@ -285,62 +294,76 @@ export function useGoogleDesktopAuth(options: DesktopAuthOptions = {}) {
|
|
|
285
294
|
})();
|
|
286
295
|
|
|
287
296
|
pollRef.current = setInterval(async () => {
|
|
297
|
+
if (document.hidden || pollInFlightRef.current) return;
|
|
298
|
+
pollInFlightRef.current = true;
|
|
299
|
+
const controller = new AbortController();
|
|
300
|
+
const abortTimer = setTimeout(
|
|
301
|
+
() => controller.abort(),
|
|
302
|
+
DESKTOP_AUTH_POLL_ABORT_MS,
|
|
303
|
+
);
|
|
288
304
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
`/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
|
|
292
|
-
),
|
|
293
|
-
{ credentials: "include" },
|
|
294
|
-
);
|
|
295
|
-
const exchange = await exchangeRes.json();
|
|
296
|
-
if (exchange?.error) {
|
|
297
|
-
clearPoll();
|
|
298
|
-
setIsPending(false);
|
|
299
|
-
onError?.(exchange);
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
if (exchange?.token) {
|
|
303
|
-
await fetch(
|
|
305
|
+
try {
|
|
306
|
+
const exchangeRes = await fetch(
|
|
304
307
|
agentNativePath(
|
|
305
|
-
`/_agent-native/auth/
|
|
308
|
+
`/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
|
|
306
309
|
),
|
|
307
|
-
{ credentials: "include" },
|
|
310
|
+
{ credentials: "include", signal: controller.signal },
|
|
308
311
|
);
|
|
309
|
-
|
|
310
|
-
|
|
312
|
+
const exchange = await exchangeRes.json();
|
|
313
|
+
if (exchange?.error) {
|
|
314
|
+
clearPoll();
|
|
315
|
+
setIsPending(false);
|
|
316
|
+
onError?.(exchange);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (exchange?.token) {
|
|
320
|
+
await fetch(
|
|
321
|
+
agentNativePath(
|
|
322
|
+
`/_agent-native/auth/session?_session=${exchange.token}`,
|
|
323
|
+
),
|
|
324
|
+
{ credentials: "include", signal: controller.signal },
|
|
325
|
+
);
|
|
326
|
+
await finish({ token: exchange.token, email: exchange.email });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
// coercion-ok: keep polling; the status endpoint below may still
|
|
331
|
+
// observe success, and the timeout below reports desktop_auth_timeout.
|
|
311
332
|
}
|
|
312
|
-
} catch {
|
|
313
|
-
// Keep polling; the status endpoint below may still observe success.
|
|
314
|
-
}
|
|
315
333
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
334
|
+
try {
|
|
335
|
+
const statusRes = await fetch(
|
|
336
|
+
agentNativePath("/_agent-native/google/status"),
|
|
337
|
+
{ credentials: "include", signal: controller.signal },
|
|
338
|
+
);
|
|
339
|
+
if (statusRes.ok) {
|
|
340
|
+
const status = (await statusRes.json()) as GoogleAuthStatus;
|
|
341
|
+
const connected = startOptions.addAccount
|
|
342
|
+
? (status.accounts?.length ?? 0) >
|
|
343
|
+
(startOptions.previousAccountCount ?? 0)
|
|
344
|
+
: status.connected;
|
|
345
|
+
if (connected) {
|
|
346
|
+
await finish();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
330
349
|
}
|
|
350
|
+
} catch {
|
|
351
|
+
// coercion-ok: keep polling until the bounded timeout below, which
|
|
352
|
+
// reports desktop_auth_timeout rather than failing silently.
|
|
331
353
|
}
|
|
332
|
-
} catch {
|
|
333
|
-
// Keep polling until the timeout.
|
|
334
|
-
}
|
|
335
354
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
355
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
356
|
+
reportError({
|
|
357
|
+
code: "desktop_auth_timeout",
|
|
358
|
+
message:
|
|
359
|
+
"Google sign-in timed out. Finish sign-in in the browser or try again.",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
} finally {
|
|
363
|
+
clearTimeout(abortTimer);
|
|
364
|
+
pollInFlightRef.current = false;
|
|
342
365
|
}
|
|
343
|
-
},
|
|
366
|
+
}, DESKTOP_AUTH_POLL_INTERVAL_MS);
|
|
344
367
|
|
|
345
368
|
return true;
|
|
346
369
|
},
|
|
@@ -104,12 +104,25 @@ function save(prefs: CalendarViewPreferences) {
|
|
|
104
104
|
} catch {}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
const REFRESH_INTERVAL_MS = 2_000;
|
|
108
|
+
// Bounds the refresh fetch so a hung request can't stall the self-rescheduling
|
|
109
|
+
// setTimeout loop forever.
|
|
110
|
+
const REFRESH_ABORT_MS = Math.max(10_000, REFRESH_INTERVAL_MS * 4);
|
|
111
|
+
|
|
107
112
|
async function readAppStatePreferences(): Promise<CalendarViewPreferences | null> {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const abortTimer = setTimeout(() => controller.abort(), REFRESH_ABORT_MS);
|
|
115
|
+
let res: Response;
|
|
116
|
+
try {
|
|
117
|
+
res = await fetch(
|
|
118
|
+
agentNativePath(
|
|
119
|
+
`/_agent-native/application-state/${CALENDAR_VIEW_PREFERENCES_KEY}`,
|
|
120
|
+
),
|
|
121
|
+
{ signal: controller.signal },
|
|
122
|
+
);
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(abortTimer);
|
|
125
|
+
}
|
|
113
126
|
if (res.status === 404) return null;
|
|
114
127
|
if (!res.ok) throw new Error(`${res.status}`);
|
|
115
128
|
return normalizeCalendarViewPreferences(await res.json());
|
|
@@ -141,6 +154,11 @@ export function useViewPreferences() {
|
|
|
141
154
|
let timeout: number | undefined;
|
|
142
155
|
|
|
143
156
|
async function refresh() {
|
|
157
|
+
if (document.hidden) {
|
|
158
|
+
if (!cancelled)
|
|
159
|
+
timeout = window.setTimeout(refresh, REFRESH_INTERVAL_MS);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
144
162
|
try {
|
|
145
163
|
const remote = await readAppStatePreferences();
|
|
146
164
|
if (!cancelled && remote) {
|
|
@@ -182,13 +200,24 @@ export function useViewPreferences() {
|
|
|
182
200
|
// Preferences are a UI convenience; keep the local copy if app-state
|
|
183
201
|
// is temporarily unavailable.
|
|
184
202
|
} finally {
|
|
185
|
-
if (!cancelled)
|
|
203
|
+
if (!cancelled)
|
|
204
|
+
timeout = window.setTimeout(refresh, REFRESH_INTERVAL_MS);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function handleVisibilityChange() {
|
|
209
|
+
if (!document.hidden && timeout) {
|
|
210
|
+
window.clearTimeout(timeout);
|
|
211
|
+
timeout = undefined;
|
|
212
|
+
void refresh();
|
|
186
213
|
}
|
|
187
214
|
}
|
|
215
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
188
216
|
|
|
189
217
|
void refresh();
|
|
190
218
|
return () => {
|
|
191
219
|
cancelled = true;
|
|
220
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
192
221
|
if (timeout) window.clearTimeout(timeout);
|
|
193
222
|
};
|
|
194
223
|
}, []);
|
|
@@ -9,8 +9,22 @@ export interface ZoomAuthStatus {
|
|
|
9
9
|
accounts: Array<{ id: string; email?: string; displayName?: string }>;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// Bounds every zoom fetch so a hung request can't leave the status poll's
|
|
13
|
+
// `inFlight` guard stuck forever.
|
|
14
|
+
const FETCH_ABORT_MS = 10_000;
|
|
15
|
+
|
|
12
16
|
async function fetchJson<T>(input: string, init?: RequestInit): Promise<T> {
|
|
13
|
-
const
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const abortTimer = setTimeout(() => controller.abort(), FETCH_ABORT_MS);
|
|
19
|
+
let res: Response;
|
|
20
|
+
try {
|
|
21
|
+
res = await fetch(input, {
|
|
22
|
+
...init,
|
|
23
|
+
signal: init?.signal ?? controller.signal,
|
|
24
|
+
});
|
|
25
|
+
} finally {
|
|
26
|
+
clearTimeout(abortTimer);
|
|
27
|
+
}
|
|
14
28
|
if (!res.ok) {
|
|
15
29
|
let message = `${input} -> ${res.status}`;
|
|
16
30
|
try {
|
|
@@ -124,8 +138,15 @@ export function useConnectZoom() {
|
|
|
124
138
|
queryClient.invalidateQueries({ queryKey: ["zoom-status"] });
|
|
125
139
|
|
|
126
140
|
const startedAt = Date.now();
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
let inFlight = false;
|
|
142
|
+
const pollId = window.setInterval(async () => {
|
|
143
|
+
if (document.hidden || inFlight) return;
|
|
144
|
+
inFlight = true;
|
|
145
|
+
try {
|
|
146
|
+
await queryClient.invalidateQueries({ queryKey: ["zoom-status"] });
|
|
147
|
+
} finally {
|
|
148
|
+
inFlight = false;
|
|
149
|
+
}
|
|
129
150
|
if (Date.now() - startedAt > 120_000) {
|
|
130
151
|
window.clearInterval(pollId);
|
|
131
152
|
}
|
|
@@ -594,6 +594,8 @@ function TranscriptSetupCard({
|
|
|
594
594
|
const [connectError, setConnectError] = useState<string | null>(null);
|
|
595
595
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
596
596
|
const mountedRef = useRef(true);
|
|
597
|
+
const inFlightRef = useRef(false);
|
|
598
|
+
const visibilityHandlerRef = useRef<(() => void) | null>(null);
|
|
597
599
|
|
|
598
600
|
const autoRetryRef = useRef(false);
|
|
599
601
|
const onRetryRef = useRef(onRetry);
|
|
@@ -602,6 +604,16 @@ function TranscriptSetupCard({
|
|
|
602
604
|
onRetryRef.current = onRetry;
|
|
603
605
|
}, [onRetry]);
|
|
604
606
|
|
|
607
|
+
const stopVisibilityHandler = useCallback(() => {
|
|
608
|
+
if (visibilityHandlerRef.current) {
|
|
609
|
+
document.removeEventListener(
|
|
610
|
+
"visibilitychange",
|
|
611
|
+
visibilityHandlerRef.current,
|
|
612
|
+
);
|
|
613
|
+
visibilityHandlerRef.current = null;
|
|
614
|
+
}
|
|
615
|
+
}, []);
|
|
616
|
+
|
|
605
617
|
useEffect(() => {
|
|
606
618
|
mountedRef.current = true;
|
|
607
619
|
fetch(agentNativePath("/_agent-native/builder/status"))
|
|
@@ -617,8 +629,9 @@ function TranscriptSetupCard({
|
|
|
617
629
|
return () => {
|
|
618
630
|
mountedRef.current = false;
|
|
619
631
|
if (pollRef.current) clearInterval(pollRef.current);
|
|
632
|
+
stopVisibilityHandler();
|
|
620
633
|
};
|
|
621
|
-
}, []);
|
|
634
|
+
}, [stopVisibilityHandler]);
|
|
622
635
|
|
|
623
636
|
useEffect(() => {
|
|
624
637
|
if (!builderConfigured || autoRetryRef.current) return;
|
|
@@ -630,36 +643,62 @@ function TranscriptSetupCard({
|
|
|
630
643
|
|
|
631
644
|
const handleConnect = useCallback(() => {
|
|
632
645
|
if (pollRef.current) clearInterval(pollRef.current);
|
|
646
|
+
stopVisibilityHandler();
|
|
647
|
+
inFlightRef.current = false;
|
|
633
648
|
setConnecting(true);
|
|
634
649
|
setConnectError(null);
|
|
635
650
|
|
|
636
651
|
openBuilderConnectPopup({ source: "clips_transcript_panel" });
|
|
637
652
|
|
|
638
653
|
const start = Date.now();
|
|
639
|
-
|
|
654
|
+
const tick = async () => {
|
|
655
|
+
if (document.hidden || inFlightRef.current) return;
|
|
656
|
+
inFlightRef.current = true;
|
|
657
|
+
const controller = new AbortController();
|
|
658
|
+
const abortTimer = setTimeout(
|
|
659
|
+
() => controller.abort(),
|
|
660
|
+
Math.max(10_000, 2000 * 4),
|
|
661
|
+
);
|
|
640
662
|
try {
|
|
641
|
-
const r = await fetch(
|
|
663
|
+
const r = await fetch(
|
|
664
|
+
agentNativePath("/_agent-native/builder/status"),
|
|
665
|
+
{
|
|
666
|
+
signal: controller.signal,
|
|
667
|
+
},
|
|
668
|
+
);
|
|
642
669
|
if (!r.ok) return;
|
|
643
670
|
const s = (await r.json()) as { configured: boolean };
|
|
644
671
|
if (!mountedRef.current) {
|
|
645
672
|
clearInterval(pollRef.current!);
|
|
673
|
+
stopVisibilityHandler();
|
|
646
674
|
return;
|
|
647
675
|
}
|
|
648
676
|
if (s.configured) {
|
|
649
677
|
clearInterval(pollRef.current!);
|
|
678
|
+
stopVisibilityHandler();
|
|
650
679
|
setBuilderConfigured(true);
|
|
651
680
|
setConnecting(false);
|
|
652
681
|
onRetry?.();
|
|
653
682
|
} else if (Date.now() - start > 5 * 60 * 1000) {
|
|
654
683
|
clearInterval(pollRef.current!);
|
|
684
|
+
stopVisibilityHandler();
|
|
655
685
|
setConnecting(false);
|
|
656
686
|
setConnectError(t("transcriptPanel.builderNoResponse"));
|
|
657
687
|
}
|
|
658
688
|
} catch {
|
|
659
|
-
// transient poll error
|
|
689
|
+
// coercion-ok: a transient poll error keeps the loop running; the
|
|
690
|
+
// 5-minute bound above surfaces builderNoResponse if it never succeeds.
|
|
691
|
+
} finally {
|
|
692
|
+
clearTimeout(abortTimer);
|
|
693
|
+
inFlightRef.current = false;
|
|
660
694
|
}
|
|
661
|
-
}
|
|
662
|
-
|
|
695
|
+
};
|
|
696
|
+
pollRef.current = setInterval(() => void tick(), 2000);
|
|
697
|
+
visibilityHandlerRef.current = () => {
|
|
698
|
+
if (!document.hidden) void tick();
|
|
699
|
+
};
|
|
700
|
+
document.addEventListener("visibilitychange", visibilityHandlerRef.current);
|
|
701
|
+
}, [onRetry, stopVisibilityHandler]);
|
|
663
702
|
|
|
664
703
|
const isProviderError =
|
|
665
704
|
!isBuilderCreditsExhaustedMessage(failureReason) &&
|