@agent-native/core 0.144.2 → 0.145.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -4
  3. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +7 -3
  4. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +13 -3
  5. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +64 -23
  6. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +69 -46
  7. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +35 -6
  8. package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +24 -3
  9. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +45 -6
  10. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +38 -5
  11. package/corpus/templates/clips/changelog/2026-08-06-desktop-full-screen-recordings-now-normalize-microphone-leve.md +6 -0
  12. package/corpus/templates/clips/desktop/src/app.tsx +146 -45
  13. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +185 -69
  14. package/corpus/templates/clips/server/jobs/brain-export.ts +13 -6
  15. package/corpus/templates/clips/server/jobs/builder-media-compression.ts +10 -3
  16. package/corpus/templates/clips/server/jobs/meeting-reminders.ts +10 -3
  17. package/corpus/templates/clips/server/jobs/poll-calendars.ts +23 -21
  18. package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +10 -3
  19. package/corpus/templates/clips/server/jobs/transactional-emails.ts +10 -3
  20. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
  21. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +15 -2
  22. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +47 -4
  23. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  24. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +82 -39
  25. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +31 -9
  26. package/corpus/templates/mail/server/plugins/mail-jobs.ts +36 -17
  27. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  28. package/dist/agent/production-agent.d.ts +6 -0
  29. package/dist/client/AssistantChat.js +73 -43
  30. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  31. package/dist/client/MultiTabAssistantChat.js +79 -92
  32. package/dist/client/browser-session-bridge.js +97 -54
  33. package/dist/client/db-admin/useAgentSync.js +34 -43
  34. package/dist/client/dynamic-suggestions.d.ts +3 -0
  35. package/dist/client/dynamic-suggestions.js +54 -52
  36. package/dist/client/hooks/index.d.ts +1 -0
  37. package/dist/client/hooks/index.js +1 -0
  38. package/dist/client/i18n.d.ts +7 -0
  39. package/dist/client/i18n.js +12 -3
  40. package/dist/client/integrations/useIntegrationStatus.d.ts +1 -1
  41. package/dist/client/integrations/useIntegrationStatus.js +9 -22
  42. package/dist/client/notifications/NotificationsBell.js +14 -9
  43. package/dist/client/progress/RunsTray.js +6 -5
  44. package/dist/client/settings/useBuilderStatus.js +95 -73
  45. package/dist/client/transcription/BuilderTranscriptionCta.js +27 -31
  46. package/dist/client/use-agent-engine-configured.js +21 -3
  47. package/dist/client/use-pausing-interval.d.ts +6 -0
  48. package/dist/client/use-pausing-interval.js +6 -0
  49. package/dist/client/use-poll-loop.d.ts +39 -0
  50. package/dist/client/use-poll-loop.js +73 -0
  51. package/dist/client/use-run-stuck-detection.js +41 -13
  52. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  53. package/dist/framework-tools.d.ts +126 -0
  54. package/dist/framework-tools.js +114 -0
  55. package/dist/integrations/google-docs-poller.js +26 -7
  56. package/dist/integrations/pending-tasks-retry-job.js +17 -23
  57. package/dist/integrations/plugin.js +16 -15
  58. package/dist/integrations/remote-push-delivery-job.js +16 -25
  59. package/dist/integrations/remote-retry-job.js +19 -23
  60. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  61. package/dist/notifications/routes.d.ts +3 -3
  62. package/dist/oauth-tokens/google-refresh.js +12 -19
  63. package/dist/observability/routes.d.ts +3 -3
  64. package/dist/progress/routes.d.ts +1 -1
  65. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  66. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  67. package/dist/resources/handlers.d.ts +1 -1
  68. package/dist/secrets/routes.d.ts +3 -3
  69. package/dist/server/action-discovery.d.ts +27 -0
  70. package/dist/server/action-discovery.js +89 -0
  71. package/dist/server/agent-chat/action-filters-a2a.d.ts +11 -0
  72. package/dist/server/agent-chat/action-filters-a2a.js +17 -1
  73. package/dist/server/agent-chat/framework-prompts.d.ts +2 -0
  74. package/dist/server/agent-chat/plugin-options.d.ts +30 -0
  75. package/dist/server/agent-chat/prompt-resources.d.ts +4 -1
  76. package/dist/server/agent-chat/prompt-resources.js +8 -2
  77. package/dist/server/agent-chat-plugin.js +148 -100
  78. package/dist/server/index.d.ts +1 -0
  79. package/dist/server/index.js +1 -0
  80. package/dist/server/interval-job.d.ts +25 -0
  81. package/dist/server/interval-job.js +19 -0
  82. package/dist/server/prompts/framework-core-compact.d.ts +5 -0
  83. package/dist/server/prompts/framework-core-compact.js +28 -12
  84. package/dist/server/prompts/framework-core.d.ts +5 -0
  85. package/dist/server/prompts/framework-core.js +33 -16
  86. package/dist/server/prompts/index.d.ts +1 -1
  87. package/dist/server/prompts/index.js +1 -1
  88. package/dist/server/prompts/shared-rules.d.ts +12 -4
  89. package/dist/server/prompts/shared-rules.js +14 -4
  90. package/dist/server/realtime-token.d.ts +1 -1
  91. package/dist/shared/index.d.ts +1 -0
  92. package/dist/shared/index.js +1 -0
  93. package/dist/shared/poll-engine.d.ts +49 -0
  94. package/dist/shared/poll-engine.js +148 -0
  95. package/dist/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  96. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
  97. package/dist/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
  98. package/dist/tracking/providers.js +10 -1
  99. package/docs/content/deployment.mdx +47 -13
  100. package/docs/content/key-concepts.mdx +13 -6
  101. package/package.json +2 -1
  102. package/src/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  103. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
  104. package/src/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
@@ -18,6 +18,7 @@ const SWEEP_LIMIT = 4;
18
18
  const DISCOVERY_LIMIT = 20;
19
19
  const BACKFILL_DAYS = 7;
20
20
  let skippingLogged = false;
21
+ let running = false;
21
22
 
22
23
  async function seedRecentBrainExports(): Promise<void> {
23
24
  const now = new Date();
@@ -173,12 +174,18 @@ export default function registerBrainExportJob(): void {
173
174
  return;
174
175
  }
175
176
  setInterval(() => {
176
- reapExpiredUploads().catch((error) =>
177
- console.error("[uploads] lease reaper interval failed:", error),
178
- );
179
- runBrainExportSweepOnce().catch((error) =>
180
- console.error("[brain-export] interval failed:", error),
181
- );
177
+ if (running) return;
178
+ running = true;
179
+ Promise.allSettled([
180
+ reapExpiredUploads().catch((error) =>
181
+ console.error("[uploads] lease reaper interval failed:", error),
182
+ ),
183
+ runBrainExportSweepOnce().catch((error) =>
184
+ console.error("[brain-export] interval failed:", error),
185
+ ),
186
+ ]).finally(() => {
187
+ running = false;
188
+ });
182
189
  }, SWEEP_INTERVAL_MS);
183
190
  console.log(
184
191
  `[brain-export] Recurring recovery sweep every ${SWEEP_INTERVAL_MS / 1000}s.`,
@@ -13,6 +13,7 @@ import { runBuilderMediaCompressionSweepOnce } from "../lib/builder-media-compre
13
13
 
14
14
  const SWEEP_INTERVAL_MS = 5 * 60 * 1000;
15
15
  let skippingLogged = false;
16
+ let running = false;
16
17
 
17
18
  export { runBuilderMediaCompressionSweepOnce };
18
19
 
@@ -31,9 +32,15 @@ export default function registerBuilderMediaCompressionJob(): void {
31
32
  }
32
33
 
33
34
  setInterval(() => {
34
- runBuilderMediaCompressionSweepOnce().catch((err) =>
35
- console.error("[builder-media-compression] interval failed:", err),
36
- );
35
+ if (running) return;
36
+ running = true;
37
+ runBuilderMediaCompressionSweepOnce()
38
+ .catch((err) =>
39
+ console.error("[builder-media-compression] interval failed:", err),
40
+ )
41
+ .finally(() => {
42
+ running = false;
43
+ });
37
44
  }, SWEEP_INTERVAL_MS);
38
45
  console.log(
39
46
  `[builder-media-compression] Recurring compression sweep every ${SWEEP_INTERVAL_MS / 1000}s.`,
@@ -29,6 +29,7 @@ import { getDb, schema } from "../db/index.js";
29
29
  const REMINDER_INTERVAL_MS = 60 * 1000;
30
30
  const REMINDER_WINDOW_MS = 5 * 60 * 1000;
31
31
  let skippingLogged = false;
32
+ let running = false;
32
33
 
33
34
  const meetingReminderSchema = z.object({
34
35
  meetingId: z.string(),
@@ -114,9 +115,15 @@ export default function registerMeetingRemindersJob(): void {
114
115
  return;
115
116
  }
116
117
  setInterval(() => {
117
- runMeetingRemindersOnce().catch((err) =>
118
- console.error("[meeting-reminders] interval failed:", err),
119
- );
118
+ if (running) return;
119
+ running = true;
120
+ runMeetingRemindersOnce()
121
+ .catch((err) =>
122
+ console.error("[meeting-reminders] interval failed:", err),
123
+ )
124
+ .finally(() => {
125
+ running = false;
126
+ });
120
127
  }, REMINDER_INTERVAL_MS);
121
128
  console.log(
122
129
  `[meeting-reminders] Recurring meeting reminders every ${REMINDER_INTERVAL_MS / 1000}s.`,
@@ -2,15 +2,16 @@
2
2
  * poll-calendars — recurring job (every 5 min).
3
3
  *
4
4
  * Sweeps every connected calendar_accounts row and refreshes its events.
5
- * Runs as a Nitro plugin via `setInterval` (matches the mail-jobs pattern
6
- * — works on every host that keeps a long-lived process; on serverless
7
- * the deployment plugin should call `runPollCalendarsOnce` from a
8
- * scheduled function instead).
5
+ * Runs as a Nitro plugin via `startIntervalJob` (matches the mail-jobs
6
+ * pattern — works on every host that keeps a long-lived process; on
7
+ * serverless the deployment plugin should call `runPollCalendarsOnce` from
8
+ * a scheduled function instead).
9
9
  *
10
10
  * Token refresh + per-account error capture happens inside the
11
11
  * `sync-calendars` action; this job is just a thin scheduler.
12
12
  */
13
13
 
14
+ import { startIntervalJob } from "@agent-native/core/server/interval-job";
14
15
  import { runWithRequestContext } from "@agent-native/core/server/request-context";
15
16
 
16
17
  import syncCalendars from "../../actions/sync-calendars.js";
@@ -21,24 +22,21 @@ let skippingLogged = false;
21
22
  /**
22
23
  * Run a single poll pass. Exported so deployment-specific schedulers
23
24
  * (e.g. Netlify Scheduled Functions) can invoke it directly without
24
- * relying on a long-lived process.
25
+ * relying on a long-lived process. Throws on failure so a scheduled
26
+ * invocation records a failed run rather than a silent empty one.
25
27
  */
26
28
  export async function runPollCalendarsOnce(): Promise<void> {
27
29
  // No request context — this job runs as the system. The sync-calendars
28
30
  // action accepts `allAccounts: true` to skip the per-user accessFilter.
29
31
  await runWithRequestContext({}, async () => {
30
- try {
31
- const result = await syncCalendars.run(
32
- { allAccounts: true } as any,
33
- {} as any,
32
+ const result = await syncCalendars.run(
33
+ { allAccounts: true } as any,
34
+ {} as any,
35
+ );
36
+ if (result?.synced) {
37
+ console.log(
38
+ `[poll-calendars] synced ${result.synced} accounts, ${result.events ?? 0} events, ${result.meetings ?? 0} meetings`,
34
39
  );
35
- if (result?.synced) {
36
- console.log(
37
- `[poll-calendars] synced ${result.synced} accounts, ${result.events ?? 0} events, ${result.meetings ?? 0} meetings`,
38
- );
39
- }
40
- } catch (err: any) {
41
- console.error(`[poll-calendars] sync failed:`, err?.message ?? err);
42
40
  }
43
41
  });
44
42
  }
@@ -62,11 +60,15 @@ export default function registerPollCalendarsJob(): void {
62
60
  }
63
61
  return;
64
62
  }
65
- setInterval(() => {
66
- runPollCalendarsOnce().catch((err) =>
67
- console.error("[poll-calendars] interval failed:", err),
68
- );
69
- }, POLL_INTERVAL_MS);
63
+ // sync-calendars calls Google's Calendar API with no fetch-level timeout of
64
+ // its own, so the overlap guard and the hung-sweep timeout both come from
65
+ // startIntervalJob rather than a module-level `running` flag here.
66
+ startIntervalJob(() => runPollCalendarsOnce(), {
67
+ intervalMs: POLL_INTERVAL_MS,
68
+ leading: false,
69
+ onError: (err: any) =>
70
+ console.error("[poll-calendars] sync failed:", err?.message ?? err),
71
+ });
70
72
  console.log(
71
73
  `[poll-calendars] Recurring calendar sync every ${POLL_INTERVAL_MS / 1000}s.`,
72
74
  );
@@ -46,6 +46,7 @@ const STALE_THRESHOLD_MS = 60 * 60 * 1000; // 60 min of zero transcript activity
46
46
  // a slow Gemini call. Mirrors finalize-meeting.ts's force-takeover window.
47
47
  const PENDING_STALE_MS = 2 * 60 * 1000; // 2 min
48
48
  let skippingLogged = false;
49
+ let running = false;
49
50
 
50
51
  /**
51
52
  * Close out a single stranded-live meeting row: stamp actualEnd, set
@@ -284,9 +285,15 @@ export default function registerStaleMeetingSweeperJob(): void {
284
285
  return;
285
286
  }
286
287
  setInterval(() => {
287
- runStaleMeetingSweepOnce().catch((err) =>
288
- console.error("[stale-meeting-sweeper] interval failed:", err),
289
- );
288
+ if (running) return;
289
+ running = true;
290
+ runStaleMeetingSweepOnce()
291
+ .catch((err) =>
292
+ console.error("[stale-meeting-sweeper] interval failed:", err),
293
+ )
294
+ .finally(() => {
295
+ running = false;
296
+ });
290
297
  }, SWEEP_INTERVAL_MS);
291
298
  console.log(
292
299
  `[stale-meeting-sweeper] Recurring stale-meeting reconciliation every ${SWEEP_INTERVAL_MS / 1000}s.`,
@@ -49,6 +49,7 @@ const SENDING_LEASE_MS = 2 * 60 * 1000;
49
49
  const MAX_ATTEMPTS = 3;
50
50
  const RETRY_BASE_DELAY_MS = 60_000;
51
51
  let skippingLogged = false;
52
+ let running = false;
52
53
 
53
54
  type DirectShare = {
54
55
  id: string;
@@ -1304,9 +1305,15 @@ export default function registerTransactionalEmailsJob(): void {
1304
1305
  return;
1305
1306
  }
1306
1307
  setInterval(() => {
1307
- runTransactionalEmailsOnce().catch((error) =>
1308
- console.error("[transactional-emails] interval failed:", error),
1309
- );
1308
+ if (running) return;
1309
+ running = true;
1310
+ runTransactionalEmailsOnce()
1311
+ .catch((error) =>
1312
+ console.error("[transactional-emails] interval failed:", error),
1313
+ )
1314
+ .finally(() => {
1315
+ running = false;
1316
+ });
1310
1317
  }, JOB_INTERVAL_MS);
1311
1318
  console.log(
1312
1319
  `[transactional-emails] Recurring reconciliation and delivery every ${JOB_INTERVAL_MS / 1000}s.`,
@@ -167,6 +167,7 @@ export function NotionButton() {
167
167
 
168
168
  // Poll for connection
169
169
  pollRef.current = setInterval(async () => {
170
+ if (document.hidden) return;
170
171
  const result = await refetch();
171
172
  if (result.data?.connected) {
172
173
  clearInterval(pollRef.current);
@@ -83,11 +83,24 @@ export function FusionAppBanner({
83
83
  // ── Building: poll sync-fusion-app on mount + every ~8s while building ────
84
84
  const syncMutateRef = useRef(syncMutation.mutate);
85
85
  syncMutateRef.current = syncMutation.mutate;
86
+ const syncInFlightRef = useRef(false);
86
87
  useEffect(() => {
87
88
  if (status !== "building") return;
88
- syncMutateRef.current({ designId } as any);
89
+ // The leading sync goes through the same guard as the interval ticks: a
90
+ // container check still pending at the first tick would otherwise let a
91
+ // second status sync start and race the provisioning/screen-upsert work.
92
+ const runSync = (): void => {
93
+ if (syncInFlightRef.current) return;
94
+ syncInFlightRef.current = true;
95
+ syncMutateRef.current(
96
+ { designId } as any,
97
+ { onSettled: () => (syncInFlightRef.current = false) } as any,
98
+ );
99
+ };
100
+ runSync();
89
101
  const interval = window.setInterval(() => {
90
- syncMutateRef.current({ designId } as any);
102
+ if (document.hidden) return;
103
+ runSync();
91
104
  }, SYNC_POLL_INTERVAL_MS);
92
105
  return () => window.clearInterval(interval);
93
106
  }, [designId, status]);
@@ -182,6 +182,19 @@ const MAX_WHEEL_PAN_DELTA = 140;
182
182
  const CHROME_SCALE_CSS_VAR = "--an-chrome-scale";
183
183
  const PIXEL_GRID_ZOOM = 800;
184
184
 
185
+ function hasScreenChildLayers(content: string): boolean {
186
+ const body = content.match(/<body\b[^>]*>([\s\S]*?)<\/body\s*>/i)?.[1] ?? "";
187
+ const editableMarkup = body
188
+ .replace(/<!--[\s\S]*?-->/g, "")
189
+ .replace(
190
+ /<(script|style|template)\b[^>]*>[\s\S]*?<\/\1\s*>|<(link|meta|base)\b[^>]*>/gi, // i18n-ignore regex removes non-rendered document elements
191
+ "",
192
+ )
193
+ .trim();
194
+
195
+ return editableMarkup.length > 0;
196
+ }
197
+
185
198
  import {
186
199
  BOARD_SURFACE_BACKGROUND,
187
200
  getBoardContentKey,
@@ -3430,6 +3443,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
3430
3443
  const originCanvas = getCanvasPoint(e.clientX, e.clientY);
3431
3444
  let latestRect = normalizeRectFromPoints(originCanvas, originCanvas);
3432
3445
  let layerCandidates: CanvasLayerMarqueeCandidate[] = [];
3446
+ let lastLayerSelectionSignature: string | null = null;
3433
3447
  const marqueeState: MarqueeDragState = {
3434
3448
  type: "marquee",
3435
3449
  originClient: { x: e.clientX, y: e.clientY },
@@ -3469,6 +3483,14 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
3469
3483
  screenId: candidate.screenId,
3470
3484
  info: candidate.info,
3471
3485
  }));
3486
+ const signature = selection
3487
+ .map(
3488
+ (item) =>
3489
+ `${item.screenId}:${item.info.sourceId ?? item.info.pendingNodeId ?? item.info.selector ?? item.info.id ?? ""}`,
3490
+ )
3491
+ .join("|");
3492
+ if (signature === lastLayerSelectionSignature) return;
3493
+ lastLayerSelectionSignature = signature;
3472
3494
  onLayerMarqueeSelectionChange?.(selection, {
3473
3495
  source: "marquee",
3474
3496
  additive: state.additive,
@@ -3513,6 +3535,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
3513
3535
  additive: false,
3514
3536
  shiftKey: false,
3515
3537
  });
3538
+ lastLayerSelectionSignature = "";
3516
3539
  }
3517
3540
  setIsDragging(true);
3518
3541
  // Seed collection with whatever the zero-size origin rect already
@@ -8236,6 +8259,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
8236
8259
  onStartRotate={(event) =>
8237
8260
  beginRotate(singleSelectedFrame.id, event)
8238
8261
  }
8262
+ onStartDrag={(event) =>
8263
+ beginFrameDrag(singleSelectedFrame.id, event)
8264
+ }
8239
8265
  />
8240
8266
  ) : null}
8241
8267
 
@@ -8259,6 +8285,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
8259
8285
  chromeScale={chromeScale}
8260
8286
  chromeSettling={chromeSettling}
8261
8287
  handlesEnabled={!readOnly}
8288
+ onStartDrag={(event) =>
8289
+ beginFrameDrag(selectedFrameEntries[0]!.id, event)
8290
+ }
8262
8291
  onStartResize={beginGroupResize}
8263
8292
  onStartRotate={beginGroupRotate}
8264
8293
  />
@@ -8270,6 +8299,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
8270
8299
  chromeScale={chromeScale}
8271
8300
  chromeSettling={chromeSettling}
8272
8301
  handlesEnabled={!readOnly}
8302
+ onStartDrag={(event) =>
8303
+ beginDraftDrag(selectedDraftEntries[0]!.id, event)
8304
+ }
8273
8305
  onStartResize={beginDraftGroupResize}
8274
8306
  />
8275
8307
  ) : null}
@@ -9563,6 +9595,7 @@ const Screen = memo(function Screen({
9563
9595
  !suppressFrameChromeForChild;
9564
9596
  const screenContentInteractive =
9565
9597
  Boolean(screenContent) &&
9598
+ (isSelected || hasScreenChildLayers(screen.content)) &&
9566
9599
  !locked &&
9567
9600
  !penActive &&
9568
9601
  !creationToolActive &&
@@ -9771,16 +9804,14 @@ const Screen = memo(function Screen({
9771
9804
  role="button"
9772
9805
  tabIndex={0}
9773
9806
  onClick={(e) => {
9774
- if (isInteractiveScreenContentTarget(e.target)) {
9775
- e.stopPropagation();
9776
- return;
9777
- }
9807
+ const interactiveContent = isInteractiveScreenContentTarget(e.target);
9778
9808
  e.stopPropagation();
9779
9809
  if (suppressNextClick.current) {
9780
9810
  suppressNextClick.current = false;
9781
9811
  return;
9782
9812
  }
9783
9813
  if (e.detail > 1) return;
9814
+ if (interactiveContent && isSelected) return;
9784
9815
  onPick(screen.id, e);
9785
9816
  }}
9786
9817
  onDoubleClick={(e) => {
@@ -10672,6 +10703,7 @@ function GroupSelectionBox({
10672
10703
  bounds,
10673
10704
  chromeScale,
10674
10705
  chromeSettling,
10706
+ onStartDrag,
10675
10707
  onStartResize,
10676
10708
  onStartRotate,
10677
10709
  handlesEnabled = true,
@@ -10679,6 +10711,7 @@ function GroupSelectionBox({
10679
10711
  bounds: NonNullable<ReturnType<typeof getFrameGroupBounds>>;
10680
10712
  chromeScale: number;
10681
10713
  chromeSettling: boolean;
10714
+ onStartDrag?: (e: React.MouseEvent) => void;
10682
10715
  onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
10683
10716
  /** Multi-selection rotate (CV14). Omit to keep the previous behavior (no
10684
10717
  * rotate handle shown) — used by callers whose selection kind doesn't
@@ -10699,6 +10732,7 @@ function GroupSelectionBox({
10699
10732
  showRotate={!!onStartRotate}
10700
10733
  handlesEnabled={handlesEnabled}
10701
10734
  filled
10735
+ onStartDrag={onStartDrag}
10702
10736
  onStartResize={onStartResize}
10703
10737
  onStartRotate={onStartRotate ?? (() => {})}
10704
10738
  />
@@ -10765,6 +10799,7 @@ function SelectionBox({
10765
10799
  handlesEnabled = true,
10766
10800
  onStartResize,
10767
10801
  onStartRotate,
10802
+ onStartDrag,
10768
10803
  }: {
10769
10804
  geometry: FrameGeometry;
10770
10805
  chromeScale: number;
@@ -10774,6 +10809,7 @@ function SelectionBox({
10774
10809
  handlesEnabled?: boolean;
10775
10810
  onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
10776
10811
  onStartRotate: (e: React.MouseEvent) => void;
10812
+ onStartDrag?: (e: React.MouseEvent) => void;
10777
10813
  }) {
10778
10814
  return (
10779
10815
  <div
@@ -10798,6 +10834,13 @@ function SelectionBox({
10798
10834
  zIndex: 1_000_000,
10799
10835
  }}
10800
10836
  >
10837
+ {onStartDrag ? (
10838
+ <span
10839
+ data-frame-drag-surface
10840
+ className="pointer-events-auto absolute inset-0 z-[5] cursor-move"
10841
+ onMouseDown={onStartDrag}
10842
+ />
10843
+ ) : null}
10801
10844
  <ResizeHandles
10802
10845
  active
10803
10846
  enabled={handlesEnabled}
@@ -56,7 +56,7 @@ export default createAgentChatPlugin({
56
56
  actions: loadActionsFromStaticRegistry(actionsRegistry),
57
57
  nativeActionsInDev: true,
58
58
  skipFilesContext: true,
59
- databaseTools: false,
59
+ frameworkTools: { database: "off" },
60
60
  resolveOrgId: async (event) => (await getOrgContext(event)).orgId,
61
61
  mentionProviders: async () => {
62
62
  const { getDb } = await import("../db/index.js");
@@ -66,6 +66,16 @@ const STEPS = [
66
66
  },
67
67
  ];
68
68
 
69
+ const DESKTOP_POLL_INTERVAL_MS = 1500;
70
+ const ADD_ACCOUNT_POLL_INTERVAL_MS = 2000;
71
+ // Bounds each poll's fetches so a hung request can't leave its in-flight
72
+ // guard stuck and stall the interval forever.
73
+ const DESKTOP_POLL_ABORT_MS = Math.max(10_000, DESKTOP_POLL_INTERVAL_MS * 4);
74
+ const ADD_ACCOUNT_POLL_ABORT_MS = Math.max(
75
+ 10_000,
76
+ ADD_ACCOUNT_POLL_INTERVAL_MS * 4,
77
+ );
78
+
69
79
  interface GoogleConnectBannerProps {
70
80
  variant?: "banner" | "hero";
71
81
  }
@@ -104,6 +114,8 @@ export function GoogleConnectBanner({
104
114
  );
105
115
  const desktopPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
106
116
  const addAccountPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
117
+ const desktopPollInFlightRef = useRef(false);
118
+ const addAccountPollInFlightRef = useRef(false);
107
119
  useEffect(() => {
108
120
  return () => {
109
121
  if (desktopPollRef.current) clearInterval(desktopPollRef.current);
@@ -130,40 +142,54 @@ export function GoogleConnectBanner({
130
142
  const start = Date.now();
131
143
  if (desktopPollRef.current) clearInterval(desktopPollRef.current);
132
144
  desktopPollRef.current = setInterval(async () => {
145
+ if (document.hidden || desktopPollInFlightRef.current) return;
146
+ desktopPollInFlightRef.current = true;
147
+ const controller = new AbortController();
148
+ const abortTimer = setTimeout(
149
+ () => controller.abort(),
150
+ DESKTOP_POLL_ABORT_MS,
151
+ );
133
152
  try {
134
- const res = await fetch(
135
- agentNativePath(
136
- `/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
137
- ),
138
- );
139
- const data = await res.json();
140
- if (data?.error) {
141
- clearInterval(desktopPollRef.current!);
142
- desktopPollRef.current = null;
143
- setDesktopAuthIssue(data);
144
- } else if (data?.token) {
145
- clearInterval(desktopPollRef.current!);
146
- desktopPollRef.current = null;
147
- await fetch(
153
+ try {
154
+ const res = await fetch(
148
155
  agentNativePath(
149
- `/_agent-native/auth/session?_session=${data.token}`,
156
+ `/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
150
157
  ),
151
- {
152
- credentials: "include",
153
- },
158
+ { signal: controller.signal },
154
159
  );
155
- window.location.reload();
156
- } else if (Date.now() - start > 120_000) {
157
- clearInterval(desktopPollRef.current!);
158
- desktopPollRef.current = null;
159
- }
160
- } catch {
161
- if (Date.now() - start > 120_000) {
162
- clearInterval(desktopPollRef.current!);
163
- desktopPollRef.current = null;
160
+ const data = await res.json();
161
+ if (data?.error) {
162
+ clearInterval(desktopPollRef.current!);
163
+ desktopPollRef.current = null;
164
+ setDesktopAuthIssue(data);
165
+ } else if (data?.token) {
166
+ clearInterval(desktopPollRef.current!);
167
+ desktopPollRef.current = null;
168
+ await fetch(
169
+ agentNativePath(
170
+ `/_agent-native/auth/session?_session=${data.token}`,
171
+ ),
172
+ {
173
+ credentials: "include",
174
+ signal: controller.signal,
175
+ },
176
+ );
177
+ window.location.reload();
178
+ } else if (Date.now() - start > 120_000) {
179
+ clearInterval(desktopPollRef.current!);
180
+ desktopPollRef.current = null;
181
+ }
182
+ } catch {
183
+ if (Date.now() - start > 120_000) {
184
+ clearInterval(desktopPollRef.current!);
185
+ desktopPollRef.current = null;
186
+ }
164
187
  }
188
+ } finally {
189
+ clearTimeout(abortTimer);
190
+ desktopPollInFlightRef.current = false;
165
191
  }
166
- }, 1500);
192
+ }, DESKTOP_POLL_INTERVAL_MS);
167
193
  }
168
194
 
169
195
  const [authError, setAuthError] = useState<string | null>(null);
@@ -274,20 +300,37 @@ export function GoogleConnectBanner({
274
300
  const prevCount = accounts.length;
275
301
  if (addAccountPollRef.current) clearInterval(addAccountPollRef.current);
276
302
  addAccountPollRef.current = setInterval(async () => {
277
- const res = await fetch(
278
- agentNativePath("/_agent-native/google/status"),
279
- ).catch(() => null);
280
- if (res?.ok) {
281
- const data = await res.json();
282
- if (data.accounts?.length > prevCount) {
283
- if (addAccountPollRef.current) {
284
- clearInterval(addAccountPollRef.current);
285
- addAccountPollRef.current = null;
303
+ if (document.hidden || addAccountPollInFlightRef.current) return;
304
+ addAccountPollInFlightRef.current = true;
305
+ const controller = new AbortController();
306
+ const abortTimer = setTimeout(
307
+ () => controller.abort(),
308
+ ADD_ACCOUNT_POLL_ABORT_MS,
309
+ );
310
+ try {
311
+ const res = await fetch(
312
+ agentNativePath("/_agent-native/google/status"),
313
+ { signal: controller.signal },
314
+ )
315
+ // coercion-ok: a failed probe and a not-yet-added-account response
316
+ // both mean "keep waiting"; this loop only acts on an observed
317
+ // account-count increase.
318
+ .catch(() => null);
319
+ if (res?.ok) {
320
+ const data = await res.json();
321
+ if (data.accounts?.length > prevCount) {
322
+ if (addAccountPollRef.current) {
323
+ clearInterval(addAccountPollRef.current);
324
+ addAccountPollRef.current = null;
325
+ }
326
+ window.location.reload();
286
327
  }
287
- window.location.reload();
288
328
  }
329
+ } finally {
330
+ clearTimeout(abortTimer);
331
+ addAccountPollInFlightRef.current = false;
289
332
  }
290
- }, 2000);
333
+ }, ADD_ACCOUNT_POLL_INTERVAL_MS);
291
334
  // accounts.length is captured into prevCount above; including it in deps
292
335
  // would tear down and recreate the interval whenever the count changes.
293
336
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -96,6 +96,10 @@ type SnoozeTarget = {
96
96
  };
97
97
  const COMPOSE_FULLSCREEN_PARAM = "composeFullscreen";
98
98
  const SIDEBAR_COLLAPSE_KEY = "mail-sidebar-collapsed";
99
+ const ACCOUNT_POLL_INTERVAL_MS = 2000;
100
+ // Bounds the account-status poll so a hung fetch can't leave the in-flight
101
+ // guard stuck and stall the interval forever.
102
+ const ACCOUNT_POLL_ABORT_MS = Math.max(10_000, ACCOUNT_POLL_INTERVAL_MS * 4);
99
103
 
100
104
  function AccountAvatar({
101
105
  email,
@@ -2491,18 +2495,36 @@ function AccountPopover({
2491
2495
  setWantAuthUrl(false);
2492
2496
  window.open(authUrl.data.url, "_blank");
2493
2497
 
2498
+ let inFlight = false;
2494
2499
  const interval = setInterval(async () => {
2495
- const res = await fetch(
2496
- agentNativePath("/_agent-native/google/status"),
2497
- ).catch(() => null);
2498
- if (res?.ok) {
2499
- const data = await res.json();
2500
- if (data.accounts?.length > accounts.length) {
2501
- clearInterval(interval);
2502
- window.location.reload();
2500
+ if (document.hidden || inFlight) return;
2501
+ inFlight = true;
2502
+ const controller = new AbortController();
2503
+ const abortTimer = setTimeout(
2504
+ () => controller.abort(),
2505
+ ACCOUNT_POLL_ABORT_MS,
2506
+ );
2507
+ try {
2508
+ const res = await fetch(
2509
+ agentNativePath("/_agent-native/google/status"),
2510
+ { signal: controller.signal },
2511
+ )
2512
+ // coercion-ok: a failed probe and a not-yet-added-account response
2513
+ // both mean "keep waiting"; this loop only acts on an observed
2514
+ // account-count increase.
2515
+ .catch(() => null);
2516
+ if (res?.ok) {
2517
+ const data = await res.json();
2518
+ if (data.accounts?.length > accounts.length) {
2519
+ clearInterval(interval);
2520
+ window.location.reload();
2521
+ }
2503
2522
  }
2523
+ } finally {
2524
+ clearTimeout(abortTimer);
2525
+ inFlight = false;
2504
2526
  }
2505
- }, 2000);
2527
+ }, ACCOUNT_POLL_INTERVAL_MS);
2506
2528
 
2507
2529
  return () => clearInterval(interval);
2508
2530
  }, [wantAuthUrl, authUrl.data, accounts.length]);