@agent-native/core 0.68.1 → 0.68.3

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 (129) hide show
  1. package/README.md +4 -4
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +16 -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 +35 -2
  7. package/corpus/core/package.json +1 -1
  8. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  9. package/corpus/core/src/client/AgentPanel.tsx +2 -4
  10. package/corpus/core/src/client/CommandMenu.tsx +98 -3
  11. package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
  12. package/corpus/core/src/client/index.ts +2 -0
  13. package/corpus/core/src/extensions/content-patch.ts +12 -2
  14. package/corpus/core/src/oauth-tokens/google-refresh.ts +32 -17
  15. package/corpus/core/src/provider-api/index.ts +37 -17
  16. package/corpus/core/src/server/auth.ts +6 -8
  17. package/corpus/core/src/server/better-auth-instance.ts +19 -4
  18. package/corpus/core/src/server/core-routes-plugin.ts +157 -5
  19. package/corpus/core/src/server/google-oauth-credentials.ts +51 -0
  20. package/corpus/core/src/server/index.ts +8 -0
  21. package/corpus/core/src/server/onboarding-html.ts +2 -1
  22. package/corpus/core/src/server/social-og-image.ts +6 -1
  23. package/corpus/core/src/vite/client.ts +83 -1
  24. package/corpus/templates/analytics/app/routes/$.tsx +9 -0
  25. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +33 -1
  26. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +11 -3
  27. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +25 -2
  28. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +12 -0
  29. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +7 -2
  30. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +37 -78
  31. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +33 -1
  32. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +46 -3
  33. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +203 -2
  34. package/corpus/templates/calendar/app/lib/event-colors.ts +1 -4
  35. package/corpus/templates/calendar/app/lib/rsvp-status.tsx +6 -0
  36. package/corpus/templates/calendar/app/pages/CalendarView.tsx +9 -4
  37. package/corpus/templates/calendar/app/pages/NotFound.tsx +1 -1
  38. package/corpus/templates/calendar/app/pages/Settings.tsx +25 -1
  39. package/corpus/templates/calendar/app/routes/_app.$.tsx +9 -0
  40. package/corpus/templates/calendar/server/handlers/google-auth.ts +152 -12
  41. package/corpus/templates/calendar/server/lib/google-calendar.ts +63 -14
  42. package/corpus/templates/calendar/server/plugins/auth.ts +4 -20
  43. package/corpus/templates/calendar/shared/api.ts +4 -0
  44. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +17 -0
  45. package/corpus/templates/clips/actions/sync-calendars.ts +6 -7
  46. package/corpus/templates/clips/app/components/capture-install-options.tsx +157 -0
  47. package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -8
  48. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +4 -6
  49. package/corpus/templates/clips/app/lib/capture-install-options.ts +14 -0
  50. package/corpus/templates/clips/app/root.tsx +54 -2
  51. package/corpus/templates/clips/app/routes/_app.dictate.tsx +9 -13
  52. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -7
  53. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +5 -7
  54. package/corpus/templates/clips/app/routes/download.tsx +49 -0
  55. package/corpus/templates/clips/app/routes/record.tsx +4 -4
  56. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -6
  57. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +29 -0
  58. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -7
  59. package/corpus/templates/clips/server/lib/google-calendar-client.ts +61 -0
  60. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +19 -12
  61. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -6
  62. package/corpus/templates/content/vite.config.ts +37 -1
  63. package/corpus/templates/design/app/pages/NotFound.tsx +1 -1
  64. package/corpus/templates/design/app/routes/$.tsx +9 -0
  65. package/corpus/templates/mail/app/pages/NotFound.tsx +1 -1
  66. package/corpus/templates/mail/app/routes/$.tsx +9 -0
  67. package/corpus/templates/plan/app/global.css +2 -2
  68. package/corpus/templates/slides/app/pages/NotFound.tsx +1 -1
  69. package/corpus/templates/slides/app/routes/$.tsx +9 -0
  70. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +81 -25
  71. package/corpus/templates/videos/app/pages/NotFound.tsx +1 -1
  72. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  73. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  74. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  75. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  76. package/dist/client/AgentPanel.d.ts +1 -1
  77. package/dist/client/AgentPanel.d.ts.map +1 -1
  78. package/dist/client/AgentPanel.js +1 -1
  79. package/dist/client/AgentPanel.js.map +1 -1
  80. package/dist/client/CommandMenu.d.ts +15 -2
  81. package/dist/client/CommandMenu.d.ts.map +1 -1
  82. package/dist/client/CommandMenu.js +46 -3
  83. package/dist/client/CommandMenu.js.map +1 -1
  84. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  85. package/dist/client/ConnectBuilderCard.js +11 -2
  86. package/dist/client/ConnectBuilderCard.js.map +1 -1
  87. package/dist/client/index.d.ts +1 -1
  88. package/dist/client/index.d.ts.map +1 -1
  89. package/dist/client/index.js.map +1 -1
  90. package/dist/extensions/content-patch.d.ts.map +1 -1
  91. package/dist/extensions/content-patch.js +10 -2
  92. package/dist/extensions/content-patch.js.map +1 -1
  93. package/dist/oauth-tokens/google-refresh.d.ts.map +1 -1
  94. package/dist/oauth-tokens/google-refresh.js +29 -17
  95. package/dist/oauth-tokens/google-refresh.js.map +1 -1
  96. package/dist/provider-api/index.d.ts.map +1 -1
  97. package/dist/provider-api/index.js +28 -16
  98. package/dist/provider-api/index.js.map +1 -1
  99. package/dist/server/auth.d.ts.map +1 -1
  100. package/dist/server/auth.js +6 -6
  101. package/dist/server/auth.js.map +1 -1
  102. package/dist/server/better-auth-instance.d.ts.map +1 -1
  103. package/dist/server/better-auth-instance.js +18 -4
  104. package/dist/server/better-auth-instance.js.map +1 -1
  105. package/dist/server/core-routes-plugin.d.ts +5 -0
  106. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  107. package/dist/server/core-routes-plugin.js +107 -5
  108. package/dist/server/core-routes-plugin.js.map +1 -1
  109. package/dist/server/google-oauth-credentials.d.ts +15 -0
  110. package/dist/server/google-oauth-credentials.d.ts.map +1 -0
  111. package/dist/server/google-oauth-credentials.js +34 -0
  112. package/dist/server/google-oauth-credentials.js.map +1 -0
  113. package/dist/server/index.d.ts +1 -0
  114. package/dist/server/index.d.ts.map +1 -1
  115. package/dist/server/index.js +1 -0
  116. package/dist/server/index.js.map +1 -1
  117. package/dist/server/onboarding-html.d.ts.map +1 -1
  118. package/dist/server/onboarding-html.js +2 -1
  119. package/dist/server/onboarding-html.js.map +1 -1
  120. package/dist/server/social-og-image.d.ts.map +1 -1
  121. package/dist/server/social-og-image.js +5 -1
  122. package/dist/server/social-og-image.js.map +1 -1
  123. package/dist/vite/client.d.ts.map +1 -1
  124. package/dist/vite/client.js +83 -1
  125. package/dist/vite/client.js.map +1 -1
  126. package/docs/content/authentication.md +16 -2
  127. package/docs/content/template-analytics.md +14 -9
  128. package/docs/content/template-clips.md +35 -2
  129. package/package.json +1 -1
@@ -1320,6 +1320,87 @@ function ssrStubPlugin(packages: string[]): Plugin | null {
1320
1320
  if (!packages.length) return null;
1321
1321
  const stubbed = new Set(packages);
1322
1322
  const STUB_ID = "\0agent-native-ssr-stub";
1323
+ const namedExports = [
1324
+ "ActionBarPrimitive",
1325
+ "AllSelection",
1326
+ "Array",
1327
+ "AssistantRuntimeProvider",
1328
+ "Awareness",
1329
+ "BranchPickerPrimitive",
1330
+ "BubbleMenu",
1331
+ "CodeBlockLowlight",
1332
+ "Collaboration",
1333
+ "CollaborationCaret",
1334
+ "ComposerPrimitive",
1335
+ "CompositeAttachmentAdapter",
1336
+ "DOMParser",
1337
+ "Decoration",
1338
+ "DecorationSet",
1339
+ "Editor",
1340
+ "EditorContent",
1341
+ "Extension",
1342
+ "FitAddon",
1343
+ "Fragment",
1344
+ "Image",
1345
+ "Link",
1346
+ "Map",
1347
+ "Markdown",
1348
+ "Mark",
1349
+ "MessagePrimitive",
1350
+ "Node",
1351
+ "NodeSelection",
1352
+ "NodeViewContent",
1353
+ "NodeViewWrapper",
1354
+ "Placeholder",
1355
+ "Plugin",
1356
+ "PluginKey",
1357
+ "ReactNodeViewRenderer",
1358
+ "Selection",
1359
+ "SimpleImageAttachmentAdapter",
1360
+ "SimpleTextAttachmentAdapter",
1361
+ "StarterKit",
1362
+ "Table",
1363
+ "TableCell",
1364
+ "TableHeader",
1365
+ "TableRow",
1366
+ "TaskItem",
1367
+ "TaskList",
1368
+ "Terminal",
1369
+ "TextSelection",
1370
+ "ThreadPrimitive",
1371
+ "WebLinksAddon",
1372
+ "captureException",
1373
+ "common",
1374
+ "createLowlight",
1375
+ "defaultUrlTransform",
1376
+ "extensions",
1377
+ "findTable",
1378
+ "format",
1379
+ "Doc",
1380
+ "getHTMLFromFragment",
1381
+ "getIsolationScope",
1382
+ "init",
1383
+ "isChangeOrigin",
1384
+ "mergeAttributes",
1385
+ "renderToString",
1386
+ "applyUpdate",
1387
+ "encodeStateVector",
1388
+ "encodeStateAsUpdate",
1389
+ "mergeUpdates",
1390
+ "useAui",
1391
+ "useComposer",
1392
+ "useComposerRuntime",
1393
+ "useCurrentEditor",
1394
+ "useEditor",
1395
+ "useLocalRuntime",
1396
+ "useMessagePartText",
1397
+ "useMessageRuntime",
1398
+ "useThread",
1399
+ "useThreadRuntime",
1400
+ "withScope",
1401
+ "XmlFragment",
1402
+ "XmlText",
1403
+ ];
1323
1404
  return {
1324
1405
  name: "agent-native-ssr-stub-heavy-libs",
1325
1406
  enforce: "pre",
@@ -1345,7 +1426,8 @@ function ssrStubPlugin(packages: string[]): Plugin | null {
1345
1426
  "return new Proxy(() => {}, handler); " +
1346
1427
  "} };" +
1347
1428
  "const stub = new Proxy(() => {}, handler);" +
1348
- "export default stub;"
1429
+ "export default stub;" +
1430
+ namedExports.map((name) => `export const ${name} = stub;`).join("")
1349
1431
  );
1350
1432
  },
1351
1433
  };
@@ -0,0 +1,9 @@
1
+ import NotFound from "@/pages/NotFound";
2
+
3
+ export function meta() {
4
+ return [{ title: "Not Found - Analytics" }];
5
+ }
6
+
7
+ export default function CatchAllRoute() {
8
+ return <NotFound />;
9
+ }
@@ -262,7 +262,17 @@ export function DayView({
262
262
  onDraftDiscard={onDraftDiscard}
263
263
  >
264
264
  <button
265
- className="flex w-full items-center gap-1.5 rounded-md px-3 py-1.5 text-left text-sm font-medium text-foreground transition-all hover:brightness-110"
265
+ className={cn(
266
+ "relative flex w-full items-center gap-1.5 rounded-md px-3 py-1.5 text-left text-sm font-medium text-foreground transition-all hover:brightness-110",
267
+ event.ownerColor && "pr-5",
268
+ )}
269
+ aria-label={
270
+ event.ownerName || event.overlayEmail
271
+ ? `${event.title}, ${
272
+ event.ownerName || event.overlayEmail
273
+ }'s calendar`
274
+ : event.title
275
+ }
266
276
  style={
267
277
  color
268
278
  ? {
@@ -283,6 +293,13 @@ export function DayView({
283
293
  )}
284
294
  <EventStatusIcon event={event} className="shrink-0" />
285
295
  <span className="truncate">{event.title}</span>
296
+ {event.ownerColor && (
297
+ <span
298
+ aria-hidden="true"
299
+ className="absolute right-2 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
300
+ style={{ backgroundColor: event.ownerColor }}
301
+ />
302
+ )}
286
303
  </button>
287
304
  </EventDetailPopover>
288
305
  );
@@ -458,7 +475,15 @@ export function DayView({
458
475
  isBeingDragged && isDragging && "ring-2 ring-primary/40",
459
476
  canDrag && isStart && "cursor-grab",
460
477
  isBeingDragged && isDragging && "cursor-grabbing",
478
+ event.ownerColor && "pr-4",
461
479
  )}
480
+ aria-label={
481
+ event.ownerName || event.overlayEmail
482
+ ? `${event.title}, ${
483
+ event.ownerName || event.overlayEmail
484
+ }'s calendar`
485
+ : event.title
486
+ }
462
487
  style={{
463
488
  ...posStyle,
464
489
  left: `${li.left}px`,
@@ -483,6 +508,13 @@ export function DayView({
483
508
  opacity: isBeingDragged && isDragging ? 0.9 : undefined,
484
509
  }}
485
510
  >
511
+ {event.ownerColor && (
512
+ <span
513
+ aria-hidden="true"
514
+ className="absolute right-1.5 top-1.5 size-1.5 rounded-full ring-1 ring-background/70"
515
+ style={{ backgroundColor: event.ownerColor }}
516
+ />
517
+ )}
486
518
  {durationMin <= 30 ? (
487
519
  <div className="flex items-baseline gap-1.5 truncate">
488
520
  {allOthersOut && (
@@ -16,6 +16,7 @@ import { useAttendeePhotos } from "@/hooks/use-attendee-photos";
16
16
  import { useRsvpEvent } from "@/hooks/use-events";
17
17
  import { cn } from "@/lib/utils";
18
18
  import {
19
+ canInlineRsvp,
19
20
  getRsvpStatusLabel,
20
21
  RsvpStatusIcon,
21
22
  type RsvpStatus,
@@ -406,6 +407,7 @@ export function EventAttendeesSection({
406
407
  | "id"
407
408
  | "accountEmail"
408
409
  | "attendees"
410
+ | "overlayEmail"
409
411
  | "responseStatus"
410
412
  | "source"
411
413
  | "recurringEventId"
@@ -423,8 +425,14 @@ export function EventAttendeesSection({
423
425
  const { data: photos } = useAttendeePhotos(emails);
424
426
 
425
427
  const sorted = useMemo(() => sortAttendees(attendees), [attendees]);
426
- const selfAttendee = sorted.find((attendee) => attendee.self);
427
- const others = sorted.filter((attendee) => !attendee.self);
428
+ const canRsvpInline = canInlineRsvp(event);
429
+ const selfAttendee = canRsvpInline
430
+ ? sorted.find((attendee) => attendee.self)
431
+ : undefined;
432
+ const others =
433
+ canRsvpInline && selfAttendee
434
+ ? sorted.filter((attendee) => !attendee.self)
435
+ : sorted;
428
436
 
429
437
  useEffect(() => {
430
438
  setSelfStatus(event.responseStatus || "needsAction");
@@ -505,7 +513,7 @@ export function EventAttendeesSection({
505
513
  attendee={selfAttendee}
506
514
  event={event}
507
515
  photoUrl={photos?.[selfAttendee.email.toLowerCase()]}
508
- inlineRsvp={event.source === "google"}
516
+ inlineRsvp={canRsvpInline}
509
517
  currentStatus={selfStatus}
510
518
  currentNote={selfNote}
511
519
  onResponseChange={handleSelfResponseChange}
@@ -30,6 +30,7 @@ export function EventCard({
30
30
  colorPreferences,
31
31
  }: EventCardProps) {
32
32
  const accentColor = getEventDisplayColor(event, colorPreferences);
33
+ const ownerLabel = event.ownerName || event.overlayEmail;
33
34
 
34
35
  const handleDragStart = (e: React.DragEvent) => {
35
36
  e.dataTransfer.setData("text/plain", event.id);
@@ -47,10 +48,14 @@ export function EventCard({
47
48
  onDragStart={canDrag ? handleDragStart : undefined}
48
49
  onDragEnd={canDrag ? onDragEnd : undefined}
49
50
  className={cn(
50
- "flex w-full items-center gap-1.5 rounded px-1.5 py-0.5 text-left text-xs text-foreground transition-all hover:brightness-110",
51
+ "relative flex w-full items-center gap-1.5 rounded px-1.5 py-0.5 text-left text-xs text-foreground transition-all hover:brightness-110",
51
52
  canDrag && "cursor-grab active:cursor-grabbing",
52
53
  dimmed && "opacity-40",
54
+ event.ownerColor && "pr-3.5",
53
55
  )}
56
+ aria-label={
57
+ ownerLabel ? `${event.title}, ${ownerLabel}'s calendar` : event.title
58
+ }
54
59
  style={{
55
60
  backgroundColor: `${accentColor}25`,
56
61
  }}
@@ -68,6 +73,13 @@ export function EventCard({
68
73
  )}
69
74
  <EventStatusIcon event={event} />
70
75
  <span className="truncate font-medium">{event.title}</span>
76
+ {event.ownerColor && (
77
+ <span
78
+ aria-hidden="true"
79
+ className="absolute right-1 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
80
+ style={{ backgroundColor: event.ownerColor }}
81
+ />
82
+ )}
71
83
  </button>
72
84
  );
73
85
  }
@@ -79,10 +91,14 @@ export function EventCard({
79
91
  onDragStart={canDrag ? handleDragStart : undefined}
80
92
  onDragEnd={canDrag ? onDragEnd : undefined}
81
93
  className={cn(
82
- "flex w-full flex-col gap-0.5 rounded-md px-2 py-1.5 text-left text-xs text-foreground transition-all hover:brightness-110",
94
+ "relative flex w-full flex-col gap-0.5 rounded-md px-2 py-1.5 text-left text-xs text-foreground transition-all hover:brightness-110",
83
95
  canDrag && "cursor-grab active:cursor-grabbing",
84
96
  dimmed && "opacity-40",
97
+ event.ownerColor && "pr-4",
85
98
  )}
99
+ aria-label={
100
+ ownerLabel ? `${event.title}, ${ownerLabel}'s calendar` : event.title
101
+ }
86
102
  style={{
87
103
  backgroundColor: `${accentColor}25`,
88
104
  borderLeft: `2px solid ${accentColor}`,
@@ -106,6 +122,13 @@ export function EventCard({
106
122
  })}
107
123
  </span>
108
124
  )}
125
+ {event.ownerColor && (
126
+ <span
127
+ aria-hidden="true"
128
+ className="absolute right-1.5 top-1.5 size-1.5 rounded-full ring-1 ring-background/70"
129
+ style={{ backgroundColor: event.ownerColor }}
130
+ />
131
+ )}
109
132
  </button>
110
133
  );
111
134
  }
@@ -101,6 +101,7 @@ export function EventDetailPanel({
101
101
  const isOverlay = !!event?.overlayEmail;
102
102
  const lastSavedDescriptionRef = useRef(event?.description || "");
103
103
  const meetingLink = event ? extractMeetingLink(event) : null;
104
+ const ownerLabel = event?.ownerName || event?.overlayEmail;
104
105
 
105
106
  // Reset editing state when event changes
106
107
  useEffect(() => {
@@ -310,6 +311,17 @@ export function EventDetailPanel({
310
311
  </div>
311
312
  )}
312
313
 
314
+ {event.overlayEmail && ownerLabel && (
315
+ <div className="flex items-center gap-2.5 text-sm text-muted-foreground">
316
+ <span
317
+ aria-hidden="true"
318
+ className="ml-0.5 size-2 shrink-0 rounded-full ring-1 ring-border"
319
+ style={{ backgroundColor: event.ownerColor }}
320
+ />
321
+ <span>Viewing {ownerLabel}'s calendar</span>
322
+ </div>
323
+ )}
324
+
313
325
  {meetingLink ? (
314
326
  <a
315
327
  href={safeUrl(meetingLink)}
@@ -395,6 +395,7 @@ export function EventDetailPopover({
395
395
  "meet" | "zoom" | null
396
396
  >(null);
397
397
  const isOverlay = !!event.overlayEmail;
398
+ const ownerLabel = event.ownerName || event.overlayEmail;
398
399
 
399
400
  const updateEvent = useUpdateEvent();
400
401
  const masterEventId =
@@ -2086,9 +2087,13 @@ Write a short, useful meeting description. If I ask you to apply it, update this
2086
2087
  <>
2087
2088
  <div className="mx-4 my-2 border-t border-border/50" />
2088
2089
  <div className="flex items-center gap-3 px-4 py-1.5">
2089
- <IconUser className="h-4 w-4 shrink-0 text-muted-foreground" />
2090
+ <span
2091
+ aria-hidden="true"
2092
+ className="ml-1 size-2 shrink-0 rounded-full ring-1 ring-border"
2093
+ style={{ backgroundColor: event.ownerColor }}
2094
+ />
2090
2095
  <span className="text-sm text-muted-foreground">
2091
- {event.overlayEmail}
2096
+ Viewing {ownerLabel}'s calendar
2092
2097
  </span>
2093
2098
  </div>
2094
2099
  </>
@@ -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" />