@agent-native/core 0.159.5 → 0.160.1

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 (81) 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 +68 -31
  46. package/dist/agent/run-manager.js +30 -0
  47. package/dist/agent/thread-data-builder.js +4 -0
  48. package/dist/agent/tool-approval-migrations.d.ts +1 -0
  49. package/dist/agent/tool-approval-migrations.js +10 -0
  50. package/dist/agent/tool-approval-store.d.ts +5 -3
  51. package/dist/agent/tool-approval-store.js +11 -6
  52. package/dist/agent/types.d.ts +9 -0
  53. package/dist/cli/code-agent-connector.js +79 -7
  54. package/dist/cli/portal-transfer.d.ts +43 -0
  55. package/dist/cli/portal-transfer.js +201 -0
  56. package/dist/client/agent-chat-adapter.js +46 -15
  57. package/dist/client/error-format.js +25 -2
  58. package/dist/client/sse-event-processor.d.ts +2 -0
  59. package/dist/client/sse-event-processor.js +12 -1
  60. package/dist/collab/awareness.d.ts +2 -2
  61. package/dist/collab/struct-routes.d.ts +1 -1
  62. package/dist/jobs/background-automation-runner.js +9 -15
  63. package/dist/notifications/routes.d.ts +6 -6
  64. package/dist/observability/routes.d.ts +5 -5
  65. package/dist/resources/handlers.d.ts +1 -1
  66. package/dist/secrets/register-framework-secrets.js +25 -0
  67. package/dist/secrets/routes.d.ts +9 -9
  68. package/dist/server/agent-chat-plugin.js +9 -2
  69. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  70. package/dist/server/credential-provider.d.ts +80 -0
  71. package/dist/server/credential-provider.js +168 -1
  72. package/dist/server/google-realtime-session.d.ts +1 -1
  73. package/dist/server/google-realtime-session.js +6 -6
  74. package/dist/server/index.d.ts +1 -1
  75. package/dist/server/index.js +6 -1
  76. package/dist/server/realtime-token.d.ts +1 -1
  77. package/dist/server/realtime-voice.js +12 -6
  78. package/dist/server/transcribe-voice.d.ts +1 -1
  79. package/dist/server/transcribe-voice.js +18 -16
  80. package/dist/transcription/builder-transcription.js +8 -4
  81. package/package.json +3 -3
@@ -18,8 +18,6 @@ import {
18
18
  import { useQueryClient } from "@tanstack/react-query";
19
19
  import {
20
20
  format,
21
- startOfMonth,
22
- endOfMonth,
23
21
  startOfWeek,
24
22
  endOfWeek,
25
23
  addMonths,
@@ -28,8 +26,6 @@ import {
28
26
  subWeeks,
29
27
  addDays,
30
28
  subDays,
31
- parseISO,
32
- startOfDay,
33
29
  } from "date-fns";
34
30
  import { useState, useMemo, useEffect, useCallback, useRef } from "react";
35
31
  import { Link } from "react-router";
@@ -48,6 +44,7 @@ import {
48
44
  } from "@/components/calendar/GuestNotificationDialog";
49
45
  import { MonthView } from "@/components/calendar/MonthView";
50
46
  import { PeopleSearchDialog } from "@/components/calendar/PeopleSearchDialog";
47
+ import { TimezoneSwitchDialog } from "@/components/calendar/TimezoneSwitchDialog";
51
48
  import { WeekView } from "@/components/calendar/WeekView";
52
49
  import { useCalendarContext } from "@/components/layout/AppLayout";
53
50
  import type { ViewMode } from "@/components/layout/AppLayout";
@@ -81,9 +78,26 @@ import { useGoogleAuthStatus } from "@/hooks/use-google-auth";
81
78
  import { useMeetingStartNotifications } from "@/hooks/use-meeting-start-notifications";
82
79
  import { useIsMobile } from "@/hooks/use-mobile";
83
80
  import { useOverlayPeople } from "@/hooks/use-overlay-people";
84
- import { useSettings } from "@/hooks/use-settings";
81
+ import { useSettings, useUpdateSettings } from "@/hooks/use-settings";
85
82
  import { setUndoAction, runUndo } from "@/hooks/use-undo";
86
83
  import { useViewPreferences } from "@/hooks/use-view-preferences";
84
+ import {
85
+ buildWorkingLocationDraft,
86
+ resolveDraftWorkingLocation,
87
+ } from "@/lib/calendar-drafts";
88
+ import {
89
+ addCalendarDays,
90
+ dateKeyToDate,
91
+ dateKeyToTimezoneIso,
92
+ dateToCalendarDateKey,
93
+ eventOverlapsCalendarDay,
94
+ getBrowserTimezone,
95
+ getDateKeyInTimezone,
96
+ getEventDateKey,
97
+ getViewDateRange,
98
+ moveEventToCalendarDate,
99
+ normalizeTimezone,
100
+ } from "@/lib/calendar-timezone";
87
101
  import { resolveEventAccountEmail } from "@/lib/event-account-selection";
88
102
  import { getGoogleEventColorHex } from "@/lib/event-colors";
89
103
  import {
@@ -97,8 +111,19 @@ import { buildDeleteEventMutationInput } from "@/lib/event-mutation-inputs";
97
111
  import { getLocationSuggestions } from "@/lib/location-suggestions";
98
112
  import { isMcpEmbedSurface } from "@/lib/mcp-embed";
99
113
  import { cn } from "@/lib/utils";
114
+ import {
115
+ buildWorkingLocationProperties,
116
+ findOwnedWorkingLocationForDay,
117
+ } from "@/lib/working-location";
100
118
 
101
119
  const CALENDAR_DRAFT_EVENT_PREFIX = "calendar-draft-event:";
120
+ const TIMEZONE_DISMISSAL_PREFIX = "calendar.timezone.dismissed.";
121
+
122
+ function timezoneDismissalKey(savedTimezone: string, browserTimezone: string) {
123
+ return `${TIMEZONE_DISMISSAL_PREFIX}${encodeURIComponent(
124
+ savedTimezone,
125
+ )}:${encodeURIComponent(browserTimezone)}`;
126
+ }
102
127
 
103
128
  type DraftEventPatch = Partial<CalendarEvent> & {
104
129
  fullDay?: boolean;
@@ -176,6 +201,14 @@ function draftRange(draft: CalendarEventDraft, fallbackDate: Date) {
176
201
  const fallback = fallbackDraftRange(fallbackDate);
177
202
  const fullDayTimezone = draft.startTimeZone ?? draft.endTimeZone;
178
203
  const fullDayDatePattern = /^\d{4}-\d{2}-\d{2}$/;
204
+ const dateOnlyAllDay =
205
+ draft.allDay === true &&
206
+ Boolean(
207
+ draft.start &&
208
+ draft.end &&
209
+ fullDayDatePattern.test(draft.start) &&
210
+ fullDayDatePattern.test(draft.end),
211
+ );
179
212
  const semanticFullDay =
180
213
  draft.eventType === "outOfOffice" &&
181
214
  draft.fullDay &&
@@ -184,18 +217,24 @@ function draftRange(draft: CalendarEventDraft, fallbackDate: Date) {
184
217
  draft.end &&
185
218
  fullDayDatePattern.test(draft.start) &&
186
219
  fullDayDatePattern.test(draft.end);
187
- const start = semanticFullDay
188
- ? new Date(dateTimeInTimezoneToIso(draft.start!, "00:00", fullDayTimezone!))
189
- : (parseValidDate(draft.start) ?? fallback.start);
190
- const parsedEnd = semanticFullDay
191
- ? new Date(
192
- dateTimeInTimezoneToIso(
193
- format(addDays(parseISO(draft.end!), 1), "yyyy-MM-dd"),
194
- "00:00",
195
- fullDayTimezone!,
196
- ),
197
- )
198
- : parseValidDate(draft.end);
220
+ const start = dateOnlyAllDay
221
+ ? dateKeyToDate(draft.start!)
222
+ : semanticFullDay
223
+ ? new Date(
224
+ dateTimeInTimezoneToIso(draft.start!, "00:00", fullDayTimezone!),
225
+ )
226
+ : (parseValidDate(draft.start) ?? fallback.start);
227
+ const parsedEnd = dateOnlyAllDay
228
+ ? dateKeyToDate(draft.end!)
229
+ : semanticFullDay
230
+ ? new Date(
231
+ dateTimeInTimezoneToIso(
232
+ addCalendarDays(draft.end!, 1),
233
+ "00:00",
234
+ fullDayTimezone!,
235
+ ),
236
+ )
237
+ : parseValidDate(draft.end);
199
238
  const end =
200
239
  parsedEnd && parsedEnd.getTime() > start.getTime()
201
240
  ? parsedEnd
@@ -209,24 +248,37 @@ function draftToCalendarEvent(
209
248
  ): CalendarEvent {
210
249
  const { start, end } = draftRange(draft, fallbackDate);
211
250
  const editableTitle = draft.title?.trim() ?? "";
251
+ const allDay =
252
+ draft.eventType === "outOfOffice" && draft.fullDay
253
+ ? false
254
+ : (draft.allDay ?? false);
255
+ const dateOnlyAllDay =
256
+ allDay &&
257
+ Boolean(
258
+ draft.start &&
259
+ draft.end &&
260
+ /^\d{4}-\d{2}-\d{2}$/.test(draft.start) &&
261
+ /^\d{4}-\d{2}-\d{2}$/.test(draft.end),
262
+ );
263
+ const { workingLocationType, workingLocationLabel } =
264
+ resolveDraftWorkingLocation(draft);
212
265
  return {
213
266
  id: calendarDraftEventId(draft.id),
214
267
  title:
215
268
  editableTitle ||
216
269
  (draft.eventType === "outOfOffice"
217
270
  ? "Out of office"
218
- : UNNAMED_EVENT_TITLE),
271
+ : draft.eventType === "workingLocation"
272
+ ? "Working location"
273
+ : UNNAMED_EVENT_TITLE),
219
274
  titleIsGenerated: !editableTitle,
220
275
  description: draft.description ?? "",
221
- start: start.toISOString(),
222
- end: end.toISOString(),
276
+ start: dateOnlyAllDay ? draft.start! : start.toISOString(),
277
+ end: dateOnlyAllDay ? draft.end! : end.toISOString(),
223
278
  startTimeZone: draft.startTimeZone,
224
279
  endTimeZone: draft.endTimeZone ?? draft.startTimeZone,
225
- location: draft.location ?? draft.workingLocationLabel ?? "",
226
- allDay:
227
- draft.eventType === "outOfOffice" && draft.fullDay
228
- ? false
229
- : (draft.allDay ?? false),
280
+ location: draft.location || workingLocationLabel,
281
+ allDay,
230
282
  source: "local",
231
283
  accountEmail: draft.accountEmail,
232
284
  colorId: draft.colorId,
@@ -234,6 +286,16 @@ function draftToCalendarEvent(
234
286
  transparency: draft.transparency,
235
287
  visibility: draft.visibility,
236
288
  eventType: draft.eventType ?? "default",
289
+ workingLocationProperties:
290
+ draft.eventType === "workingLocation"
291
+ ? buildWorkingLocationProperties(
292
+ { workingLocationProperties: undefined },
293
+ {
294
+ type: workingLocationType,
295
+ label: workingLocationLabel,
296
+ },
297
+ )
298
+ : undefined,
237
299
  outOfOfficeProperties: draft.outOfOfficeProperties,
238
300
  recurrence: draft.recurrence,
239
301
  attendees: draft.attendees,
@@ -273,6 +335,10 @@ function applyDraftPatch(
273
335
  copy("location");
274
336
  copy("allDay");
275
337
  copy("fullDay");
338
+ if (patch.allDay === true) {
339
+ delete next.startTimeZone;
340
+ delete next.endTimeZone;
341
+ }
276
342
  copy("eventType");
277
343
  copy("outOfOfficeProperties");
278
344
  copy("transparency");
@@ -361,6 +427,7 @@ export default function CalendarView() {
361
427
  Record<string, string>
362
428
  >({});
363
429
  const openedDraftIdRef = useRef<string | null>(null);
430
+ const preserveDraftViewRef = useRef(false);
364
431
  const committingDraftIdsRef = useRef<Set<string>>(new Set());
365
432
  const discardedCommittingDraftsRef = useRef<Map<string, CalendarEventDraft>>(
366
433
  new Map(),
@@ -375,6 +442,12 @@ export default function CalendarView() {
375
442
  const settingsQuery = useSettings();
376
443
  const { data: settings } = settingsQuery;
377
444
  const weekStartsOn = getWeekStartsOn(settings?.weekStart);
445
+ const updateSettings = useUpdateSettings();
446
+ const displayTimezone = normalizeTimezone(settings?.timezone);
447
+ const [timezonePrompt, setTimezonePrompt] = useState<{
448
+ savedTimezone: string;
449
+ browserTimezone: string;
450
+ } | null>(null);
378
451
  const { data: rawOverlayPeople } = useOverlayPeople();
379
452
  const overlayPeople = Array.isArray(rawOverlayPeople) ? rawOverlayPeople : [];
380
453
  const overlayEmails = useMemo(
@@ -393,32 +466,83 @@ export default function CalendarView() {
393
466
  day: t("calendarView.day"),
394
467
  };
395
468
 
396
- // Compute date range for query based on view
397
- const { from, to } = useMemo(() => {
398
- switch (viewMode) {
399
- case "month": {
400
- const ms = startOfMonth(selectedDate);
401
- const me = endOfMonth(selectedDate);
402
- return {
403
- from: startOfWeek(ms, { weekStartsOn }).toISOString(),
404
- to: endOfWeek(me, { weekStartsOn }).toISOString(),
405
- };
406
- }
407
- case "week": {
408
- return {
409
- from: startOfWeek(selectedDate, { weekStartsOn }).toISOString(),
410
- to: endOfWeek(selectedDate, { weekStartsOn }).toISOString(),
411
- };
469
+ useEffect(() => {
470
+ if (!settings?.timezone || typeof window === "undefined") return;
471
+
472
+ const checkBrowserTimezone = () => {
473
+ const browserTimezone = getBrowserTimezone();
474
+ if (browserTimezone === settings.timezone) {
475
+ setTimezonePrompt(null);
476
+ return;
412
477
  }
413
- case "day": {
414
- const dayStart = new Date(selectedDate);
415
- dayStart.setHours(0, 0, 0, 0);
416
- const dayEnd = new Date(selectedDate);
417
- dayEnd.setHours(23, 59, 59, 999);
418
- return { from: dayStart.toISOString(), to: dayEnd.toISOString() };
478
+
479
+ const dismissalKey = timezoneDismissalKey(
480
+ settings.timezone,
481
+ browserTimezone,
482
+ );
483
+ try {
484
+ if (window.localStorage.getItem(dismissalKey) === "1") {
485
+ setTimezonePrompt(null);
486
+ return;
487
+ }
488
+ } catch (error) {
489
+ console.warn(
490
+ "Calendar timezone dismissal could not be read from local storage.",
491
+ error,
492
+ );
419
493
  }
494
+ setTimezonePrompt({
495
+ savedTimezone: settings.timezone,
496
+ browserTimezone,
497
+ });
498
+ };
499
+
500
+ checkBrowserTimezone();
501
+ window.addEventListener("focus", checkBrowserTimezone);
502
+ document.addEventListener("visibilitychange", checkBrowserTimezone);
503
+ return () => {
504
+ window.removeEventListener("focus", checkBrowserTimezone);
505
+ document.removeEventListener("visibilitychange", checkBrowserTimezone);
506
+ };
507
+ }, [settings?.timezone]);
508
+
509
+ const keepSavedTimezone = useCallback(() => {
510
+ if (!timezonePrompt) return;
511
+ try {
512
+ window.localStorage.setItem(
513
+ timezoneDismissalKey(
514
+ timezonePrompt.savedTimezone,
515
+ timezonePrompt.browserTimezone,
516
+ ),
517
+ "1",
518
+ );
519
+ } catch (error) {
520
+ console.warn(
521
+ "Calendar timezone dismissal could not be saved to local storage.",
522
+ error,
523
+ );
420
524
  }
421
- }, [viewMode, selectedDate, weekStartsOn]);
525
+ setTimezonePrompt(null);
526
+ }, [timezonePrompt]);
527
+
528
+ const switchToBrowserTimezone = useCallback(() => {
529
+ if (!timezonePrompt || !settings) return;
530
+ updateSettings.mutate(
531
+ { ...settings, timezone: timezonePrompt.browserTimezone },
532
+ {
533
+ onSuccess: () => setTimezonePrompt(null),
534
+ onError: () => toast.error(t("settings.saveFailed")),
535
+ },
536
+ );
537
+ }, [settings, t, timezonePrompt, updateSettings]);
538
+
539
+ // The saved Calendar Settings timezone owns the range boundary. The browser
540
+ // timezone is only a temporary fallback while settings are loading.
541
+ const { from, to } = useMemo(
542
+ () =>
543
+ getViewDateRange(viewMode, selectedDate, displayTimezone, weekStartsOn),
544
+ [displayTimezone, selectedDate, viewMode, weekStartsOn],
545
+ );
422
546
 
423
547
  const {
424
548
  data: rawEventsData,
@@ -471,31 +595,25 @@ export default function CalendarView() {
471
595
  case "month": {
472
596
  const next = addMonths(selectedDate, 1);
473
597
  const prev = subMonths(selectedDate, 1);
474
- return [next, prev].map((d) => ({
475
- from: startOfWeek(startOfMonth(d), { weekStartsOn }).toISOString(),
476
- to: endOfWeek(endOfMonth(d), { weekStartsOn }).toISOString(),
477
- }));
598
+ return [next, prev].map((date) =>
599
+ getViewDateRange("month", date, displayTimezone, weekStartsOn),
600
+ );
478
601
  }
479
602
  case "week": {
480
603
  // Two weeks forward so rapid `j j` stays instant, plus one back.
481
604
  const next = addWeeks(selectedDate, 1);
482
605
  const next2 = addWeeks(selectedDate, 2);
483
606
  const prev = subWeeks(selectedDate, 1);
484
- return [next, next2, prev].map((d) => ({
485
- from: startOfWeek(d, { weekStartsOn }).toISOString(),
486
- to: endOfWeek(d, { weekStartsOn }).toISOString(),
487
- }));
607
+ return [next, next2, prev].map((date) =>
608
+ getViewDateRange("week", date, displayTimezone, weekStartsOn),
609
+ );
488
610
  }
489
611
  case "day": {
490
612
  const next = addDays(selectedDate, 1);
491
613
  const prev = subDays(selectedDate, 1);
492
- return [next, prev].map((d) => {
493
- const start = new Date(d);
494
- start.setHours(0, 0, 0, 0);
495
- const end = new Date(d);
496
- end.setHours(23, 59, 59, 999);
497
- return { from: start.toISOString(), to: end.toISOString() };
498
- });
614
+ return [next, prev].map((date) =>
615
+ getViewDateRange("day", date, displayTimezone, weekStartsOn),
616
+ );
499
617
  }
500
618
  }
501
619
  })();
@@ -503,11 +621,12 @@ export default function CalendarView() {
503
621
  void prefetchEvents(queryClient, range.from, range.to, overlayEmails);
504
622
  }
505
623
  }, [
624
+ displayTimezone,
506
625
  isLoading,
507
- viewMode,
508
- selectedDate,
509
626
  overlayEmails,
510
627
  queryClient,
628
+ selectedDate,
629
+ viewMode,
511
630
  weekStartsOn,
512
631
  ]);
513
632
 
@@ -581,31 +700,31 @@ export default function CalendarView() {
581
700
 
582
701
  // Filter events for day view — use overlap check so multi-day continuation
583
702
  // events (started on a prior day) still appear on the selected day.
584
- const dayEvents = useMemo(
585
- () =>
586
- viewMode === "day"
587
- ? events.filter((e) => {
588
- const evStart = parseISO(e.start);
589
- const evEnd = parseISO(e.end);
590
- const dayStart = startOfDay(selectedDate);
591
- const dayEnd = addDays(dayStart, 1);
592
- return evStart < dayEnd && evEnd > dayStart;
593
- })
594
- : events,
595
- [events, viewMode, selectedDate],
596
- );
703
+ const dayEvents = useMemo(() => {
704
+ if (viewMode !== "day") return events;
705
+ return events.filter((event) =>
706
+ eventOverlapsCalendarDay(event, selectedDate, displayTimezone),
707
+ );
708
+ }, [displayTimezone, events, selectedDate, viewMode]);
597
709
  const locationSuggestions = useMemo(
598
710
  () => getLocationSuggestions(events),
599
711
  [events],
600
712
  );
601
713
  const openNotificationEvent = useCallback(
602
714
  (event: CalendarEvent) => {
603
- setSelectedDate(parseISO(event.start));
715
+ const eventDate = getEventDateKey(event, displayTimezone);
716
+ if (eventDate) setSelectedDate(dateKeyToDate(eventDate));
604
717
  setViewMode("day");
605
718
  setSidebarEvent(event);
606
719
  setFocusedEvent(event);
607
720
  },
608
- [setFocusedEvent, setSelectedDate, setSidebarEvent, setViewMode],
721
+ [
722
+ displayTimezone,
723
+ setFocusedEvent,
724
+ setSelectedDate,
725
+ setSidebarEvent,
726
+ setViewMode,
727
+ ],
609
728
  );
610
729
  useMeetingStartNotifications(events, openNotificationEvent);
611
730
 
@@ -616,10 +735,17 @@ export default function CalendarView() {
616
735
  }
617
736
  if (openedDraftIdRef.current === eventDraft.id) return;
618
737
  openedDraftIdRef.current = eventDraft.id;
738
+ const preserveView = preserveDraftViewRef.current;
739
+ preserveDraftViewRef.current = false;
619
740
 
620
- const { start } = draftRange(eventDraft, selectedDate);
621
- setSelectedDate(start);
622
- if (isMobile || viewMode === "month") {
741
+ const draftDate = getEventDateKey(
742
+ draftToCalendarEvent(eventDraft, selectedDate),
743
+ displayTimezone,
744
+ );
745
+ if (!preserveView && draftDate) {
746
+ setSelectedDate(dateKeyToDate(draftDate));
747
+ }
748
+ if (!preserveView && (isMobile || viewMode === "month")) {
623
749
  setViewMode("day");
624
750
  }
625
751
  setCreateDefaultStart(undefined);
@@ -630,6 +756,7 @@ export default function CalendarView() {
630
756
  setQuickEditEventId(calendarDraftEventId(eventDraft.id));
631
757
  }, [
632
758
  eventDraft,
759
+ displayTimezone,
633
760
  isMobile,
634
761
  selectedDate,
635
762
  setEventDetailSidebar,
@@ -670,7 +797,11 @@ export default function CalendarView() {
670
797
  const eventType = draft.eventType ?? "default";
671
798
  const title =
672
799
  editableTitle || (eventType === "outOfOffice" ? "Out of office" : "");
673
- if (!title && !isSlotDraftId(draftId)) {
800
+ if (
801
+ !title &&
802
+ eventType !== "workingLocation" &&
803
+ !isSlotDraftId(draftId)
804
+ ) {
674
805
  committingDraftIdsRef.current.delete(draftId);
675
806
  toast.error(t("calendarView.addTitleBeforeCreate"));
676
807
  return;
@@ -683,27 +814,31 @@ export default function CalendarView() {
683
814
  return;
684
815
  }
685
816
 
686
- const location = draft.location ?? draft.workingLocationLabel ?? "";
817
+ const { workingLocationType, workingLocationLabel } =
818
+ resolveDraftWorkingLocation(draft);
819
+ const location =
820
+ eventType === "workingLocation"
821
+ ? workingLocationLabel
822
+ : (draft.location ?? "");
687
823
  const timezone = resolveEventTimezone(
688
- draft.startTimeZone ?? draft.endTimeZone,
824
+ draft.startTimeZone ?? draft.endTimeZone ?? displayTimezone,
689
825
  );
690
826
  const semanticFullDay =
691
827
  eventType === "outOfOffice" &&
692
828
  draft.fullDay === true &&
693
829
  /^\d{4}-\d{2}-\d{2}$/.test(draft.start ?? "") &&
694
830
  /^\d{4}-\d{2}-\d{2}$/.test(draft.end ?? "");
831
+ const dateOnlyAllDay =
832
+ draft.allDay === true &&
833
+ /^\d{4}-\d{2}-\d{2}$/.test(draft.start ?? "") &&
834
+ /^\d{4}-\d{2}-\d{2}$/.test(draft.end ?? "");
695
835
  const statusPatch =
696
836
  eventType === "default"
697
837
  ? {}
698
838
  : {
699
839
  eventType,
700
- workingLocationType:
701
- draft.workingLocationType ?? "customLocation",
702
- workingLocationLabel:
703
- (draft.workingLocationType ?? "customLocation") ===
704
- "customLocation"
705
- ? location
706
- : draft.workingLocationLabel,
840
+ workingLocationType,
841
+ workingLocationLabel,
707
842
  autoDeclineMode:
708
843
  eventType === "outOfOffice"
709
844
  ? draft.outOfOfficeProperties?.autoDeclineMode
@@ -720,8 +855,11 @@ export default function CalendarView() {
720
855
  titleIsGenerated: !editableTitle,
721
856
  description:
722
857
  eventType === "outOfOffice" ? "" : (draft.description ?? ""),
723
- start: semanticFullDay ? draft.start! : start.toISOString(),
724
- end: semanticFullDay ? draft.end! : end.toISOString(),
858
+ start:
859
+ semanticFullDay || dateOnlyAllDay
860
+ ? draft.start!
861
+ : start.toISOString(),
862
+ end: semanticFullDay || dateOnlyAllDay ? draft.end! : end.toISOString(),
725
863
  startTimeZone: draft.allDay ? undefined : timezone,
726
864
  endTimeZone: draft.allDay
727
865
  ? undefined
@@ -803,6 +941,7 @@ export default function CalendarView() {
803
941
  defaultAccountEmail,
804
942
  deleteEvent,
805
943
  eventDraft,
944
+ displayTimezone,
806
945
  googleStatus.data?.accounts,
807
946
  selectedDate,
808
947
  setEventDraft,
@@ -882,7 +1021,8 @@ export default function CalendarView() {
882
1021
  }
883
1022
 
884
1023
  function handleToday() {
885
- setSelectedDate(new Date());
1024
+ const today = getDateKeyInTimezone(new Date(), displayTimezone);
1025
+ if (today) setSelectedDate(dateKeyToDate(today));
886
1026
  }
887
1027
 
888
1028
  const handleDateSelect = useCallback(
@@ -1047,54 +1187,24 @@ export default function CalendarView() {
1047
1187
  const event = events.find((e) => e.id === eventId);
1048
1188
  if (!event) return;
1049
1189
 
1190
+ const moved = moveEventToCalendarDate(event, newDate, displayTimezone);
1191
+ if (!moved) return;
1192
+
1050
1193
  if (calendarDraftIdFromEventId(eventId)) {
1051
- const originalStart = parseISO(event.start);
1052
- const originalEnd = parseISO(event.end);
1053
- const newStart = new Date(originalStart);
1054
- const newEnd = new Date(originalEnd);
1055
- newStart.setFullYear(
1056
- newDate.getFullYear(),
1057
- newDate.getMonth(),
1058
- newDate.getDate(),
1059
- );
1060
- newEnd.setFullYear(
1061
- newDate.getFullYear(),
1062
- newDate.getMonth(),
1063
- newDate.getDate(),
1064
- );
1065
- updateDraftEvent(eventId, {
1066
- start: newStart.toISOString(),
1067
- end: newEnd.toISOString(),
1068
- });
1194
+ updateDraftEvent(eventId, moved);
1069
1195
  return;
1070
1196
  }
1071
1197
 
1072
1198
  const oldStartISO = event.start;
1073
1199
  const oldEndISO = event.end;
1074
- const originalStart = parseISO(event.start);
1075
- const originalEnd = parseISO(event.end);
1076
- const newStart = new Date(originalStart);
1077
- const newEnd = new Date(originalEnd);
1078
-
1079
- newStart.setFullYear(
1080
- newDate.getFullYear(),
1081
- newDate.getMonth(),
1082
- newDate.getDate(),
1083
- );
1084
- newEnd.setFullYear(
1085
- newDate.getFullYear(),
1086
- newDate.getMonth(),
1087
- newDate.getDate(),
1088
- );
1200
+ const newStart = new Date(moved.start);
1201
+ const newEnd = new Date(moved.end);
1089
1202
 
1090
1203
  // Guard against a zero/negative duration reaching the server (e.g. a
1091
1204
  // DST transition collapsing a short event's start/end onto each other).
1092
1205
  if (newEnd.getTime() <= newStart.getTime()) return;
1093
1206
 
1094
- const updates = {
1095
- start: newStart.toISOString(),
1096
- end: newEnd.toISOString(),
1097
- };
1207
+ const updates = moved;
1098
1208
  const isRecurring = isRecurringCalendarEvent(event);
1099
1209
  const guestNotification = await promptGuestNotification({
1100
1210
  event,
@@ -1155,7 +1265,7 @@ export default function CalendarView() {
1155
1265
 
1156
1266
  if (calendarDraftIdFromEventId(eventId)) {
1157
1267
  const timezone = resolveEventTimezone(
1158
- event.startTimeZone ?? event.endTimeZone,
1268
+ event.startTimeZone ?? event.endTimeZone ?? displayTimezone,
1159
1269
  );
1160
1270
  updateDraftEvent(eventId, {
1161
1271
  start: newStart.toISOString(),
@@ -1167,8 +1277,8 @@ export default function CalendarView() {
1167
1277
  return;
1168
1278
  }
1169
1279
 
1170
- const oldStart = parseISO(event.start).getTime();
1171
- const oldEnd = parseISO(event.end).getTime();
1280
+ const oldStart = new Date(event.start).getTime();
1281
+ const oldEnd = new Date(event.end).getTime();
1172
1282
  if (oldStart === newStart.getTime() && oldEnd === newEnd.getTime()) {
1173
1283
  return;
1174
1284
  }
@@ -1226,8 +1336,9 @@ export default function CalendarView() {
1226
1336
  );
1227
1337
  },
1228
1338
  [
1339
+ displayTimezone,
1229
1340
  events,
1230
- settings,
1341
+ displayTimezone,
1231
1342
  updateDraftEvent,
1232
1343
  promptGuestNotification,
1233
1344
  updateEvent,
@@ -1261,7 +1372,7 @@ export default function CalendarView() {
1261
1372
  setCreateDefaultStart(startTime);
1262
1373
  setCreateDialogOpen(false);
1263
1374
 
1264
- const dateStr = format(clickedDate, "yyyy-MM-dd");
1375
+ const dateStr = dateToCalendarDateKey(clickedDate);
1265
1376
  // A drag-to-create gesture already computed the exact dragged range;
1266
1377
  // a plain click falls back to the user's configured default duration.
1267
1378
  const end = options?.explicitDuration
@@ -1302,6 +1413,53 @@ export default function CalendarView() {
1302
1413
  ],
1303
1414
  );
1304
1415
 
1416
+ const handleCreateWorkingLocation = useCallback(
1417
+ (date: Date) => {
1418
+ const existing = findOwnedWorkingLocationForDay(
1419
+ events,
1420
+ date,
1421
+ displayTimezone,
1422
+ defaultAccountEmail,
1423
+ );
1424
+
1425
+ preserveDraftViewRef.current = true;
1426
+ setCreateDefaultStart(undefined);
1427
+ setCreateDefaultEnd(undefined);
1428
+
1429
+ if (existing) {
1430
+ const existingDraftId = calendarDraftIdFromEventId(existing.id);
1431
+ if (!existingDraftId && eventDraft) {
1432
+ discardDraftEvent(calendarDraftEventId(eventDraft.id));
1433
+ }
1434
+ setQuickEditEventId(existing.id);
1435
+ return;
1436
+ }
1437
+
1438
+ if (eventDraft) {
1439
+ discardDraftEvent(calendarDraftEventId(eventDraft.id));
1440
+ }
1441
+
1442
+ const draftId = `slot-working-location-${Date.now()}`;
1443
+ const draft = buildWorkingLocationDraft({
1444
+ id: draftId,
1445
+ date,
1446
+ accountEmail: defaultAccountEmail,
1447
+ });
1448
+
1449
+ persistCalendarDraft(draft);
1450
+ setEventDraft(draft);
1451
+ setQuickEditEventId(calendarDraftEventId(draftId));
1452
+ },
1453
+ [
1454
+ defaultAccountEmail,
1455
+ discardDraftEvent,
1456
+ displayTimezone,
1457
+ eventDraft,
1458
+ events,
1459
+ setEventDraft,
1460
+ ],
1461
+ );
1462
+
1305
1463
  // Command palette natural-language quick create (e.g. "lunch with Sam
1306
1464
  // tomorrow 12:30") — builds a prefilled draft and jumps to it, reusing the
1307
1465
  // same draft/quick-edit flow as clicking a time slot.
@@ -1806,7 +1964,9 @@ export default function CalendarView() {
1806
1964
  <MonthView
1807
1965
  events={events}
1808
1966
  selectedDate={selectedDate}
1967
+ timezone={displayTimezone}
1809
1968
  onDateSelect={handleDateSelect}
1969
+ onCreateWorkingLocation={handleCreateWorkingLocation}
1810
1970
  onDeleteEvent={handleDeleteEvent}
1811
1971
  onEventDrop={handleEventDrop}
1812
1972
  draftEventIds={draftEventIds}
@@ -1821,10 +1981,12 @@ export default function CalendarView() {
1821
1981
  <WeekView
1822
1982
  events={events}
1823
1983
  selectedDate={selectedDate}
1984
+ timezone={displayTimezone}
1824
1985
  onDateSelect={handleDateSelect}
1825
1986
  onDeleteEvent={handleDeleteEvent}
1826
1987
  onEventTimeChange={handleEventTimeChange}
1827
1988
  onClickTimeSlot={handleClickTimeSlot}
1989
+ onCreateWorkingLocation={handleCreateWorkingLocation}
1828
1990
  quickEditEventId={quickEditEventId}
1829
1991
  onQuickEditSave={handleQuickEditSave}
1830
1992
  onQuickEditCancel={handleQuickEditCancel}
@@ -1840,9 +2002,11 @@ export default function CalendarView() {
1840
2002
  <DayView
1841
2003
  events={dayEvents}
1842
2004
  date={selectedDate}
2005
+ timezone={displayTimezone}
1843
2006
  onDeleteEvent={handleDeleteEvent}
1844
2007
  onEventTimeChange={handleEventTimeChange}
1845
2008
  onClickTimeSlot={handleClickTimeSlot}
2009
+ onCreateWorkingLocation={handleCreateWorkingLocation}
1846
2010
  quickEditEventId={quickEditEventId}
1847
2011
  onQuickEditSave={handleQuickEditSave}
1848
2012
  onQuickEditCancel={handleQuickEditCancel}
@@ -1867,6 +2031,19 @@ export default function CalendarView() {
1867
2031
  )}
1868
2032
 
1869
2033
  {/* Dialogs */}
2034
+ {timezonePrompt && (
2035
+ <TimezoneSwitchDialog
2036
+ open
2037
+ savedTimezone={timezonePrompt.savedTimezone}
2038
+ browserTimezone={timezonePrompt.browserTimezone}
2039
+ isSwitching={updateSettings.isPending}
2040
+ onKeep={keepSavedTimezone}
2041
+ onSwitch={switchToBrowserTimezone}
2042
+ onOpenChange={(open) => {
2043
+ if (!open) keepSavedTimezone();
2044
+ }}
2045
+ />
2046
+ )}
1870
2047
  <CommandPalette
1871
2048
  open={commandPaletteOpen}
1872
2049
  onClose={() => setCommandPaletteOpen(false)}
@@ -1874,7 +2051,8 @@ export default function CalendarView() {
1874
2051
  onGoToDate={handleGoToDate}
1875
2052
  onEventClick={(event) => {
1876
2053
  setCommandPaletteOpen(false);
1877
- handleGoToDate(parseISO(event.start));
2054
+ const eventDate = getEventDateKey(event, displayTimezone);
2055
+ if (eventDate) handleGoToDate(dateKeyToDate(eventDate));
1878
2056
  }}
1879
2057
  onCreateEvent={() => {
1880
2058
  setCommandPaletteOpen(false);