@agent-native/core 0.159.3 → 0.160.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 (90) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/assets/server/lib/generation.ts +10 -6
  3. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +18 -4
  4. package/corpus/templates/calendar/actions/create-event.ts +4 -2
  5. package/corpus/templates/calendar/actions/event-action-helpers.ts +56 -9
  6. package/corpus/templates/calendar/actions/update-event.ts +1 -0
  7. package/corpus/templates/calendar/agent-native.config.ts +1 -0
  8. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +8 -2
  9. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +102 -68
  10. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +150 -49
  11. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +57 -23
  12. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +4 -1
  13. package/corpus/templates/calendar/app/components/calendar/TimezoneSwitchDialog.tsx +70 -0
  14. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +127 -88
  15. package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +59 -25
  16. package/corpus/templates/calendar/app/hooks/use-event-drag.ts +55 -18
  17. package/corpus/templates/calendar/app/i18n-data.ts +60 -0
  18. package/corpus/templates/calendar/app/lib/all-day-layout.ts +16 -8
  19. package/corpus/templates/calendar/app/lib/calendar-drafts.ts +56 -0
  20. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +351 -0
  21. package/corpus/templates/calendar/app/lib/event-layout.ts +14 -8
  22. package/corpus/templates/calendar/app/lib/out-of-office.ts +29 -61
  23. package/corpus/templates/calendar/app/lib/working-location.ts +34 -2
  24. package/corpus/templates/calendar/app/pages/CalendarView.tsx +317 -139
  25. package/corpus/templates/calendar/changelog/2026-08-13-calendar-stays-pinned-to-the-saved-timezone-and-asks-before-.md +6 -0
  26. package/corpus/templates/calendar/changelog/2026-08-13-working-locations-can-be-added-from-calendar-days-with-a-ful.md +6 -0
  27. package/corpus/templates/calendar/changelog/2026-08-14-choosing-office-or-other-when-adding-a-working-location-creates.md +6 -0
  28. package/corpus/templates/calendar/changelog/2026-08-14-creating-an-other-working-location-now-keeps-the-custom-name.md +6 -0
  29. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-keep-home-office-or-custom-titles.md +6 -0
  30. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-now-keep-a-home-office-or-custom-title.md +6 -0
  31. package/corpus/templates/calendar/changelog/2026-08-14-turning-a-midnight-ending-working-location-back-to-all-day.md +6 -0
  32. package/corpus/templates/calendar/server/lib/google-calendar.ts +22 -1
  33. package/dist/agent/engine/builder-engine.d.ts +16 -2
  34. package/dist/agent/engine/builder-engine.js +178 -115
  35. package/dist/agent/engine/builtin.js +13 -4
  36. package/dist/agent/engine/credential-errors.d.ts +23 -0
  37. package/dist/agent/engine/credential-errors.js +19 -0
  38. package/dist/agent/engine/error-detail.d.ts +15 -0
  39. package/dist/agent/engine/error-detail.js +29 -0
  40. package/dist/agent/engine/registry.d.ts +14 -3
  41. package/dist/agent/engine/registry.js +106 -61
  42. package/dist/agent/engine/types.d.ts +16 -0
  43. package/dist/agent/engine/types.js +9 -0
  44. package/dist/agent/production-agent.d.ts +7 -0
  45. package/dist/agent/production-agent.js +58 -20
  46. package/dist/agent/run-manager.js +30 -0
  47. package/dist/agent/thread-data-builder.js +4 -0
  48. package/dist/agent/types.d.ts +9 -0
  49. package/dist/client/agent-chat-adapter.js +46 -15
  50. package/dist/client/chat-first/app-pane.js +1 -1
  51. package/dist/client/chat-first/copy.js +1 -0
  52. package/dist/client/error-format.js +25 -2
  53. package/dist/client/sse-event-processor.d.ts +2 -0
  54. package/dist/client/sse-event-processor.js +12 -1
  55. package/dist/feature-flags/index.d.ts +0 -2
  56. package/dist/feature-flags/index.js +3 -2
  57. package/dist/feature-flags/server.d.ts +2 -0
  58. package/dist/feature-flags/server.js +2 -0
  59. package/dist/index.d.ts +2 -1
  60. package/dist/index.js +2 -1
  61. package/dist/jobs/background-automation-runner.js +9 -15
  62. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  63. package/dist/notifications/routes.d.ts +3 -3
  64. package/dist/observability/routes.d.ts +2 -2
  65. package/dist/progress/routes.d.ts +1 -1
  66. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  67. package/dist/resources/handlers.d.ts +1 -1
  68. package/dist/secrets/register-framework-secrets.js +25 -0
  69. package/dist/secrets/routes.d.ts +6 -6
  70. package/dist/server/agent-chat-plugin.js +9 -2
  71. package/dist/server/auth.js +10 -0
  72. package/dist/server/credential-provider.d.ts +80 -0
  73. package/dist/server/credential-provider.js +168 -1
  74. package/dist/server/google-realtime-session.d.ts +1 -1
  75. package/dist/server/google-realtime-session.js +6 -6
  76. package/dist/server/index.d.ts +2 -2
  77. package/dist/server/index.js +7 -2
  78. package/dist/server/onboarding-html.js +16 -257
  79. package/dist/server/realtime-token.d.ts +1 -1
  80. package/dist/server/realtime-voice.js +12 -6
  81. package/dist/server/transcribe-voice.d.ts +1 -1
  82. package/dist/server/transcribe-voice.js +18 -16
  83. package/dist/shared/auth-copy.d.ts +34 -0
  84. package/dist/shared/auth-copy.js +293 -0
  85. package/dist/shared/index.d.ts +1 -0
  86. package/dist/shared/index.js +1 -0
  87. package/dist/transcription/builder-transcription.js +8 -4
  88. package/docs/content/actions-advanced.mdx +1 -1
  89. package/docs/content/portal.mdx +21 -8
  90. package/package.json +3 -1
@@ -51,6 +51,7 @@ import {
51
51
  import { WorkingLocationEditor } from "@/components/calendar/WorkingLocationEditor";
52
52
  import { useCalendarContext } from "@/components/layout/AppLayout";
53
53
  import { Button } from "@/components/ui/button";
54
+ import { Label } from "@/components/ui/label";
54
55
  import {
55
56
  Popover,
56
57
  PopoverTrigger,
@@ -64,6 +65,7 @@ import {
64
65
  SelectValue,
65
66
  } from "@/components/ui/select";
66
67
  import { Skeleton } from "@/components/ui/skeleton";
68
+ import { Switch } from "@/components/ui/switch";
67
69
  import {
68
70
  Tooltip,
69
71
  TooltipContent,
@@ -73,6 +75,11 @@ import {
73
75
  import { useEvent, useUpdateEvent } from "@/hooks/use-events";
74
76
  import { useIsMobile } from "@/hooks/use-mobile";
75
77
  import { useConnectZoom, useZoomStatus } from "@/hooks/use-zoom-auth";
78
+ import { addCalendarDays } from "@/lib/calendar-timezone";
79
+ import {
80
+ getDateKeyInTimezone,
81
+ getDateTimePartsInTimezone,
82
+ } from "@/lib/calendar-timezone";
76
83
  import {
77
84
  attachmentsToDrafts,
78
85
  buildRecurrenceRules,
@@ -83,7 +90,6 @@ import {
83
90
  getEventEndValidationMessage,
84
91
  getLocalTimezone,
85
92
  getRecurrencePreset,
86
- normalizeAllDayEditEndDate,
87
93
  remindersToDraftState,
88
94
  resolveTimeEditScope,
89
95
  type AttachmentDraft,
@@ -103,6 +109,7 @@ import {
103
109
  buildWorkingLocationUpdate,
104
110
  createWorkingLocationDisplayLabels,
105
111
  getWorkingLocationTitle,
112
+ isWorkingLocationDraftReadyToCreate,
106
113
  isWorkingLocationEvent,
107
114
  type WorkingLocationSelection,
108
115
  } from "@/lib/working-location";
@@ -395,19 +402,45 @@ function isUrl(str: string): boolean {
395
402
  return /^https?:\/\//i.test(str.trim());
396
403
  }
397
404
 
398
- /** Convert ISO date string to local date input value (YYYY-MM-DD) */
399
- function toDateInputValue(iso: string): string {
405
+ /** Convert an event date to the calendar's date input value. */
406
+ function toDateInputValue(iso: string, timezone?: string): string {
407
+ if (/^\d{4}-\d{2}-\d{2}$/.test(iso)) return iso;
408
+ const zonedDate = timezone ? getDateKeyInTimezone(iso, timezone) : null;
409
+ if (zonedDate) return zonedDate;
400
410
  const d = parseISO(iso);
401
411
  return format(d, "yyyy-MM-dd");
402
412
  }
403
413
 
404
- function toAllDayEndDateInputValue(iso: string): string {
414
+ function toAllDayEndDateInputValue(iso: string, timezone?: string): string {
415
+ if (/^\d{4}-\d{2}-\d{2}$/.test(iso)) return addCalendarDays(iso, -1);
416
+ const zonedDate = timezone ? getDateKeyInTimezone(iso, timezone) : null;
417
+ if (zonedDate) return addCalendarDays(zonedDate, -1);
405
418
  const d = parseISO(iso);
406
419
  return format(new Date(d.getTime() - 1), "yyyy-MM-dd");
407
420
  }
408
421
 
409
- /** Convert ISO date string to local time input value (HH:mm) */
410
- function toTimeInputValue(iso: string): string {
422
+ /** Timed events ending at 00:00 already store the next date; don't add another day. */
423
+ function inclusiveEndDateForAllDayConversion(
424
+ startDate: string,
425
+ endDate: string,
426
+ endTime: string,
427
+ ): string {
428
+ const bounded = endDate < startDate ? startDate : endDate;
429
+ if (endTime === "00:00" && bounded > startDate) {
430
+ return addCalendarDays(bounded, -1);
431
+ }
432
+ return bounded;
433
+ }
434
+
435
+ /** Convert an event time to the calendar's time input value. */
436
+ function toTimeInputValue(iso: string, timezone?: string): string {
437
+ if (/^\d{4}-\d{2}-\d{2}$/.test(iso)) return "00:00";
438
+ const zonedTime = timezone ? getDateTimePartsInTimezone(iso, timezone) : null;
439
+ if (zonedTime) {
440
+ return `${String(zonedTime.hour).padStart(2, "0")}:${String(
441
+ zonedTime.minute,
442
+ ).padStart(2, "0")}`;
443
+ }
411
444
  const d = parseISO(iso);
412
445
  return format(d, "HH:mm");
413
446
  }
@@ -426,6 +459,7 @@ interface EventDetailPopoverProps {
426
459
  children: React.ReactNode;
427
460
  onDelete: (eventId: string) => void;
428
461
  isDraft?: boolean;
462
+ timezone?: string;
429
463
  /** When true, the popover opens immediately and title is focused for editing */
430
464
  defaultOpen?: boolean;
431
465
  /** Called when the title is changed and should be persisted */
@@ -458,6 +492,7 @@ export function EventDetailPopover({
458
492
  children,
459
493
  onDelete,
460
494
  isDraft = false,
495
+ timezone,
461
496
  defaultOpen = false,
462
497
  onTitleSave,
463
498
  onDismissNew,
@@ -469,6 +504,7 @@ export function EventDetailPopover({
469
504
  const t = useT();
470
505
  const workingLocationLabels = createWorkingLocationDisplayLabels(t);
471
506
  const isMobile = useIsMobile();
507
+ const eventTimezone = event.startTimeZone || timezone || getLocalTimezone();
472
508
  const [open, setOpen] = useState(defaultOpen);
473
509
  const [editingTitle, setEditingTitle] = useState(
474
510
  defaultOpen ? getEditableEventTitle(event) : "",
@@ -489,7 +525,6 @@ export function EventDetailPopover({
489
525
  } = useCalendarContext();
490
526
  const isWorkingLocation = isWorkingLocationEvent(event);
491
527
  const isOutOfOffice = isOutOfOfficeEvent(event);
492
- const isSingleDayWorkingLocation = isWorkingLocation && event.allDay;
493
528
  const editableLocationValue = event.location || "";
494
529
 
495
530
  // Inline editing state
@@ -499,21 +534,23 @@ export function EventDetailPopover({
499
534
  event.description || "",
500
535
  );
501
536
  const [editLocation, setEditLocation] = useState(editableLocationValue);
502
- const [editDate, setEditDate] = useState(() => toDateInputValue(event.start));
537
+ const [editDate, setEditDate] = useState(() =>
538
+ toDateInputValue(event.start, eventTimezone),
539
+ );
503
540
  const [editEndDate, setEditEndDate] = useState(() =>
504
541
  event.allDay
505
- ? toAllDayEndDateInputValue(event.end)
506
- : toDateInputValue(event.end),
542
+ ? toAllDayEndDateInputValue(event.end, eventTimezone)
543
+ : toDateInputValue(event.end, eventTimezone),
507
544
  );
508
545
  const [editStartTime, setEditStartTime] = useState(() =>
509
- toTimeInputValue(event.start),
546
+ toTimeInputValue(event.start, eventTimezone),
510
547
  );
511
548
  const [editEndTime, setEditEndTime] = useState(() =>
512
- toTimeInputValue(event.end),
513
- );
514
- const [editTimezone, setEditTimezone] = useState(
515
- event.startTimeZone || getLocalTimezone(),
549
+ toTimeInputValue(event.end, eventTimezone),
516
550
  );
551
+ const [editTimezone, setEditTimezone] = useState(eventTimezone);
552
+ const isSingleDayWorkingLocation =
553
+ isWorkingLocation && event.allDay && editDate === editEndDate;
517
554
  const [editReminderMode, setEditReminderMode] = useState<ReminderMode>(
518
555
  () => remindersToDraftState(event).mode,
519
556
  );
@@ -615,15 +652,15 @@ export function EventDetailPopover({
615
652
  setEditDescription(event.description || "");
616
653
  if (editingField !== "location") setEditLocation(editableLocationValue);
617
654
  if (editingField !== "time") {
618
- setEditDate(toDateInputValue(event.start));
655
+ setEditDate(toDateInputValue(event.start, eventTimezone));
619
656
  setEditEndDate(
620
657
  event.allDay
621
- ? toAllDayEndDateInputValue(event.end)
622
- : toDateInputValue(event.end),
658
+ ? toAllDayEndDateInputValue(event.end, eventTimezone)
659
+ : toDateInputValue(event.end, eventTimezone),
623
660
  );
624
- setEditStartTime(toTimeInputValue(event.start));
625
- setEditEndTime(toTimeInputValue(event.end));
626
- setEditTimezone(event.startTimeZone || getLocalTimezone());
661
+ setEditStartTime(toTimeInputValue(event.start, eventTimezone));
662
+ setEditEndTime(toTimeInputValue(event.end, eventTimezone));
663
+ setEditTimezone(eventTimezone);
627
664
  setEditTimeScope("single");
628
665
  }
629
666
  if (editingField !== "reminders") {
@@ -652,6 +689,7 @@ export function EventDetailPopover({
652
689
  event.remindersUseDefault,
653
690
  event.attachments,
654
691
  editableLocationValue,
692
+ eventTimezone,
655
693
  ]);
656
694
 
657
695
  // When defaultOpen changes to true (new event created), open the popover
@@ -659,14 +697,14 @@ export function EventDetailPopover({
659
697
  if (defaultOpen) {
660
698
  setOpen(true);
661
699
  setIsEditingTitle(true);
662
- isNewEventRef.current = true;
700
+ isNewEventRef.current = isDraft;
663
701
  setEditingTitle((current) => {
664
702
  const eventTitle = getEditableEventTitle(event);
665
703
  if (current.trim() && current !== eventTitle) return current;
666
704
  return eventTitle;
667
705
  });
668
706
  }
669
- }, [defaultOpen, event.title, event.titleIsGenerated]);
707
+ }, [defaultOpen, event.title, event.titleIsGenerated, isDraft]);
670
708
 
671
709
  // Focus title input when editing starts
672
710
  useEffect(() => {
@@ -998,32 +1036,29 @@ export function EventDetailPopover({
998
1036
  );
999
1037
 
1000
1038
  const saveTimeValues = useCallback(
1001
- (values: TimeEditValues) => {
1002
- const normalizedEndDate = normalizeAllDayEditEndDate(
1003
- isSingleDayWorkingLocation,
1004
- values.date,
1005
- values.endDate,
1006
- );
1007
- const allDayEnd = new Date(`${normalizedEndDate}T00:00:00`);
1008
- allDayEnd.setDate(allDayEnd.getDate() + 1);
1009
- const newStart = event.allDay
1010
- ? new Date(`${values.date}T00:00:00`).toISOString()
1039
+ (values: TimeEditValues, nextAllDay = event.allDay) => {
1040
+ const newStart = nextAllDay
1041
+ ? values.date
1011
1042
  : dateTimeInTimezoneToIso(
1012
1043
  values.date,
1013
1044
  values.startTime,
1014
1045
  values.timezone,
1015
1046
  );
1016
- const newEnd = event.allDay
1017
- ? allDayEnd.toISOString()
1047
+ const newEnd = nextAllDay
1048
+ ? addCalendarDays(values.endDate, 1)
1018
1049
  : dateTimeInTimezoneToIso(
1019
1050
  values.endDate,
1020
1051
  values.endTime,
1021
1052
  values.timezone,
1022
1053
  );
1023
- if (new Date(newEnd).getTime() <= new Date(newStart).getTime()) {
1054
+ if (
1055
+ nextAllDay
1056
+ ? values.endDate < values.date
1057
+ : new Date(newEnd).getTime() <= new Date(newStart).getTime()
1058
+ ) {
1024
1059
  toast.error(
1025
1060
  getEventEndValidationMessage({
1026
- allDay: event.allDay ?? false,
1061
+ allDay: nextAllDay,
1027
1062
  startDate: values.date,
1028
1063
  endDate: values.endDate,
1029
1064
  startTime: values.startTime,
@@ -1037,9 +1072,9 @@ export function EventDetailPopover({
1037
1072
  saved = saveField({
1038
1073
  start: newStart,
1039
1074
  end: newEnd,
1040
- allDay: event.allDay,
1041
- startTimeZone: event.allDay ? undefined : values.timezone,
1042
- endTimeZone: event.allDay ? undefined : values.timezone,
1075
+ allDay: nextAllDay,
1076
+ startTimeZone: nextAllDay ? undefined : values.timezone,
1077
+ endTimeZone: nextAllDay ? undefined : values.timezone,
1043
1078
  scope: resolveTimeEditScope(
1044
1079
  isRecurringEvent,
1045
1080
  isSingleDayWorkingLocation,
@@ -1061,6 +1096,49 @@ export function EventDetailPopover({
1061
1096
  ],
1062
1097
  );
1063
1098
 
1099
+ const handleWorkingLocationAllDayChange = useCallback(
1100
+ (nextAllDay: boolean) => {
1101
+ const nextStartTime =
1102
+ !nextAllDay && editStartTime === "00:00" && editEndTime === "00:00"
1103
+ ? "09:00"
1104
+ : editStartTime;
1105
+ const nextEndTime =
1106
+ !nextAllDay && editStartTime === "00:00" && editEndTime === "00:00"
1107
+ ? "17:00"
1108
+ : editEndTime;
1109
+ const nextEndDate = nextAllDay
1110
+ ? inclusiveEndDateForAllDayConversion(
1111
+ editDate,
1112
+ editEndDate,
1113
+ editEndTime,
1114
+ )
1115
+ : editEndDate < editDate
1116
+ ? editDate
1117
+ : editEndDate;
1118
+ setEditEndDate(nextEndDate);
1119
+ setEditStartTime(nextStartTime);
1120
+ setEditEndTime(nextEndTime);
1121
+ saveTimeValues(
1122
+ {
1123
+ date: editDate,
1124
+ endDate: nextEndDate,
1125
+ startTime: nextStartTime,
1126
+ endTime: nextEndTime,
1127
+ timezone: editTimezone,
1128
+ },
1129
+ nextAllDay,
1130
+ );
1131
+ },
1132
+ [
1133
+ editDate,
1134
+ editEndDate,
1135
+ editEndTime,
1136
+ editStartTime,
1137
+ editTimezone,
1138
+ saveTimeValues,
1139
+ ],
1140
+ );
1141
+
1064
1142
  const handleInlineTimeChange = useCallback(
1065
1143
  (field: "startTime" | "endTime", nextValue: string) => {
1066
1144
  let nextDate = editDate;
@@ -1182,10 +1260,10 @@ export function EventDetailPopover({
1182
1260
 
1183
1261
  const handleSelectFindTimeSlot = useCallback(
1184
1262
  (slot: FindTimeSlot) => {
1185
- setEditDate(toDateInputValue(slot.start));
1186
- setEditEndDate(toDateInputValue(slot.end));
1187
- setEditStartTime(toTimeInputValue(slot.start));
1188
- setEditEndTime(toTimeInputValue(slot.end));
1263
+ setEditDate(toDateInputValue(slot.start, findTimeTimezone));
1264
+ setEditEndDate(toDateInputValue(slot.end, findTimeTimezone));
1265
+ setEditStartTime(toTimeInputValue(slot.start, findTimeTimezone));
1266
+ setEditEndTime(toTimeInputValue(slot.end, findTimeTimezone));
1189
1267
  setEditTimezone(findTimeTimezone);
1190
1268
  setEditingField(null);
1191
1269
  setFindTimeOpen(false);
@@ -1312,6 +1390,7 @@ export function EventDetailPopover({
1312
1390
 
1313
1391
  const handleCreateDraft = useCallback(() => {
1314
1392
  if (!onDraftCreate) return;
1393
+ if (!isWorkingLocationDraftReadyToCreate(event)) return;
1315
1394
  const title = editingTitle.trim();
1316
1395
  const updates = isEditingTitle && title ? { title } : undefined;
1317
1396
  if (updates) {
@@ -1586,11 +1665,29 @@ export function EventDetailPopover({
1586
1665
  <div className="flex items-start gap-3 rounded-md py-1.5">
1587
1666
  <IconClock className="mt-1 h-4 w-4 shrink-0 text-muted-foreground" />
1588
1667
  <div className="min-w-0 flex-1">
1668
+ {isWorkingLocation && (
1669
+ <div className="mb-1 flex items-center gap-2">
1670
+ <Switch
1671
+ id={`working-location-all-day-${event.id}`}
1672
+ checked={event.allDay}
1673
+ onCheckedChange={handleWorkingLocationAllDayChange}
1674
+ disabled={updateEvent.isPending}
1675
+ />
1676
+ <Label
1677
+ htmlFor={`working-location-all-day-${event.id}`}
1678
+ className="text-xs"
1679
+ >
1680
+ {t("eventForm.allDay")}
1681
+ </Label>
1682
+ </div>
1683
+ )}
1589
1684
  {event.allDay ? (
1590
1685
  <div className="flex flex-wrap items-center gap-1 text-sm">
1591
- <span className="text-muted-foreground">
1592
- {t("eventForm.allDay")}
1593
- </span>
1686
+ {!isWorkingLocation && (
1687
+ <span className="text-muted-foreground">
1688
+ {t("eventForm.allDay")}
1689
+ </span>
1690
+ )}
1594
1691
  <DatePickerPopover
1595
1692
  value={editDate}
1596
1693
  label={t("eventForm.startDate")}
@@ -1598,7 +1695,7 @@ export function EventDetailPopover({
1598
1695
  handleInlineDateChange("date", value)
1599
1696
  }
1600
1697
  />
1601
- {editEndDate !== editDate && (
1698
+ {(isWorkingLocation || editEndDate !== editDate) && (
1602
1699
  <>
1603
1700
  <span className="text-muted-foreground/50">→</span>
1604
1701
  <DatePickerPopover
@@ -1715,7 +1812,9 @@ export function EventDetailPopover({
1715
1812
  onOpenChange={setFindTimeOpen}
1716
1813
  title={t("eventForm.findTime")}
1717
1814
  subtitle={event.title}
1718
- date={editDate || toDateInputValue(event.start)}
1815
+ date={
1816
+ editDate || toDateInputValue(event.start, findTimeTimezone)
1817
+ }
1719
1818
  timezone={findTimeTimezone}
1720
1819
  durationMinutes={findTimeDurationMinutes}
1721
1820
  attendees={schedulingAttendees}
@@ -1733,6 +1832,7 @@ export function EventDetailPopover({
1733
1832
  <WorkingLocationEditor
1734
1833
  event={event}
1735
1834
  isRecurring={isRecurringEvent}
1835
+ isDraft={isDraft}
1736
1836
  readOnly={isOverlay}
1737
1837
  disabled={updateEvent.isPending}
1738
1838
  onSave={handleSaveWorkingLocation}
@@ -2422,6 +2522,7 @@ export function EventDetailPopover({
2422
2522
  <Button
2423
2523
  size="sm"
2424
2524
  className="ml-auto text-xs"
2525
+ disabled={!isWorkingLocationDraftReadyToCreate(event)}
2425
2526
  onClick={handleCreateDraft}
2426
2527
  >
2427
2528
  {event.attendees?.length
@@ -1,23 +1,35 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
1
2
  import type { CalendarEvent } from "@shared/api";
2
3
  import { getWeekdayOrder } from "@shared/calendar-week";
4
+ import { IconPlus } from "@tabler/icons-react";
3
5
  import {
4
6
  startOfMonth,
5
7
  endOfMonth,
6
8
  startOfWeek,
7
9
  endOfWeek,
8
10
  eachDayOfInterval,
9
- startOfDay,
10
- addDays,
11
11
  isSameMonth,
12
12
  isSameDay,
13
- isToday,
14
13
  format,
15
- parseISO,
16
14
  } from "date-fns";
17
15
  import { memo, useState, useMemo } from "react";
18
16
 
17
+ import {
18
+ Tooltip,
19
+ TooltipContent,
20
+ TooltipTrigger,
21
+ } from "@/components/ui/tooltip";
19
22
  import { useIsMobile } from "@/hooks/use-mobile";
20
23
  import { useViewPreferences } from "@/hooks/use-view-preferences";
24
+ import {
25
+ dateToCalendarDateKey,
26
+ addCalendarDays,
27
+ dateKeyToDate,
28
+ eventOverlapsCalendarDay,
29
+ getBrowserTimezone,
30
+ getDateKeyInTimezone,
31
+ getEventDateKey,
32
+ } from "@/lib/calendar-timezone";
21
33
  import { getFullDayOutOfOfficeDateRange } from "@/lib/out-of-office";
22
34
  import { shouldSuppressAfterPopoverClose } from "@/lib/popover-click-guard";
23
35
  import { cn } from "@/lib/utils";
@@ -28,7 +40,9 @@ import { EventDetailPopover } from "./EventDetailPopover";
28
40
  interface MonthViewProps {
29
41
  events: CalendarEvent[];
30
42
  selectedDate: Date;
43
+ timezone?: string;
31
44
  onDateSelect: (date: Date) => void;
45
+ onCreateWorkingLocation?: (date: Date) => void;
32
46
  onDeleteEvent?: (eventId: string) => void;
33
47
  onEventDrop?: (eventId: string, newDate: Date) => void;
34
48
  draftEventIds?: string[];
@@ -79,7 +93,9 @@ interface DayOccurrence {
79
93
  export const MonthView = memo(function MonthView({
80
94
  events,
81
95
  selectedDate,
96
+ timezone = getBrowserTimezone(),
82
97
  onDateSelect,
98
+ onCreateWorkingLocation,
83
99
  onDeleteEvent,
84
100
  onEventDrop,
85
101
  draftEventIds = [],
@@ -89,6 +105,7 @@ export const MonthView = memo(function MonthView({
89
105
  isLoading = false,
90
106
  weekStartsOn = 0,
91
107
  }: MonthViewProps) {
108
+ const t = useT();
92
109
  const isMobile = useIsMobile();
93
110
  const { prefs } = useViewPreferences();
94
111
  const [dragOverDay, setDragOverDay] = useState<string | null>(null);
@@ -119,7 +136,7 @@ export const MonthView = memo(function MonthView({
119
136
  const fullDayOutOfOfficeRange = getFullDayOutOfOfficeDateRange(e);
120
137
  if (fullDayOutOfOfficeRange) {
121
138
  for (const day of days) {
122
- const key = format(day, "yyyy-MM-dd");
139
+ const key = dateToCalendarDateKey(day);
123
140
  if (
124
141
  key < fullDayOutOfOfficeRange.startDate ||
125
142
  key >= fullDayOutOfOfficeRange.endDateExclusive
@@ -130,7 +147,7 @@ export const MonthView = memo(function MonthView({
130
147
  event: e,
131
148
  isStart: key === fullDayOutOfOfficeRange.startDate,
132
149
  continuesNext:
133
- format(addDays(day, 1), "yyyy-MM-dd") <
150
+ addCalendarDays(key, 1) <
134
151
  fullDayOutOfOfficeRange.endDateExclusive,
135
152
  };
136
153
  const list = map.get(key);
@@ -139,17 +156,17 @@ export const MonthView = memo(function MonthView({
139
156
  }
140
157
  continue;
141
158
  }
142
- const evStart = parseISO(e.start);
143
- const evEnd = e.end ? parseISO(e.end) : addDays(evStart, 1);
144
159
  for (const day of days) {
145
- const dayStart = startOfDay(day);
146
- const dayEnd = addDays(dayStart, 1);
147
- if (evStart < dayEnd && evEnd > dayStart) {
148
- const key = format(day, "yyyy-MM-dd");
160
+ if (eventOverlapsCalendarDay(e, day, timezone)) {
161
+ const key = dateToCalendarDateKey(day);
149
162
  const occurrence: DayOccurrence = {
150
163
  event: e,
151
- isStart: evStart >= dayStart && evStart < dayEnd,
152
- continuesNext: evEnd > dayEnd,
164
+ isStart: getEventDateKey(e, timezone) === key,
165
+ continuesNext: eventOverlapsCalendarDay(
166
+ e,
167
+ dateKeyToDate(addCalendarDays(key, 1)),
168
+ timezone,
169
+ ),
153
170
  };
154
171
  const list = map.get(key);
155
172
  if (list) list.push(occurrence);
@@ -158,7 +175,7 @@ export const MonthView = memo(function MonthView({
158
175
  }
159
176
  }
160
177
  return map;
161
- }, [events, days]);
178
+ }, [days, events, timezone]);
162
179
 
163
180
  function handleDragOver(e: React.DragEvent, dayKey: string) {
164
181
  e.preventDefault();
@@ -200,11 +217,13 @@ export const MonthView = memo(function MonthView({
200
217
  >
201
218
  {days.map((day) => {
202
219
  const dayOccurrences =
203
- eventsByDay.get(format(day, "yyyy-MM-dd")) ?? [];
220
+ eventsByDay.get(dateToCalendarDateKey(day)) ?? [];
204
221
  const inMonth = isSameMonth(day, selectedDate);
205
- const today = isToday(day);
222
+ const today =
223
+ dateToCalendarDateKey(day) ===
224
+ getDateKeyInTimezone(new Date(), timezone);
206
225
  const selected = isSameDay(day, selectedDate);
207
- const dayKey = day.toISOString();
226
+ const dayKey = dateToCalendarDateKey(day);
208
227
  const isDragTarget = dragOverDay === dayKey;
209
228
 
210
229
  return (
@@ -249,11 +268,25 @@ export const MonthView = memo(function MonthView({
249
268
  {format(day, "d")}
250
269
  </span>
251
270
 
252
- {/* Subtle "+" on hover */}
253
- {inMonth && (
254
- <span className="mr-0.5 text-xs text-muted-foreground opacity-0 transition-opacity group-hover:opacity-60">
255
- +
256
- </span>
271
+ {inMonth && onCreateWorkingLocation && (
272
+ <Tooltip>
273
+ <TooltipTrigger asChild>
274
+ <button
275
+ type="button"
276
+ aria-label={t("calendarView.addWorkingLocation")}
277
+ className="mr-0.5 flex size-5 items-center justify-center rounded text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover:opacity-100"
278
+ onClick={(event) => {
279
+ event.stopPropagation();
280
+ onCreateWorkingLocation(day);
281
+ }}
282
+ >
283
+ <IconPlus aria-hidden="true" className="size-3.5" />
284
+ </button>
285
+ </TooltipTrigger>
286
+ <TooltipContent side="top">
287
+ {t("calendarView.addWorkingLocation")}
288
+ </TooltipContent>
289
+ </Tooltip>
257
290
  )}
258
291
  </div>
259
292
 
@@ -274,6 +307,7 @@ export const MonthView = memo(function MonthView({
274
307
  <EventDetailPopover
275
308
  key={event.id}
276
309
  event={event}
310
+ timezone={timezone}
277
311
  onDelete={onDeleteEvent ?? (() => {})}
278
312
  isDraft={draftEventIds.includes(event.id)}
279
313
  onDraftUpdate={onDraftUpdate}
@@ -8,6 +8,7 @@ import { EventDetailPopover } from "./EventDetailPopover";
8
8
  interface OutOfOfficeEventProps {
9
9
  event: CalendarEvent;
10
10
  day: Date;
11
+ timezone?: string;
11
12
  hourHeight: number;
12
13
  color: string;
13
14
  label: string;
@@ -51,6 +52,7 @@ interface OutOfOfficeEventProps {
51
52
  export function OutOfOfficeEvent({
52
53
  event,
53
54
  day,
55
+ timezone,
54
56
  hourHeight,
55
57
  color,
56
58
  label,
@@ -76,7 +78,7 @@ export function OutOfOfficeEvent({
76
78
  onDraftDiscard,
77
79
  onOpenChange,
78
80
  }: OutOfOfficeEventProps) {
79
- const segment = getOutOfOfficeSegment(event, day);
81
+ const segment = getOutOfOfficeSegment(event, day, timezone);
80
82
  const hasDragOverride =
81
83
  isBeingDragged &&
82
84
  isDragTargetDay &&
@@ -114,6 +116,7 @@ export function OutOfOfficeEvent({
114
116
  </div>
115
117
  <EventDetailPopover
116
118
  event={event}
119
+ timezone={timezone}
117
120
  onDelete={onDelete}
118
121
  isDraft={isDraft}
119
122
  defaultOpen={defaultOpen}
@@ -0,0 +1,70 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ } from "@/components/ui/alert-dialog";
13
+
14
+ interface TimezoneSwitchDialogProps {
15
+ open: boolean;
16
+ savedTimezone: string;
17
+ browserTimezone: string;
18
+ isSwitching?: boolean;
19
+ onKeep: () => void;
20
+ onSwitch: () => void;
21
+ onOpenChange: (open: boolean) => void;
22
+ }
23
+
24
+ export function TimezoneSwitchDialog({
25
+ open,
26
+ savedTimezone,
27
+ browserTimezone,
28
+ isSwitching = false,
29
+ onKeep,
30
+ onSwitch,
31
+ onOpenChange,
32
+ }: TimezoneSwitchDialogProps) {
33
+ const t = useT();
34
+
35
+ return (
36
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
37
+ <AlertDialogContent>
38
+ <AlertDialogHeader>
39
+ <AlertDialogTitle>
40
+ {t("calendarView.timezoneSwitchTitle")}
41
+ </AlertDialogTitle>
42
+ <AlertDialogDescription>
43
+ {t("calendarView.timezoneSwitchDescription", {
44
+ savedTimezone,
45
+ browserTimezone,
46
+ })}
47
+ </AlertDialogDescription>
48
+ </AlertDialogHeader>
49
+ <AlertDialogFooter>
50
+ <AlertDialogCancel onClick={onKeep}>
51
+ {t("calendarView.timezoneSwitchKeep", {
52
+ timezone: savedTimezone,
53
+ })}
54
+ </AlertDialogCancel>
55
+ <AlertDialogAction
56
+ disabled={isSwitching}
57
+ onClick={(event) => {
58
+ event.preventDefault();
59
+ onSwitch();
60
+ }}
61
+ >
62
+ {t("calendarView.timezoneSwitchSwitch", {
63
+ timezone: browserTimezone,
64
+ })}
65
+ </AlertDialogAction>
66
+ </AlertDialogFooter>
67
+ </AlertDialogContent>
68
+ </AlertDialog>
69
+ );
70
+ }