@agent-native/core 0.68.0 → 0.68.2

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 (152) hide show
  1. package/README.md +4 -4
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +18 -0
  4. package/corpus/core/docs/content/authentication.md +16 -2
  5. package/corpus/core/docs/content/template-analytics.md +14 -9
  6. package/corpus/core/docs/content/template-clips.md +30 -2
  7. package/corpus/core/package.json +1 -1
  8. package/corpus/core/src/agent/engine/builder-engine.ts +22 -3
  9. package/corpus/core/src/agent/engine/types.ts +1 -0
  10. package/corpus/core/src/agent/production-agent.ts +2 -0
  11. package/corpus/core/src/agent/types.ts +1 -0
  12. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  13. package/corpus/core/src/client/AssistantChat.tsx +45 -6
  14. package/corpus/core/src/client/CommandMenu.tsx +98 -3
  15. package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
  16. package/corpus/core/src/client/active-run-state.ts +16 -0
  17. package/corpus/core/src/client/guided-questions.tsx +73 -17
  18. package/corpus/core/src/client/index.ts +2 -0
  19. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  20. package/corpus/core/src/oauth-tokens/google-refresh.ts +32 -17
  21. package/corpus/core/src/provider-api/index.ts +37 -17
  22. package/corpus/core/src/server/auth.ts +6 -8
  23. package/corpus/core/src/server/better-auth-instance.ts +19 -4
  24. package/corpus/core/src/server/core-routes-plugin.ts +157 -5
  25. package/corpus/core/src/server/google-oauth-credentials.ts +51 -0
  26. package/corpus/core/src/server/index.ts +8 -0
  27. package/corpus/core/src/server/onboarding-html.ts +2 -1
  28. package/corpus/core/src/templates/workspace-root/.env.example +4 -0
  29. package/corpus/templates/analytics/app/routes/$.tsx +9 -0
  30. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +33 -1
  31. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +11 -3
  32. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +25 -2
  33. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +12 -0
  34. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +7 -2
  35. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +37 -78
  36. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +33 -1
  37. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +46 -3
  38. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +203 -2
  39. package/corpus/templates/calendar/app/lib/event-colors.ts +1 -4
  40. package/corpus/templates/calendar/app/lib/rsvp-status.tsx +6 -0
  41. package/corpus/templates/calendar/app/pages/CalendarView.tsx +9 -4
  42. package/corpus/templates/calendar/app/pages/NotFound.tsx +1 -1
  43. package/corpus/templates/calendar/app/pages/Settings.tsx +25 -1
  44. package/corpus/templates/calendar/app/routes/_app.$.tsx +9 -0
  45. package/corpus/templates/calendar/server/handlers/google-auth.ts +152 -12
  46. package/corpus/templates/calendar/server/lib/google-calendar.ts +63 -14
  47. package/corpus/templates/calendar/server/plugins/auth.ts +4 -20
  48. package/corpus/templates/calendar/shared/api.ts +4 -0
  49. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +17 -0
  50. package/corpus/templates/clips/actions/sync-calendars.ts +6 -7
  51. package/corpus/templates/clips/app/components/capture-install-options.tsx +157 -0
  52. package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -8
  53. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +4 -6
  54. package/corpus/templates/clips/app/lib/capture-install-options.ts +13 -0
  55. package/corpus/templates/clips/app/root.tsx +54 -2
  56. package/corpus/templates/clips/app/routes/_app.dictate.tsx +9 -13
  57. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -7
  58. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +5 -7
  59. package/corpus/templates/clips/app/routes/download.tsx +49 -0
  60. package/corpus/templates/clips/app/routes/record.tsx +4 -4
  61. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -6
  62. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +26 -0
  63. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -7
  64. package/corpus/templates/clips/server/lib/google-calendar-client.ts +61 -0
  65. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +19 -12
  66. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -6
  67. package/corpus/templates/design/actions/show-design-questions.ts +15 -1
  68. package/corpus/templates/design/app/hooks/use-agent-generating.ts +47 -8
  69. package/corpus/templates/design/app/pages/DesignEditor.tsx +6 -0
  70. package/corpus/templates/design/app/pages/NotFound.tsx +1 -1
  71. package/corpus/templates/design/app/routes/$.tsx +9 -0
  72. package/corpus/templates/mail/app/pages/NotFound.tsx +1 -1
  73. package/corpus/templates/mail/app/routes/$.tsx +9 -0
  74. package/corpus/templates/plan/app/global.css +2 -2
  75. package/corpus/templates/slides/app/pages/NotFound.tsx +1 -1
  76. package/corpus/templates/slides/app/routes/$.tsx +9 -0
  77. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +81 -25
  78. package/corpus/templates/videos/app/pages/NotFound.tsx +1 -1
  79. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  80. package/dist/agent/engine/builder-engine.js +21 -3
  81. package/dist/agent/engine/builder-engine.js.map +1 -1
  82. package/dist/agent/engine/types.d.ts +2 -0
  83. package/dist/agent/engine/types.d.ts.map +1 -1
  84. package/dist/agent/engine/types.js.map +1 -1
  85. package/dist/agent/production-agent.d.ts.map +1 -1
  86. package/dist/agent/production-agent.js +3 -0
  87. package/dist/agent/production-agent.js.map +1 -1
  88. package/dist/agent/types.d.ts +2 -0
  89. package/dist/agent/types.d.ts.map +1 -1
  90. package/dist/agent/types.js.map +1 -1
  91. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  92. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  93. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  94. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  95. package/dist/client/AssistantChat.d.ts.map +1 -1
  96. package/dist/client/AssistantChat.js +42 -7
  97. package/dist/client/AssistantChat.js.map +1 -1
  98. package/dist/client/CommandMenu.d.ts +15 -2
  99. package/dist/client/CommandMenu.d.ts.map +1 -1
  100. package/dist/client/CommandMenu.js +46 -3
  101. package/dist/client/CommandMenu.js.map +1 -1
  102. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  103. package/dist/client/ConnectBuilderCard.js +11 -2
  104. package/dist/client/ConnectBuilderCard.js.map +1 -1
  105. package/dist/client/active-run-state.d.ts +2 -0
  106. package/dist/client/active-run-state.d.ts.map +1 -1
  107. package/dist/client/active-run-state.js +10 -0
  108. package/dist/client/active-run-state.js.map +1 -1
  109. package/dist/client/guided-questions.d.ts +2 -0
  110. package/dist/client/guided-questions.d.ts.map +1 -1
  111. package/dist/client/guided-questions.js +52 -7
  112. package/dist/client/guided-questions.js.map +1 -1
  113. package/dist/client/index.d.ts +1 -1
  114. package/dist/client/index.d.ts.map +1 -1
  115. package/dist/client/index.js.map +1 -1
  116. package/dist/client/sse-event-processor.d.ts +1 -1
  117. package/dist/client/sse-event-processor.d.ts.map +1 -1
  118. package/dist/client/sse-event-processor.js +7 -1
  119. package/dist/client/sse-event-processor.js.map +1 -1
  120. package/dist/oauth-tokens/google-refresh.d.ts.map +1 -1
  121. package/dist/oauth-tokens/google-refresh.js +29 -17
  122. package/dist/oauth-tokens/google-refresh.js.map +1 -1
  123. package/dist/provider-api/index.d.ts.map +1 -1
  124. package/dist/provider-api/index.js +28 -16
  125. package/dist/provider-api/index.js.map +1 -1
  126. package/dist/server/auth.d.ts.map +1 -1
  127. package/dist/server/auth.js +6 -6
  128. package/dist/server/auth.js.map +1 -1
  129. package/dist/server/better-auth-instance.d.ts.map +1 -1
  130. package/dist/server/better-auth-instance.js +18 -4
  131. package/dist/server/better-auth-instance.js.map +1 -1
  132. package/dist/server/core-routes-plugin.d.ts +5 -0
  133. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  134. package/dist/server/core-routes-plugin.js +107 -5
  135. package/dist/server/core-routes-plugin.js.map +1 -1
  136. package/dist/server/google-oauth-credentials.d.ts +15 -0
  137. package/dist/server/google-oauth-credentials.d.ts.map +1 -0
  138. package/dist/server/google-oauth-credentials.js +34 -0
  139. package/dist/server/google-oauth-credentials.js.map +1 -0
  140. package/dist/server/index.d.ts +1 -0
  141. package/dist/server/index.d.ts.map +1 -1
  142. package/dist/server/index.js +1 -0
  143. package/dist/server/index.js.map +1 -1
  144. package/dist/server/onboarding-html.d.ts.map +1 -1
  145. package/dist/server/onboarding-html.js +2 -1
  146. package/dist/server/onboarding-html.js.map +1 -1
  147. package/dist/templates/workspace-root/.env.example +4 -0
  148. package/docs/content/authentication.md +16 -2
  149. package/docs/content/template-analytics.md +14 -9
  150. package/docs/content/template-clips.md +30 -2
  151. package/package.json +1 -1
  152. package/src/templates/workspace-root/.env.example +4 -0
@@ -21,7 +21,9 @@ import {
21
21
  useGoogleAuthStatus,
22
22
  useGoogleAuthUrl,
23
23
  useGoogleAddAccountUrl,
24
+ useGoogleDesktopAuth,
24
25
  useDisconnectGoogle,
26
+ type DesktopAuthIssue,
25
27
  } from "@/hooks/use-google-auth";
26
28
 
27
29
  interface EnvKeyStatus {
@@ -66,15 +68,6 @@ interface GoogleConnectBannerProps {
66
68
  variant?: "banner" | "hero";
67
69
  }
68
70
 
69
- interface DesktopAuthIssue {
70
- error?: string;
71
- message?: string;
72
- code?: string;
73
- accountId?: string;
74
- existingOwner?: string;
75
- attemptedOwner?: string;
76
- }
77
-
78
71
  export function GoogleConnectBanner({
79
72
  variant = "banner",
80
73
  }: GoogleConnectBannerProps) {
@@ -93,76 +86,23 @@ export function GoogleConnectBanner({
93
86
  const hasAccounts = accounts.length > 0;
94
87
 
95
88
  const isBuilderFrame = useMemo(() => isInBuilderFrame(), []);
96
- const useDesktopAuth = useMemo(
97
- () => /AgentNativeDesktop/i.test(navigator.userAgent) && !isBuilderFrame,
98
- [isBuilderFrame],
99
- );
100
- const desktopPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
101
89
  const authPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
102
90
  const addAccountPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
91
+ const {
92
+ isDesktopGoogleAuth,
93
+ isGoogleDesktopAuthPending,
94
+ startDesktopGoogleAuth,
95
+ } = useGoogleDesktopAuth({
96
+ onError: setDesktopAuthIssue,
97
+ onSuccess: () => window.location.reload(),
98
+ });
103
99
  useEffect(() => {
104
100
  return () => {
105
- if (desktopPollRef.current) clearInterval(desktopPollRef.current);
106
101
  if (authPollRef.current) clearInterval(authPollRef.current);
107
102
  if (addAccountPollRef.current) clearInterval(addAccountPollRef.current);
108
103
  };
109
104
  }, []);
110
105
 
111
- function signInViaDesktopBrowser(addAccount = false) {
112
- setDesktopAuthIssue(null);
113
- const flowId =
114
- crypto.randomUUID?.() ||
115
- Math.random().toString(36).slice(2) + Date.now().toString(36);
116
- const origin = window.location.origin;
117
- const endpoint = addAccount
118
- ? "/_agent-native/google/add-account/auth-url"
119
- : "/_agent-native/google/auth-url";
120
- const redirectUri = encodeURIComponent(
121
- oauthRedirectUri("/_agent-native/google/callback"),
122
- );
123
- window.open(
124
- `${origin}${agentNativePath(endpoint)}?redirect_uri=${redirectUri}&desktop=1&flow_id=${flowId}&redirect=1`,
125
- "_blank",
126
- );
127
- const start = Date.now();
128
- if (desktopPollRef.current) clearInterval(desktopPollRef.current);
129
- desktopPollRef.current = setInterval(async () => {
130
- try {
131
- const res = await fetch(
132
- agentNativePath(
133
- `/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
134
- ),
135
- );
136
- const data = await res.json();
137
- if (data?.error) {
138
- clearInterval(desktopPollRef.current!);
139
- desktopPollRef.current = null;
140
- setDesktopAuthIssue(data);
141
- } else if (data?.token) {
142
- clearInterval(desktopPollRef.current!);
143
- desktopPollRef.current = null;
144
- await fetch(
145
- agentNativePath(
146
- `/_agent-native/auth/session?_session=${data.token}`,
147
- ),
148
- {
149
- credentials: "include",
150
- },
151
- );
152
- window.location.reload();
153
- } else if (Date.now() - start > 120_000) {
154
- clearInterval(desktopPollRef.current!);
155
- desktopPollRef.current = null;
156
- }
157
- } catch {
158
- if (Date.now() - start > 120_000) {
159
- clearInterval(desktopPollRef.current!);
160
- desktopPollRef.current = null;
161
- }
162
- }
163
- }, 1500);
164
- }
165
-
166
106
  // Wizard state
167
107
  const [currentStep, setCurrentStep] = useState(0);
168
108
  const [saving, setSaving] = useState(false);
@@ -293,16 +233,19 @@ export function GoogleConnectBanner({
293
233
 
294
234
  function handleConnect() {
295
235
  setDesktopAuthIssue(null);
296
- if (useDesktopAuth) {
297
- signInViaDesktopBrowser();
236
+ if (isDesktopGoogleAuth) {
237
+ startDesktopGoogleAuth({ previousAccountCount: accounts.length });
298
238
  return;
299
239
  }
300
240
  setWantAuthUrl(true);
301
241
  }
302
242
 
303
243
  function handleAddAccount() {
304
- if (useDesktopAuth) {
305
- signInViaDesktopBrowser(true);
244
+ if (isDesktopGoogleAuth) {
245
+ startDesktopGoogleAuth({
246
+ addAccount: true,
247
+ previousAccountCount: accounts.length,
248
+ });
306
249
  return;
307
250
  }
308
251
  setWantAddAccount(true);
@@ -377,7 +320,11 @@ export function GoogleConnectBanner({
377
320
  size="sm"
378
321
  className="mt-6 gap-2 px-4 h-8 text-[13px] font-medium"
379
322
  onClick={handleConnect}
380
- disabled={authUrl.isLoading || authUrl.isFetching}
323
+ disabled={
324
+ authUrl.isLoading ||
325
+ authUrl.isFetching ||
326
+ isGoogleDesktopAuthPending
327
+ }
381
328
  >
382
329
  <GoogleIcon className="h-3.5 w-3.5" />
383
330
  {authUrl.isLoading
@@ -458,7 +405,11 @@ export function GoogleConnectBanner({
458
405
  ))}
459
406
  <button
460
407
  onClick={handleAddAccount}
461
- disabled={addAccountUrl.isLoading || addAccountUrl.isFetching}
408
+ disabled={
409
+ addAccountUrl.isLoading ||
410
+ addAccountUrl.isFetching ||
411
+ isGoogleDesktopAuthPending
412
+ }
462
413
  className="text-xs text-foreground/40 hover:text-foreground/60 transition-colors whitespace-nowrap"
463
414
  >
464
415
  + Add account
@@ -515,7 +466,11 @@ export function GoogleConnectBanner({
515
466
  size="sm"
516
467
  className="gap-1.5 text-xs h-7 font-medium bg-white text-black hover:bg-white/90"
517
468
  onClick={handleConnect}
518
- disabled={authUrl.isLoading || authUrl.isFetching}
469
+ disabled={
470
+ authUrl.isLoading ||
471
+ authUrl.isFetching ||
472
+ isGoogleDesktopAuthPending
473
+ }
519
474
  >
520
475
  <GoogleIcon className="h-3 w-3" />
521
476
  {authUrl.isLoading ? "Connecting..." : "Sign in with Google"}
@@ -526,7 +481,11 @@ export function GoogleConnectBanner({
526
481
  variant="outline"
527
482
  className="gap-1.5 text-xs h-7 font-medium"
528
483
  onClick={handleConnect}
529
- disabled={authUrl.isLoading || authUrl.isFetching}
484
+ disabled={
485
+ authUrl.isLoading ||
486
+ authUrl.isFetching ||
487
+ isGoogleDesktopAuthPending
488
+ }
530
489
  >
531
490
  {authUrl.isLoading ? "..." : "Connect Google"}
532
491
  </Button>
@@ -592,7 +592,17 @@ export function WeekView({
592
592
  onDraftDiscard={onDraftDiscard}
593
593
  >
594
594
  <button
595
- className="absolute flex items-center gap-1 truncate rounded px-1.5 text-left text-[11px] font-medium text-foreground transition-opacity hover:opacity-80"
595
+ className={cn(
596
+ "absolute flex items-center gap-1 truncate rounded px-1.5 text-left text-[11px] font-medium text-foreground transition-opacity hover:opacity-80",
597
+ event.ownerColor && "pr-3.5",
598
+ )}
599
+ aria-label={
600
+ event.ownerName || event.overlayEmail
601
+ ? `${event.title}, ${
602
+ event.ownerName || event.overlayEmail
603
+ }'s calendar`
604
+ : event.title
605
+ }
596
606
  style={{
597
607
  top: `${rowIdx * allDayRowHeight + 4}px`,
598
608
  left: `${leftPct}%`,
@@ -613,6 +623,13 @@ export function WeekView({
613
623
  )}
614
624
  <EventStatusIcon event={event} className="shrink-0" />
615
625
  <span className="truncate">{event.title}</span>
626
+ {event.ownerColor && (
627
+ <span
628
+ aria-hidden="true"
629
+ className="absolute right-1 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
630
+ style={{ backgroundColor: event.ownerColor }}
631
+ />
632
+ )}
616
633
  </button>
617
634
  </EventDetailPopover>
618
635
  );
@@ -864,7 +881,15 @@ export function WeekView({
864
881
  "ring-2 ring-primary/40",
865
882
  canDrag && segmentStartsHere && "cursor-grab",
866
883
  isBeingDragged && isDragging && "cursor-grabbing",
884
+ event.ownerColor && "pr-4",
867
885
  )}
886
+ aria-label={
887
+ event.ownerName || event.overlayEmail
888
+ ? `${event.title}, ${
889
+ event.ownerName || event.overlayEmail
890
+ }'s calendar`
891
+ : event.title
892
+ }
868
893
  style={{
869
894
  ...style,
870
895
  left: `${li.left}px`,
@@ -894,6 +919,13 @@ export function WeekView({
894
919
  isBeingDragged && isDragging ? 0.9 : undefined,
895
920
  }}
896
921
  >
922
+ {event.ownerColor && (
923
+ <span
924
+ aria-hidden="true"
925
+ className="absolute right-1.5 top-1.5 size-1.5 rounded-full ring-1 ring-background/70"
926
+ style={{ backgroundColor: event.ownerColor }}
927
+ />
928
+ )}
897
929
  {durationMin <= 30 ? (
898
930
  <div className="flex items-baseline gap-1 truncate">
899
931
  {allOthersOut && (
@@ -46,6 +46,7 @@ import {
46
46
  useGoogleAuthStatus,
47
47
  useGoogleAuthUrl,
48
48
  useGoogleAddAccountUrl,
49
+ useGoogleDesktopAuth,
49
50
  } from "@/hooks/use-google-auth";
50
51
  import {
51
52
  appPath,
@@ -305,6 +306,13 @@ function MiniCalendar({
305
306
  function GoogleConnectSidebarButton() {
306
307
  const [wantAuthUrl, setWantAuthUrl] = useState(false);
307
308
  const authUrl = useGoogleAuthUrl(wantAuthUrl);
309
+ const {
310
+ isDesktopGoogleAuth,
311
+ isGoogleDesktopAuthPending,
312
+ startDesktopGoogleAuth,
313
+ } = useGoogleDesktopAuth({
314
+ onSuccess: () => window.location.reload(),
315
+ });
308
316
 
309
317
  useEffect(() => {
310
318
  if (!wantAuthUrl || !authUrl.data?.url) return;
@@ -312,6 +320,14 @@ function GoogleConnectSidebarButton() {
312
320
  window.open(authUrl.data.url, "_blank");
313
321
  }, [wantAuthUrl, authUrl.data]);
314
322
 
323
+ function handleConnect() {
324
+ if (isDesktopGoogleAuth) {
325
+ startDesktopGoogleAuth({ previousAccountCount: 0 });
326
+ return;
327
+ }
328
+ setWantAuthUrl(true);
329
+ }
330
+
315
331
  return (
316
332
  <div className="border-t border-border p-3">
317
333
  <div className="rounded-lg bg-primary/10 p-3">
@@ -324,8 +340,12 @@ function GoogleConnectSidebarButton() {
324
340
  <Button
325
341
  size="sm"
326
342
  className="w-full gap-1.5 text-xs font-semibold"
327
- onClick={() => setWantAuthUrl(true)}
328
- disabled={authUrl.isLoading || authUrl.isFetching}
343
+ onClick={handleConnect}
344
+ disabled={
345
+ authUrl.isLoading ||
346
+ authUrl.isFetching ||
347
+ isGoogleDesktopAuthPending
348
+ }
329
349
  >
330
350
  <IconExternalLink className="h-3 w-3" />
331
351
  {authUrl.isLoading ? "Connecting..." : "Connect"}
@@ -398,6 +418,13 @@ function GoogleAccountsSection({
398
418
  } = useViewPreferences();
399
419
  const [wantAddAccount, setWantAddAccount] = useState(false);
400
420
  const addAccountUrl = useGoogleAddAccountUrl(wantAddAccount);
421
+ const {
422
+ isDesktopGoogleAuth,
423
+ isGoogleDesktopAuthPending,
424
+ startDesktopGoogleAuth,
425
+ } = useGoogleDesktopAuth({
426
+ onSuccess: () => window.location.reload(),
427
+ });
401
428
 
402
429
  useEffect(() => {
403
430
  if (!wantAddAccount || !addAccountUrl.data?.url) return;
@@ -405,6 +432,17 @@ function GoogleAccountsSection({
405
432
  setWantAddAccount(false);
406
433
  }, [wantAddAccount, addAccountUrl.data]);
407
434
 
435
+ function handleAddAccount() {
436
+ if (isDesktopGoogleAuth) {
437
+ startDesktopGoogleAuth({
438
+ addAccount: true,
439
+ previousAccountCount: accounts.length,
440
+ });
441
+ return;
442
+ }
443
+ setWantAddAccount(true);
444
+ }
445
+
408
446
  function handlePickColor(color: string) {
409
447
  updateViewPreferences({ colorMode: "single", singleColor: color });
410
448
  }
@@ -438,7 +476,12 @@ function GoogleAccountsSection({
438
476
  <TooltipTrigger asChild>
439
477
  <button
440
478
  type="button"
441
- onClick={() => setWantAddAccount(true)}
479
+ onClick={handleAddAccount}
480
+ disabled={
481
+ addAccountUrl.isLoading ||
482
+ addAccountUrl.isFetching ||
483
+ isGoogleDesktopAuthPending
484
+ }
442
485
  className="flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:text-foreground"
443
486
  >
444
487
  <IconPlus className="h-3.5 w-3.5" />
@@ -1,7 +1,36 @@
1
1
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
- import { agentNativePath, oauthRedirectUri } from "@agent-native/core/client";
2
+ import {
3
+ agentNativePath,
4
+ isInBuilderFrame,
5
+ oauthRedirectUri,
6
+ } from "@agent-native/core/client";
3
7
  import type { GoogleAuthStatus } from "@shared/api";
4
- import { useEffect } from "react";
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
+
10
+ export interface DesktopAuthIssue {
11
+ error?: string;
12
+ message?: string;
13
+ code?: string;
14
+ accountId?: string;
15
+ existingOwner?: string;
16
+ attemptedOwner?: string;
17
+ }
18
+
19
+ interface DesktopAuthResult {
20
+ token?: string;
21
+ email?: string;
22
+ }
23
+
24
+ interface DesktopAuthStartOptions {
25
+ addAccount?: boolean;
26
+ previousAccountCount?: number;
27
+ }
28
+
29
+ interface DesktopAuthOptions {
30
+ onError?: (issue: DesktopAuthIssue) => void;
31
+ onSuccess?: (result: DesktopAuthResult) => void | Promise<void>;
32
+ timeoutMs?: number;
33
+ }
5
34
 
6
35
  function bodyError(
7
36
  body: any,
@@ -134,6 +163,178 @@ export function useGoogleAddAccountUrl(enabled = false) {
134
163
  return query;
135
164
  }
136
165
 
166
+ export function useGoogleDesktopAuth(options: DesktopAuthOptions = {}) {
167
+ const { onError, onSuccess, timeoutMs = 120_000 } = options;
168
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
169
+ const [isPending, setIsPending] = useState(false);
170
+ const isDesktopGoogleAuth = useMemo(() => {
171
+ if (typeof navigator === "undefined") return false;
172
+ return (
173
+ /AgentNativeDesktop/i.test(navigator.userAgent) && !isInBuilderFrame()
174
+ );
175
+ }, []);
176
+
177
+ const clearPoll = useCallback(() => {
178
+ if (pollRef.current) {
179
+ clearInterval(pollRef.current);
180
+ pollRef.current = null;
181
+ }
182
+ }, []);
183
+
184
+ useEffect(() => clearPoll, [clearPoll]);
185
+
186
+ const startDesktopGoogleAuth = useCallback(
187
+ (startOptions: DesktopAuthStartOptions = {}) => {
188
+ if (!isDesktopGoogleAuth || typeof window === "undefined") return false;
189
+
190
+ clearPoll();
191
+ setIsPending(true);
192
+ const flowId =
193
+ globalThis.crypto?.randomUUID?.() ||
194
+ Math.random().toString(36).slice(2) + Date.now().toString(36);
195
+ const redirectUri = oauthRedirectUri("/_agent-native/google/callback");
196
+ const params = new URLSearchParams({
197
+ redirect_uri: redirectUri,
198
+ desktop: "1",
199
+ flow_id: flowId,
200
+ });
201
+ let popup: Window | null = null;
202
+ const reportError = (issue: DesktopAuthIssue) => {
203
+ clearPoll();
204
+ setIsPending(false);
205
+ onError?.(issue);
206
+ };
207
+ const openAuthUrl = (url: string) => {
208
+ if (popup && !popup.closed) {
209
+ popup.location.href = url;
210
+ return true;
211
+ }
212
+ popup = window.open(url, "_blank");
213
+ return !!popup;
214
+ };
215
+
216
+ const startedAt = Date.now();
217
+ const finish = async (result: DesktopAuthResult = {}) => {
218
+ clearPoll();
219
+ setIsPending(false);
220
+ await onSuccess?.(result);
221
+ };
222
+
223
+ if (startOptions.addAccount) {
224
+ popup = window.open("about:blank", "_blank");
225
+ void (async () => {
226
+ try {
227
+ const { url } = await fetchJson<{ url: string }>(
228
+ agentNativePath(
229
+ `/_agent-native/google/add-account/auth-url?${params.toString()}`,
230
+ ),
231
+ { credentials: "include" },
232
+ );
233
+ if (!openAuthUrl(url)) {
234
+ reportError({
235
+ code: "popup_blocked",
236
+ message:
237
+ "Calendar could not open Google sign-in. Allow popups and try again.",
238
+ });
239
+ }
240
+ } catch (err) {
241
+ popup?.close();
242
+ reportError({
243
+ code: "desktop_auth_start_failed",
244
+ message:
245
+ err instanceof Error
246
+ ? err.message
247
+ : "Could not start Google sign-in.",
248
+ });
249
+ }
250
+ })();
251
+ } else {
252
+ params.set("redirect", "1");
253
+ const opened = openAuthUrl(
254
+ `${window.location.origin}${agentNativePath(
255
+ "/_agent-native/google/auth-url",
256
+ )}?${params.toString()}`,
257
+ );
258
+ if (!opened) {
259
+ reportError({
260
+ code: "popup_blocked",
261
+ message:
262
+ "Calendar could not open Google sign-in. Allow popups and try again.",
263
+ });
264
+ return true;
265
+ }
266
+ }
267
+
268
+ pollRef.current = setInterval(async () => {
269
+ try {
270
+ const exchangeRes = await fetch(
271
+ agentNativePath(
272
+ `/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
273
+ ),
274
+ { credentials: "include" },
275
+ );
276
+ const exchange = await exchangeRes.json();
277
+ if (exchange?.error) {
278
+ clearPoll();
279
+ setIsPending(false);
280
+ onError?.(exchange);
281
+ return;
282
+ }
283
+ if (exchange?.token) {
284
+ await fetch(
285
+ agentNativePath(
286
+ `/_agent-native/auth/session?_session=${exchange.token}`,
287
+ ),
288
+ { credentials: "include" },
289
+ );
290
+ await finish({ token: exchange.token, email: exchange.email });
291
+ return;
292
+ }
293
+ } catch {
294
+ // Keep polling; the status endpoint below may still observe success.
295
+ }
296
+
297
+ try {
298
+ const statusRes = await fetch(
299
+ agentNativePath("/_agent-native/google/status"),
300
+ { credentials: "include" },
301
+ );
302
+ if (statusRes.ok) {
303
+ const status = (await statusRes.json()) as GoogleAuthStatus;
304
+ const connected = startOptions.addAccount
305
+ ? (status.accounts?.length ?? 0) >
306
+ (startOptions.previousAccountCount ?? 0)
307
+ : status.connected;
308
+ if (connected) {
309
+ await finish();
310
+ return;
311
+ }
312
+ }
313
+ } catch {
314
+ // Keep polling until the timeout.
315
+ }
316
+
317
+ if (Date.now() - startedAt > timeoutMs) {
318
+ reportError({
319
+ code: "desktop_auth_timeout",
320
+ message:
321
+ "Google sign-in timed out. Finish sign-in in the browser or try again.",
322
+ });
323
+ }
324
+ }, 1500);
325
+
326
+ return true;
327
+ },
328
+ [clearPoll, isDesktopGoogleAuth, onError, onSuccess, timeoutMs],
329
+ );
330
+
331
+ return {
332
+ isDesktopGoogleAuth,
333
+ isGoogleDesktopAuthPending: isPending,
334
+ startDesktopGoogleAuth,
335
+ };
336
+ }
337
+
137
338
  export function useDisconnectGoogle() {
138
339
  const queryClient = useQueryClient();
139
340
  return useMutation({
@@ -107,16 +107,13 @@ export function allOtherDeclined(event: CalendarEvent): boolean {
107
107
 
108
108
  /**
109
109
  * Returns a hex color for a calendar event based on its meeting type.
110
- * Respects user-set colors and overlay colors first.
110
+ * Respects user-set colors first.
111
111
  * For local (non-Google) events without a color, returns CSS var.
112
112
  */
113
113
  export function getEventAutoColor(event: CalendarEvent): string {
114
114
  // User/Google-set color takes priority
115
115
  if (event.color) return event.color;
116
116
 
117
- // Overlay events keep their overlay-assigned color
118
- if (event.overlayEmail) return EVENT_CATEGORY_COLORS.fallback;
119
-
120
117
  // Local events without a color use the theme primary
121
118
  if (event.source !== "google") return "hsl(var(--primary))";
122
119
 
@@ -27,6 +27,12 @@ export function getRsvpStatusLabel(status?: string) {
27
27
  }
28
28
  }
29
29
 
30
+ export function canInlineRsvp(
31
+ event: Pick<CalendarEvent, "source" | "overlayEmail">,
32
+ ): boolean {
33
+ return event.source === "google" && !event.overlayEmail;
34
+ }
35
+
30
36
  export function RsvpStatusIcon({
31
37
  status,
32
38
  className,
@@ -462,16 +462,21 @@ export default function CalendarView() {
462
462
  // small non-blocking spinner instead of hiding everything behind a skeleton.
463
463
  const eventsRefreshing = isFetching && !eventsLoading;
464
464
 
465
- // Apply overlay colors and filter hidden calendars
465
+ // Apply overlay ownership markers and filter hidden calendars
466
466
  const events = useMemo(() => {
467
- const colorMap = new Map(overlayPeople.map((p) => [p.email, p.color]));
467
+ const ownerMap = new Map(overlayPeople.map((p) => [p.email, p]));
468
468
  const sourceEvents = draftEvent
469
469
  ? [...rawEvents.filter((e) => e.id !== draftEvent.id), draftEvent]
470
470
  : rawEvents;
471
471
  return sourceEvents
472
472
  .map((e) => {
473
- if (e.overlayEmail && colorMap.has(e.overlayEmail)) {
474
- return { ...e, color: colorMap.get(e.overlayEmail) };
473
+ if (e.overlayEmail && ownerMap.has(e.overlayEmail)) {
474
+ const owner = ownerMap.get(e.overlayEmail);
475
+ return {
476
+ ...e,
477
+ ownerColor: owner?.color,
478
+ ownerName: owner?.name,
479
+ };
475
480
  }
476
481
  const tempId = quickEditTempIds[e.id];
477
482
  return tempId && !e._tempId ? { ...e, _tempId: tempId } : e;
@@ -10,7 +10,7 @@ export default function NotFound() {
10
10
  }, [location.pathname]);
11
11
 
12
12
  return (
13
- <div className="min-h-screen flex items-center justify-center bg-background">
13
+ <div className="flex min-h-full w-full items-center justify-center bg-background px-4 py-12">
14
14
  <div className="text-center">
15
15
  <h1 className="text-6xl font-bold text-foreground/10 mb-2">404</h1>
16
16
  <p className="text-sm text-muted-foreground mb-6">
@@ -31,6 +31,7 @@ import {
31
31
  import {
32
32
  useGoogleAuthStatus,
33
33
  useGoogleAuthUrl,
34
+ useGoogleDesktopAuth,
34
35
  useDisconnectGoogle,
35
36
  } from "@/hooks/use-google-auth";
36
37
  import {
@@ -45,6 +46,15 @@ export default function Settings() {
45
46
  const updateSettings = useUpdateSettings();
46
47
  const googleStatus = useGoogleAuthStatus();
47
48
  const disconnectGoogle = useDisconnectGoogle();
49
+ const {
50
+ isDesktopGoogleAuth,
51
+ isGoogleDesktopAuthPending,
52
+ startDesktopGoogleAuth,
53
+ } = useGoogleDesktopAuth({
54
+ onError: (issue) =>
55
+ toast.error(issue.message || issue.error || "Google sign-in failed"),
56
+ onSuccess: () => window.location.reload(),
57
+ });
48
58
  const zoomStatus = useZoomStatus();
49
59
  const connectZoom = useConnectZoom();
50
60
  const disconnectZoom = useDisconnectZoom();
@@ -81,6 +91,12 @@ export default function Settings() {
81
91
  }
82
92
 
83
93
  function handleConnect() {
94
+ if (isDesktopGoogleAuth) {
95
+ startDesktopGoogleAuth({
96
+ previousAccountCount: googleStatus.data?.accounts?.length ?? 0,
97
+ });
98
+ return;
99
+ }
84
100
  setWantAuthUrl(true);
85
101
  }
86
102
 
@@ -179,7 +195,15 @@ export default function Settings() {
179
195
  Disconnect
180
196
  </Button>
181
197
  ) : (
182
- <Button size="sm" onClick={handleConnect}>
198
+ <Button
199
+ size="sm"
200
+ onClick={handleConnect}
201
+ disabled={
202
+ authUrl.isLoading ||
203
+ authUrl.isFetching ||
204
+ isGoogleDesktopAuthPending
205
+ }
206
+ >
183
207
  <IconExternalLink className="mr-1.5 h-3.5 w-3.5" />
184
208
  Connect
185
209
  </Button>
@@ -0,0 +1,9 @@
1
+ import NotFound from "@/pages/NotFound";
2
+
3
+ export function meta() {
4
+ return [{ title: "Not Found - Calendar" }];
5
+ }
6
+
7
+ export default function AppCatchAllRoute() {
8
+ return <NotFound />;
9
+ }