@agent-native/core 0.114.8 → 0.114.10

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 (128) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +31 -3
  5. package/corpus/core/src/agent/engine/anthropic-engine.ts +20 -3
  6. package/corpus/core/src/agent/engine/builder-engine.ts +56 -9
  7. package/corpus/core/src/agent/engine/first-event-timeout.ts +64 -0
  8. package/corpus/core/src/client/AssistantChat.tsx +7 -3
  9. package/corpus/core/src/client/resources/ResourcesPanel.tsx +1 -1
  10. package/corpus/core/src/integrations/computer-supervision-store.ts +4 -4
  11. package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
  12. package/corpus/core/src/integrations/remote-commands-store.ts +5 -5
  13. package/corpus/core/src/integrations/remote-devices-store.ts +3 -3
  14. package/corpus/core/src/integrations/remote-push-store.ts +3 -3
  15. package/corpus/core/src/observability/traces.ts +38 -1
  16. package/corpus/core/src/server/action-routes.ts +23 -1
  17. package/corpus/core/src/server/http-response-telemetry.ts +6 -1
  18. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  19. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  20. package/corpus/templates/analytics/actions/account-deep-dive.ts +41 -6
  21. package/corpus/templates/analytics/changelog/2026-07-20-account-deep-dives-finish-with-partial-data-when-hubspot-lo.md +6 -0
  22. package/corpus/templates/analytics/changelog/2026-07-20-dashboard-email-reports-now-include-an-image-when-a-single-p.md +6 -0
  23. package/corpus/templates/analytics/server/lib/dashboard-report.ts +11 -3
  24. package/corpus/templates/calendar/actions/create-event.ts +5 -0
  25. package/corpus/templates/calendar/app/components/ThemeToggle.tsx +4 -1
  26. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +0 -1
  27. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +309 -421
  28. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +349 -0
  29. package/corpus/templates/calendar/app/i18n-data.ts +110 -0
  30. package/corpus/templates/calendar/app/lib/event-form-utils.ts +6 -0
  31. package/corpus/templates/calendar/app/pages/CalendarView.tsx +3 -0
  32. package/corpus/templates/calendar/app/root.tsx +4 -2
  33. package/corpus/templates/calendar/changelog/2026-07-20-events-can-be-updated-inline-with-date-time-timezone-and-rep.md +6 -0
  34. package/corpus/templates/calendar/changelog/2026-07-20-public-booking-pages-no-longer-hydrate-with-a-theme-mismatch.md +6 -0
  35. package/corpus/templates/calendar/shared/api.ts +1 -0
  36. package/corpus/templates/clips/AGENTS.md +7 -0
  37. package/corpus/templates/clips/actions/create-recording.ts +51 -10
  38. package/corpus/templates/clips/actions/get-recording-player-data.ts +34 -5
  39. package/corpus/templates/clips/actions/lib/transcript-preview.ts +48 -0
  40. package/corpus/templates/clips/actions/list-recordings.ts +15 -4
  41. package/corpus/templates/clips/actions/regenerate-summary.ts +32 -0
  42. package/corpus/templates/clips/actions/search-recordings.ts +28 -6
  43. package/corpus/templates/clips/actions/view-screen.ts +54 -47
  44. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -5
  45. package/corpus/templates/clips/app/components/library/recording-card.tsx +29 -32
  46. package/corpus/templates/clips/app/components/meetings/quick-ask-sidebar.tsx +13 -0
  47. package/corpus/templates/clips/changelog/2026-07-20-agent-summaries-use-bounded-transcript-context.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-20-clip-summaries-now-finish-without-sending-a-long-transcript-.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-20-clips-agents-only-discover-recordings-you-own-or-have-alread.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-20-hosted-recordings-no-longer-start-with-an-impossible-buffered-upload.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-20-meeting-recordings-now-offer-a-cleaner-transcript-view-with-.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-20-shared-clips-can-be-shared-with-limited-access-and-cards-sho.md +6 -0
  53. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +12 -8
  54. package/corpus/templates/clips/desktop/src/overlays/pill-logo.tsx +2 -15
  55. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +107 -340
  56. package/corpus/templates/clips/desktop/src/styles.css +27 -145
  57. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +6 -7
  58. package/corpus/templates/clips/server/lib/agent-recording-access.ts +73 -0
  59. package/corpus/templates/clips/server/plugins/agent-chat.ts +5 -2
  60. package/corpus/templates/plan/app/lib/plan-local-bridge.ts +18 -0
  61. package/corpus/templates/plan/app/pages/PlansPage.tsx +10 -11
  62. package/corpus/templates/plan/changelog/2026-07-20-bridged-local-plans-now-load-comments-from-the-local-workspace.md +6 -0
  63. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  64. package/dist/agent/engine/ai-sdk-engine.js +27 -4
  65. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  66. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  67. package/dist/agent/engine/anthropic-engine.js +19 -4
  68. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  69. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  70. package/dist/agent/engine/builder-engine.js +40 -7
  71. package/dist/agent/engine/builder-engine.js.map +1 -1
  72. package/dist/agent/engine/first-event-timeout.d.ts +25 -0
  73. package/dist/agent/engine/first-event-timeout.d.ts.map +1 -0
  74. package/dist/agent/engine/first-event-timeout.js +49 -0
  75. package/dist/agent/engine/first-event-timeout.js.map +1 -0
  76. package/dist/client/AssistantChat.d.ts.map +1 -1
  77. package/dist/client/AssistantChat.js +7 -3
  78. package/dist/client/AssistantChat.js.map +1 -1
  79. package/dist/client/resources/ResourcesPanel.js +1 -1
  80. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  81. package/dist/collab/routes.d.ts +1 -1
  82. package/dist/collab/struct-routes.d.ts +1 -1
  83. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  84. package/dist/integrations/computer-supervision-store.js +4 -4
  85. package/dist/integrations/computer-supervision-store.js.map +1 -1
  86. package/dist/integrations/pending-tasks-store.js +1 -1
  87. package/dist/integrations/pending-tasks-store.js.map +1 -1
  88. package/dist/integrations/remote-commands-store.js +5 -5
  89. package/dist/integrations/remote-commands-store.js.map +1 -1
  90. package/dist/integrations/remote-devices-store.js +3 -3
  91. package/dist/integrations/remote-devices-store.js.map +1 -1
  92. package/dist/integrations/remote-push-store.js +3 -3
  93. package/dist/integrations/remote-push-store.js.map +1 -1
  94. package/dist/notifications/routes.d.ts +1 -1
  95. package/dist/observability/routes.d.ts +3 -3
  96. package/dist/observability/traces.d.ts.map +1 -1
  97. package/dist/observability/traces.js +23 -1
  98. package/dist/observability/traces.js.map +1 -1
  99. package/dist/progress/routes.d.ts +1 -1
  100. package/dist/resources/handlers.d.ts +1 -1
  101. package/dist/server/action-routes.d.ts.map +1 -1
  102. package/dist/server/action-routes.js +23 -1
  103. package/dist/server/action-routes.js.map +1 -1
  104. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  105. package/dist/server/http-response-telemetry.d.ts +2 -0
  106. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  107. package/dist/server/http-response-telemetry.js +5 -1
  108. package/dist/server/http-response-telemetry.js.map +1 -1
  109. package/dist/server/transcribe-voice.d.ts +1 -1
  110. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  111. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  112. package/package.json +2 -2
  113. package/src/agent/engine/ai-sdk-engine.ts +31 -3
  114. package/src/agent/engine/anthropic-engine.ts +20 -3
  115. package/src/agent/engine/builder-engine.ts +56 -9
  116. package/src/agent/engine/first-event-timeout.ts +64 -0
  117. package/src/client/AssistantChat.tsx +7 -3
  118. package/src/client/resources/ResourcesPanel.tsx +1 -1
  119. package/src/integrations/computer-supervision-store.ts +4 -4
  120. package/src/integrations/pending-tasks-store.ts +1 -1
  121. package/src/integrations/remote-commands-store.ts +5 -5
  122. package/src/integrations/remote-devices-store.ts +3 -3
  123. package/src/integrations/remote-push-store.ts +3 -3
  124. package/src/observability/traces.ts +38 -1
  125. package/src/server/action-routes.ts +23 -1
  126. package/src/server/http-response-telemetry.ts +6 -1
  127. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  128. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
@@ -1,4 +1,3 @@
1
- import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
2
1
  import { ExtensionSlot } from "@agent-native/core/client/extensions";
3
2
  import { useT } from "@agent-native/core/client/i18n";
4
3
  import type {
@@ -10,19 +9,15 @@ import {
10
9
  IconX,
11
10
  IconClock,
12
11
  IconMapPin,
13
- IconUser,
14
12
  IconVideo,
15
13
  IconRefresh,
16
14
  IconBell,
17
- IconChevronRight,
18
15
  IconLayoutSidebarRight,
19
16
  IconFileText,
20
17
  IconExternalLink,
21
18
  IconAlignLeft,
22
19
  IconPlus,
23
20
  IconBrandZoom,
24
- IconMessage,
25
- IconPalette,
26
21
  IconPaperclip,
27
22
  IconCalendarTime,
28
23
  } from "@tabler/icons-react";
@@ -42,14 +37,18 @@ import {
42
37
  } from "@/components/calendar/EventDescription";
43
38
  import {
44
39
  AttachmentControls,
45
- EventColorSwatches,
46
40
  ReminderControls,
47
41
  } from "@/components/calendar/EventOptionControls";
48
42
  import { FindTimeTakeover } from "@/components/calendar/FindTimePanel";
49
43
  import { useGuestNotificationPrompt } from "@/components/calendar/GuestNotificationDialog";
44
+ import {
45
+ DatePickerPopover,
46
+ RepeatPicker,
47
+ TimePickerPopover,
48
+ TimezonePickerPopover,
49
+ } from "@/components/calendar/InlineEventPickers";
50
50
  import { WorkingLocationEditor } from "@/components/calendar/WorkingLocationEditor";
51
51
  import { useCalendarContext } from "@/components/layout/AppLayout";
52
- import { TimezoneCombobox } from "@/components/TimezoneCombobox";
53
52
  import { Button } from "@/components/ui/button";
54
53
  import {
55
54
  Popover,
@@ -78,13 +77,11 @@ import { useViewPreferences } from "@/hooks/use-view-preferences";
78
77
  import { useConnectZoom, useZoomStatus } from "@/hooks/use-zoom-auth";
79
78
  import { defaultColorForAccount } from "@/lib/calendar-view-preferences";
80
79
  import { shouldShowEventAccountSelector } from "@/lib/event-account-selection";
81
- import { getGoogleEventColorHex } from "@/lib/event-colors";
82
80
  import {
83
81
  attachmentsToDrafts,
84
82
  buildRecurrenceRules,
85
83
  buildReminderPayload,
86
84
  dateTimeInTimezoneToIso,
87
- formatRecurrenceText,
88
85
  formatReminderText,
89
86
  getEventEndValidationMessage,
90
87
  getLocalTimezone,
@@ -313,6 +310,31 @@ type EventUpdatePatch = Partial<CalendarEvent> & {
313
310
  workingLocationLabel?: string;
314
311
  };
315
312
 
313
+ interface TimeEditValues {
314
+ date: string;
315
+ endDate: string;
316
+ startTime: string;
317
+ endTime: string;
318
+ timezone: string;
319
+ }
320
+
321
+ function addMinutesToTimeValue(
322
+ date: string,
323
+ time: string,
324
+ minutes: number,
325
+ ): { date: string; time: string } {
326
+ const [hour, minute] = time.split(":").map(Number);
327
+ const total = (hour || 0) * 60 + (minute || 0) + minutes;
328
+ const dayOffset = Math.floor(total / (24 * 60));
329
+ const minuteOfDay = ((total % (24 * 60)) + 24 * 60) % (24 * 60);
330
+ const nextDate = new Date(`${date}T00:00:00`);
331
+ nextDate.setDate(nextDate.getDate() + dayOffset);
332
+ return {
333
+ date: format(nextDate, "yyyy-MM-dd"),
334
+ time: `${String(Math.floor(minuteOfDay / 60)).padStart(2, "0")}:${String(minuteOfDay % 60).padStart(2, "0")}`,
335
+ };
336
+ }
337
+
316
338
  function mergeAttendeesForPrompt(
317
339
  existing: CalendarEvent["attendees"] | undefined,
318
340
  additions: CalendarEvent["attendees"] | undefined,
@@ -569,8 +591,6 @@ export function EventDetailPopover({
569
591
  const [editMeetingLink, setEditMeetingLink] = useState("");
570
592
  const [editTimeScope, setEditTimeScope] =
571
593
  useState<UpdateEventScope>("single");
572
- const [editRecurrencePreset, setEditRecurrencePreset] =
573
- useState<RecurrencePreset>(() => getRecurrencePreset(event.recurrence));
574
594
  const [pendingVideoProvider, setPendingVideoProvider] = useState<
575
595
  "meet" | "zoom" | null
576
596
  >(null);
@@ -591,9 +611,7 @@ export function EventDetailPopover({
591
611
  const isRecurringEvent = !!(
592
612
  event.recurringEventId || recurrenceRules?.length
593
613
  );
594
- const recurrenceLoading =
595
- isRecurringEvent && !recurrenceRules?.length && masterEvent.isLoading;
596
- const canEditRecurrence = !isDraft && !isOverlay && !!recurrenceRules?.length;
614
+ const canEditRecurrence = !isOverlay && !isWorkingLocation;
597
615
  const { promptGuestNotification, guestNotificationDialog } =
598
616
  useGuestNotificationPrompt();
599
617
  const zoomStatus = useZoomStatus();
@@ -650,10 +668,6 @@ export function EventDetailPopover({
650
668
  editableLocationValue,
651
669
  ]);
652
670
 
653
- useEffect(() => {
654
- setEditRecurrencePreset(getRecurrencePreset(recurrenceRules));
655
- }, [recurrenceRules]);
656
-
657
671
  // When defaultOpen changes to true (new event created), open the popover
658
672
  useEffect(() => {
659
673
  if (defaultOpen) {
@@ -794,39 +808,6 @@ export function EventDetailPopover({
794
808
  return saved;
795
809
  }, [editAttachments, saveField]);
796
810
 
797
- const handleColorChange = useCallback(
798
- (nextColorId: string | undefined) => {
799
- if (!nextColorId) return;
800
- saveField({
801
- colorId: nextColorId,
802
- color: getGoogleEventColorHex(nextColorId),
803
- });
804
- },
805
- [saveField],
806
- );
807
-
808
- const handleDraftDescription = useCallback(() => {
809
- sendToAgentChat({
810
- message: t("eventForm.ai.descriptionMessage", {
811
- title: event.title,
812
- }),
813
- context: t("eventForm.ai.existingDescriptionContext", {
814
- id: event.id,
815
- title: event.title,
816
- start: event.start,
817
- end: event.end,
818
- timezone: event.startTimeZone || getLocalTimezone(),
819
- location: event.location || t("eventForm.ai.none"),
820
- attendees:
821
- (event.attendees ?? [])
822
- .map((attendee) => attendee.email)
823
- .join(", ") || t("eventForm.ai.none"),
824
- description: event.description || t("eventForm.ai.empty"),
825
- }),
826
- submit: true,
827
- });
828
- }, [event, t]);
829
-
830
811
  const handleAddGoogleMeet = useCallback(() => {
831
812
  if (!event.id || updateEvent.isPending) return;
832
813
  if (isDraft) {
@@ -1030,64 +1011,166 @@ export function EventDetailPopover({
1030
1011
  [event, isDraft, onDraftUpdate, t, updateEvent],
1031
1012
  );
1032
1013
 
1033
- const handleSaveTime = useCallback(() => {
1034
- const normalizedEndDate = normalizeAllDayEditEndDate(
1014
+ const saveTimeValues = useCallback(
1015
+ (values: TimeEditValues) => {
1016
+ const normalizedEndDate = normalizeAllDayEditEndDate(
1017
+ isSingleDayWorkingLocation,
1018
+ values.date,
1019
+ values.endDate,
1020
+ );
1021
+ const allDayEnd = new Date(`${normalizedEndDate}T00:00:00`);
1022
+ allDayEnd.setDate(allDayEnd.getDate() + 1);
1023
+ const newStart = event.allDay
1024
+ ? new Date(`${values.date}T00:00:00`).toISOString()
1025
+ : dateTimeInTimezoneToIso(
1026
+ values.date,
1027
+ values.startTime,
1028
+ values.timezone,
1029
+ );
1030
+ const newEnd = event.allDay
1031
+ ? allDayEnd.toISOString()
1032
+ : dateTimeInTimezoneToIso(
1033
+ values.endDate,
1034
+ values.endTime,
1035
+ values.timezone,
1036
+ );
1037
+ if (new Date(newEnd).getTime() <= new Date(newStart).getTime()) {
1038
+ toast.error(
1039
+ getEventEndValidationMessage({
1040
+ allDay: event.allDay ?? false,
1041
+ startDate: values.date,
1042
+ endDate: values.endDate,
1043
+ startTime: values.startTime,
1044
+ endTime: values.endTime,
1045
+ }),
1046
+ );
1047
+ return false;
1048
+ }
1049
+ let saved = false;
1050
+ if (newStart !== event.start || newEnd !== event.end) {
1051
+ saved = saveField({
1052
+ start: newStart,
1053
+ end: newEnd,
1054
+ allDay: event.allDay,
1055
+ startTimeZone: event.allDay ? undefined : values.timezone,
1056
+ endTimeZone: event.allDay ? undefined : values.timezone,
1057
+ scope: resolveTimeEditScope(
1058
+ isRecurringEvent,
1059
+ isSingleDayWorkingLocation,
1060
+ editTimeScope,
1061
+ ),
1062
+ });
1063
+ }
1064
+ setEditTimeScope("single");
1065
+ return saved;
1066
+ },
1067
+ [
1068
+ event.start,
1069
+ event.end,
1070
+ event.allDay,
1035
1071
  isSingleDayWorkingLocation,
1072
+ isRecurringEvent,
1073
+ editTimeScope,
1074
+ saveField,
1075
+ ],
1076
+ );
1077
+
1078
+ const handleInlineTimeChange = useCallback(
1079
+ (field: "startTime" | "endTime", nextValue: string) => {
1080
+ let nextDate = editDate;
1081
+ let nextEndDate = editEndDate;
1082
+ let nextStartTime = editStartTime;
1083
+ let nextEndTime = editEndTime;
1084
+
1085
+ if (field === "startTime") {
1086
+ nextStartTime = nextValue;
1087
+ if (nextEndDate === nextDate && nextEndTime <= nextStartTime) {
1088
+ const duration = Math.max(
1089
+ 15,
1090
+ differenceInMinutes(parseISO(event.end), parseISO(event.start)),
1091
+ );
1092
+ const nextEnd = addMinutesToTimeValue(
1093
+ nextDate,
1094
+ nextStartTime,
1095
+ duration,
1096
+ );
1097
+ nextEndDate = nextEnd.date;
1098
+ nextEndTime = nextEnd.time;
1099
+ }
1100
+ } else {
1101
+ nextEndTime = nextValue;
1102
+ if (nextEndDate === nextDate && nextEndTime <= nextStartTime) {
1103
+ const nextEnd = addMinutesToTimeValue(nextDate, nextEndTime, 24 * 60);
1104
+ nextEndDate = nextEnd.date;
1105
+ }
1106
+ }
1107
+
1108
+ setEditDate(nextDate);
1109
+ setEditEndDate(nextEndDate);
1110
+ setEditStartTime(nextStartTime);
1111
+ setEditEndTime(nextEndTime);
1112
+ saveTimeValues({
1113
+ date: nextDate,
1114
+ endDate: nextEndDate,
1115
+ startTime: nextStartTime,
1116
+ endTime: nextEndTime,
1117
+ timezone: editTimezone,
1118
+ });
1119
+ },
1120
+ [
1036
1121
  editDate,
1037
1122
  editEndDate,
1038
- );
1039
- const allDayEnd = new Date(`${normalizedEndDate}T00:00:00`);
1040
- allDayEnd.setDate(allDayEnd.getDate() + 1);
1041
- const newStart = event.allDay
1042
- ? new Date(`${editDate}T00:00:00`).toISOString()
1043
- : dateTimeInTimezoneToIso(editDate, editStartTime, editTimezone);
1044
- const newEnd = event.allDay
1045
- ? allDayEnd.toISOString()
1046
- : dateTimeInTimezoneToIso(editEndDate, editEndTime, editTimezone);
1047
- if (new Date(newEnd).getTime() <= new Date(newStart).getTime()) {
1048
- toast.error(
1049
- getEventEndValidationMessage({
1050
- allDay: event.allDay ?? false,
1051
- startDate: editDate,
1052
- endDate: editEndDate,
1053
- startTime: editStartTime,
1054
- endTime: editEndTime,
1055
- }),
1056
- );
1057
- return false;
1058
- }
1059
- let saved = false;
1060
- if (newStart !== event.start || newEnd !== event.end) {
1061
- saved = saveField({
1062
- start: newStart,
1063
- end: newEnd,
1064
- allDay: event.allDay,
1065
- startTimeZone: event.allDay ? undefined : editTimezone,
1066
- endTimeZone: event.allDay ? undefined : editTimezone,
1067
- scope: resolveTimeEditScope(
1068
- isRecurringEvent,
1069
- isSingleDayWorkingLocation,
1070
- editTimeScope,
1071
- ),
1123
+ editStartTime,
1124
+ editEndTime,
1125
+ editTimezone,
1126
+ event.end,
1127
+ event.start,
1128
+ saveTimeValues,
1129
+ ],
1130
+ );
1131
+
1132
+ const handleInlineDateChange = useCallback(
1133
+ (field: "date" | "endDate", nextValue: string) => {
1134
+ const nextDate = field === "date" ? nextValue : editDate;
1135
+ const nextEndDate =
1136
+ field === "endDate"
1137
+ ? nextValue
1138
+ : nextValue > editEndDate
1139
+ ? nextValue
1140
+ : editEndDate;
1141
+ setEditDate(nextDate);
1142
+ setEditEndDate(nextEndDate < nextDate ? nextDate : nextEndDate);
1143
+ saveTimeValues({
1144
+ date: nextDate,
1145
+ endDate: nextEndDate < nextDate ? nextDate : nextEndDate,
1146
+ startTime: editStartTime,
1147
+ endTime: editEndTime,
1148
+ timezone: editTimezone,
1072
1149
  });
1073
- }
1074
- setEditTimeScope("single");
1075
- setEditingField(null);
1076
- return saved;
1077
- }, [
1078
- editDate,
1079
- editEndDate,
1080
- editStartTime,
1081
- editEndTime,
1082
- editTimezone,
1083
- event.start,
1084
- event.end,
1085
- event.allDay,
1086
- isSingleDayWorkingLocation,
1087
- isRecurringEvent,
1088
- editTimeScope,
1089
- saveField,
1090
- ]);
1150
+ },
1151
+ [
1152
+ editDate,
1153
+ editEndDate,
1154
+ editEndTime,
1155
+ editStartTime,
1156
+ editTimezone,
1157
+ saveTimeValues,
1158
+ ],
1159
+ );
1160
+
1161
+ const handleInlineTimezoneChange = useCallback(
1162
+ (nextTimezone: string) => {
1163
+ setEditTimezone(nextTimezone);
1164
+ saveTimeValues({
1165
+ date: editDate,
1166
+ endDate: editEndDate,
1167
+ startTime: editStartTime,
1168
+ endTime: editEndTime,
1169
+ timezone: nextTimezone,
1170
+ });
1171
+ },
1172
+ [editDate, editEndDate, editEndTime, editStartTime, saveTimeValues],
1173
+ );
1091
1174
 
1092
1175
  const schedulingAttendees = useMemo(
1093
1176
  () =>
@@ -1132,27 +1215,29 @@ export function EventDetailPopover({
1132
1215
  [editTimeScope, findTimeTimezone, isRecurringEvent, saveField],
1133
1216
  );
1134
1217
 
1135
- const handleSaveRecurrence = useCallback(() => {
1136
- const recurrence = buildRecurrenceRules(
1137
- editRecurrencePreset,
1138
- masterEvent.data?.start || event.start,
1139
- masterEvent.data?.startTimeZone || event.startTimeZone || editTimezone,
1140
- );
1141
- if (!recurrence) {
1142
- toast.error(t("eventForm.customRepeatGoogleCalendar"));
1143
- return;
1144
- }
1145
- saveField({ recurrence, scope: "all" });
1146
- setEditingField(null);
1147
- }, [
1148
- editRecurrencePreset,
1149
- editTimezone,
1150
- event.start,
1151
- event.startTimeZone,
1152
- masterEvent.data?.start,
1153
- masterEvent.data?.startTimeZone,
1154
- saveField,
1155
- ]);
1218
+ const handleSaveRecurrence = useCallback(
1219
+ (preset: RecurrencePreset) => {
1220
+ const recurrence = buildRecurrenceRules(
1221
+ preset,
1222
+ masterEvent.data?.start || event.start,
1223
+ masterEvent.data?.startTimeZone || event.startTimeZone || editTimezone,
1224
+ );
1225
+ if (!recurrence) {
1226
+ toast.error(t("eventForm.customRepeatGoogleCalendar"));
1227
+ return;
1228
+ }
1229
+ saveField({ recurrence, scope: "all" });
1230
+ },
1231
+ [
1232
+ editTimezone,
1233
+ event.start,
1234
+ event.startTimeZone,
1235
+ masterEvent.data?.start,
1236
+ masterEvent.data?.startTimeZone,
1237
+ saveField,
1238
+ t,
1239
+ ],
1240
+ );
1156
1241
 
1157
1242
  const handleAddAttendee = useCallback(
1158
1243
  (attendee: AttendeeRecipient) => {
@@ -1275,10 +1360,6 @@ export function EventDetailPopover({
1275
1360
  const locationIsUrl = event.location ? isUrl(event.location) : false;
1276
1361
  const locationIsMeetingLink =
1277
1362
  meetingLink && event.location?.includes(meetingLink.url);
1278
- const recurrenceText = recurrenceLoading
1279
- ? t("eventForm.loadingRepeat")
1280
- : formatRecurrenceText(recurrenceRules) ||
1281
- (isRecurringEvent ? t("eventForm.repeats") : null);
1282
1363
  const handleOpenChange = useCallback(
1283
1364
  (newOpen: boolean) => {
1284
1365
  const isPopoverSuppressed =
@@ -1302,8 +1383,6 @@ export function EventDetailPopover({
1302
1383
  savedPendingChange = handleSaveDescription() || savedPendingChange;
1303
1384
  } else if (editingField === "location") {
1304
1385
  savedPendingChange = handleSaveLocation() || savedPendingChange;
1305
- } else if (editingField === "time") {
1306
- savedPendingChange = handleSaveTime() || savedPendingChange;
1307
1386
  } else if (editingField === "meetingLink") {
1308
1387
  savedPendingChange = handleSaveMeetingLink() || savedPendingChange;
1309
1388
  } else if (editingField === "reminders") {
@@ -1336,7 +1415,6 @@ export function EventDetailPopover({
1336
1415
  editingField,
1337
1416
  handleSaveDescription,
1338
1417
  handleSaveLocation,
1339
- handleSaveTime,
1340
1418
  handleSaveMeetingLink,
1341
1419
  handleSaveReminders,
1342
1420
  handleSaveAttachments,
@@ -1396,7 +1474,8 @@ export function EventDetailPopover({
1396
1474
  const target = e.target as HTMLElement;
1397
1475
  if (
1398
1476
  target.closest("[data-apollo-popover]") ||
1399
- target.closest("[data-attendee-autocomplete]")
1477
+ target.closest("[data-attendee-autocomplete]") ||
1478
+ target.closest("[data-time-picker-popover]")
1400
1479
  ) {
1401
1480
  e.preventDefault();
1402
1481
  return;
@@ -1414,9 +1493,7 @@ export function EventDetailPopover({
1414
1493
  ? t("eventForm.workingLocation")
1415
1494
  : isOutOfOffice
1416
1495
  ? t("eventForm.outOfOffice")
1417
- : isDraft
1418
- ? t("eventForm.draftEvent")
1419
- : t("eventForm.event")}
1496
+ : t("eventForm.event")}
1420
1497
  </span>
1421
1498
  </div>
1422
1499
  <div className="flex items-center gap-0.5">
@@ -1524,185 +1601,112 @@ export function EventDetailPopover({
1524
1601
  />
1525
1602
  )}
1526
1603
 
1527
- {/* Time editable */}
1528
- {editingField === "time" ? (
1529
- <div className="flex items-start gap-3 py-1.5">
1530
- <IconClock className="mt-2 h-4 w-4 shrink-0 text-muted-foreground" />
1531
- <div className="flex-1 space-y-2">
1532
- <div
1533
- className={`grid gap-2 ${
1534
- isSingleDayWorkingLocation
1535
- ? "grid-cols-1"
1536
- : "grid-cols-2"
1537
- }`}
1538
- >
1539
- <input
1540
- type="date"
1604
+ {/* Time, date, timezone, and repeat stay editable in place. */}
1605
+ <div className="flex items-start gap-3 rounded-md py-1.5">
1606
+ <IconClock className="mt-1 h-4 w-4 shrink-0 text-muted-foreground" />
1607
+ <div className="min-w-0 flex-1">
1608
+ {event.allDay ? (
1609
+ <div className="flex flex-wrap items-center gap-1 text-sm">
1610
+ <span className="text-muted-foreground">
1611
+ {t("eventForm.allDay")}
1612
+ </span>
1613
+ <DatePickerPopover
1541
1614
  value={editDate}
1542
- onChange={(e) => {
1543
- const next = e.target.value;
1544
- setEditDate(next);
1545
- setEditEndDate((current) =>
1546
- // i18n-ignore -- this expression selects a date; it is not visible copy.
1547
- isSingleDayWorkingLocation
1548
- ? next
1549
- : current < next
1550
- ? next
1551
- : current,
1552
- );
1553
- }}
1554
- className="w-full rounded-md border border-border bg-transparent px-2 py-1 text-sm text-foreground"
1555
- aria-label={t("eventForm.startDate")}
1615
+ label={t("eventForm.startDate")}
1616
+ onChange={(value) =>
1617
+ handleInlineDateChange("date", value)
1618
+ }
1556
1619
  />
1557
- {!isSingleDayWorkingLocation && (
1558
- <input
1559
- type="date"
1560
- min={editDate}
1561
- value={editEndDate}
1562
- onChange={(e) =>
1563
- setEditEndDate(e.target.value || editDate)
1564
- }
1565
- className="w-full rounded-md border border-border bg-transparent px-2 py-1 text-sm text-foreground"
1566
- aria-label={t("eventForm.endDate")}
1567
- />
1620
+ {editEndDate !== editDate && (
1621
+ <>
1622
+ <span className="text-muted-foreground/50">→</span>
1623
+ <DatePickerPopover
1624
+ value={editEndDate}
1625
+ label={t("eventForm.endDate")}
1626
+ onChange={(value) =>
1627
+ handleInlineDateChange("endDate", value)
1628
+ }
1629
+ />
1630
+ </>
1568
1631
  )}
1569
1632
  </div>
1570
- {!event.allDay && (
1571
- <div className="flex items-center gap-2">
1572
- <input
1573
- type="time"
1633
+ ) : (
1634
+ <>
1635
+ <div className="flex flex-wrap items-baseline gap-1">
1636
+ <TimePickerPopover
1574
1637
  value={editStartTime}
1575
- onChange={(e) => setEditStartTime(e.target.value)}
1576
- className="flex-1 rounded-md border border-border bg-transparent px-2 py-1 text-sm text-foreground"
1638
+ label={t("eventForm.start")}
1639
+ onChange={(value) =>
1640
+ handleInlineTimeChange("startTime", value)
1641
+ }
1577
1642
  />
1578
- <span className="text-muted-foreground/50 text-xs">
1579
- &rarr;
1580
- </span>
1581
- <input
1582
- type="time"
1643
+ <span className="text-muted-foreground/50">→</span>
1644
+ <TimePickerPopover
1583
1645
  value={editEndTime}
1584
- onChange={(e) => setEditEndTime(e.target.value)}
1585
- className="flex-1 rounded-md border border-border bg-transparent px-2 py-1 text-sm text-foreground"
1646
+ label={t("eventForm.end")}
1647
+ getOptionMeta={(value) => {
1648
+ const [hour, minute] = value.split(":").map(Number);
1649
+ const [startHour, startMinute] = editStartTime
1650
+ .split(":")
1651
+ .map(Number);
1652
+ const duration =
1653
+ hour * 60 +
1654
+ minute -
1655
+ (startHour * 60 + startMinute) +
1656
+ (editEndDate !== editDate ? 24 * 60 : 0);
1657
+ if (duration <= 0) return undefined;
1658
+ if (duration < 60) return `${duration}min`;
1659
+ const hours = Math.floor(duration / 60);
1660
+ const minutes = duration % 60;
1661
+ return minutes
1662
+ ? `${hours}h ${minutes}min`
1663
+ : `${hours}h`;
1664
+ }}
1665
+ onChange={(value) =>
1666
+ handleInlineTimeChange("endTime", value)
1667
+ }
1586
1668
  />
1669
+ <span className="text-xs text-muted-foreground/70">
1670
+ {formatDuration(event.start, event.end)}
1671
+ </span>
1587
1672
  </div>
1588
- )}
1589
- {!event.allDay && (
1590
- <TimezoneCombobox
1591
- id={`event-timezone-${event.id}`}
1673
+ <div className="mt-0.5 flex flex-wrap items-center gap-1 text-sm">
1674
+ <DatePickerPopover
1675
+ value={editDate}
1676
+ label={t("eventForm.startDate")}
1677
+ onChange={(value) =>
1678
+ handleInlineDateChange("date", value)
1679
+ }
1680
+ />
1681
+ {editEndDate !== editDate && (
1682
+ <>
1683
+ <span className="text-muted-foreground/50">→</span>
1684
+ <DatePickerPopover
1685
+ value={editEndDate}
1686
+ label={t("eventForm.endDate")}
1687
+ onChange={(value) =>
1688
+ handleInlineDateChange("endDate", value)
1689
+ }
1690
+ />
1691
+ </>
1692
+ )}
1693
+ </div>
1694
+ <TimezonePickerPopover
1592
1695
  value={editTimezone}
1593
- onChange={setEditTimezone}
1696
+ label={t("eventForm.timezone")}
1697
+ onChange={handleInlineTimezoneChange}
1594
1698
  />
1595
- )}
1596
- {isRecurringEvent &&
1597
- !isDraft &&
1598
- !isSingleDayWorkingLocation && (
1599
- <div className="flex items-center gap-2">
1600
- <span className="text-xs text-muted-foreground">
1601
- {t("eventForm.applyTo")}
1602
- </span>
1603
- <Select
1604
- value={editTimeScope}
1605
- onValueChange={(value) =>
1606
- setEditTimeScope(value as UpdateEventScope)
1607
- }
1608
- >
1609
- <SelectTrigger className="h-7 flex-1 text-xs">
1610
- <SelectValue />
1611
- </SelectTrigger>
1612
- <SelectContent>
1613
- <SelectItem value="single">
1614
- {isWorkingLocation
1615
- ? t("eventForm.thisDayOnly")
1616
- : t("eventForm.thisEvent")}
1617
- </SelectItem>
1618
- <SelectItem value="all">
1619
- {isWorkingLocation
1620
- ? t("eventForm.allDays")
1621
- : t("eventForm.allEvents")}
1622
- </SelectItem>
1623
- </SelectContent>
1624
- </Select>
1625
- </div>
1626
- )}
1627
- <div className="flex justify-end gap-1.5">
1628
- <Button
1629
- variant="ghost"
1630
- size="sm"
1631
- className="h-6 text-xs"
1632
- onClick={() => {
1633
- setEditDate(toDateInputValue(event.start));
1634
- setEditEndDate(
1635
- event.allDay
1636
- ? toAllDayEndDateInputValue(event.end)
1637
- : toDateInputValue(event.end),
1638
- );
1639
- setEditStartTime(toTimeInputValue(event.start));
1640
- setEditEndTime(toTimeInputValue(event.end));
1641
- setEditTimezone(
1642
- event.startTimeZone || getLocalTimezone(),
1643
- );
1644
- setEditTimeScope("single");
1645
- setEditingField(null);
1646
- }}
1647
- >
1648
- {t("eventForm.cancel")}
1649
- </Button>
1650
- <Button
1651
- size="sm"
1652
- className="h-6 text-xs"
1653
- onClick={handleSaveTime}
1654
- >
1655
- {t("eventForm.save")}
1656
- </Button>
1657
- </div>
1658
- </div>
1659
- </div>
1660
- ) : (
1661
- <div
1662
- className={`flex items-start gap-3 py-1.5 rounded-md px-0 -mx-0 ${!isOverlay ? "cursor-pointer hover:bg-muted/50" : ""}`}
1663
- onClick={() => {
1664
- if (isOverlay) return;
1665
- setEditingField("time");
1666
- }}
1667
- >
1668
- <IconClock className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
1669
- <div className="text-sm">
1670
- {event.allDay ? (
1671
- <div>
1672
- <span className="text-foreground">
1673
- {t("eventForm.allDay")}
1674
- </span>
1675
- <span className="text-muted-foreground ml-2 text-xs">
1676
- {formatEventDateRange(
1677
- event.start,
1678
- event.end,
1679
- event.allDay,
1680
- )}
1681
- </span>
1682
- </div>
1683
- ) : (
1684
- <>
1685
- <div className="flex items-baseline gap-1">
1686
- <span className="text-foreground font-medium">
1687
- {formatTimeShort(event.start)}
1688
- </span>
1689
- <span className="text-muted-foreground/50 mx-0.5">
1690
- &rarr;
1691
- </span>
1692
- <span className="text-foreground font-medium">
1693
- {formatTimeShort(event.end)}
1694
- </span>
1695
- <span className="text-muted-foreground/50 text-xs ml-1">
1696
- {formatDuration(event.start, event.end)}
1697
- </span>
1698
- </div>
1699
- <div className="text-muted-foreground text-xs mt-0.5">
1700
- {formatEventDateRange(event.start, event.end)}
1701
- </div>
1702
- </>
1703
- )}
1704
- </div>
1699
+ </>
1700
+ )}
1705
1701
  </div>
1702
+ </div>
1703
+
1704
+ {canEditRecurrence && (
1705
+ <RepeatPicker
1706
+ preset={getRecurrencePreset(recurrenceRules)}
1707
+ referenceDate={event.start}
1708
+ onChange={handleSaveRecurrence}
1709
+ />
1706
1710
  )}
1707
1711
 
1708
1712
  {!event.allDay && !isOverlay && !isWorkingLocation && (
@@ -1741,93 +1745,6 @@ export function EventDetailPopover({
1741
1745
  />
1742
1746
  )}
1743
1747
 
1744
- {/* Recurrence */}
1745
- {editingField === "recurrence" ? (
1746
- <div className="flex items-start gap-3 py-1.5">
1747
- <IconRefresh className="mt-1.5 h-4 w-4 shrink-0 text-muted-foreground" />
1748
- <div className="flex-1 space-y-2">
1749
- <Select
1750
- value={editRecurrencePreset}
1751
- onValueChange={(value) =>
1752
- setEditRecurrencePreset(value as RecurrencePreset)
1753
- }
1754
- >
1755
- <SelectTrigger className="h-8 text-sm">
1756
- <SelectValue />
1757
- </SelectTrigger>
1758
- <SelectContent>
1759
- <SelectItem value="none">
1760
- {t("eventForm.doesNotRepeat")}
1761
- </SelectItem>
1762
- <SelectItem value="daily">
1763
- {t("eventForm.daily")}
1764
- </SelectItem>
1765
- <SelectItem value="weekdays">
1766
- {t("eventForm.everyWeekday")}
1767
- </SelectItem>
1768
- <SelectItem value="weekly">
1769
- {t("eventForm.weekly")}
1770
- </SelectItem>
1771
- <SelectItem value="monthly">
1772
- {t("eventForm.monthly")}
1773
- </SelectItem>
1774
- <SelectItem value="yearly">
1775
- {t("eventForm.yearly")}
1776
- </SelectItem>
1777
- {editRecurrencePreset === "custom" && (
1778
- <SelectItem value="custom" disabled>
1779
- {t("eventForm.customSchedule")}
1780
- </SelectItem>
1781
- )}
1782
- </SelectContent>
1783
- </Select>
1784
- <div className="flex justify-end gap-1.5">
1785
- <Button
1786
- variant="ghost"
1787
- size="sm"
1788
- className="h-6 text-xs"
1789
- onClick={() => {
1790
- setEditRecurrencePreset(
1791
- getRecurrencePreset(recurrenceRules),
1792
- );
1793
- setEditingField(null);
1794
- }}
1795
- >
1796
- {t("eventForm.cancel")}
1797
- </Button>
1798
- <Button
1799
- size="sm"
1800
- className="h-6 text-xs"
1801
- onClick={handleSaveRecurrence}
1802
- disabled={editRecurrencePreset === "custom"}
1803
- >
1804
- {t("eventForm.save")}
1805
- </Button>
1806
- </div>
1807
- </div>
1808
- </div>
1809
- ) : recurrenceText ? (
1810
- <button
1811
- type="button"
1812
- className={`group flex w-full items-center gap-3 rounded-md py-1.5 text-left ${canEditRecurrence ? "cursor-pointer hover:bg-muted/50" : ""}`}
1813
- onClick={() => {
1814
- if (!canEditRecurrence) return;
1815
- setEditRecurrencePreset(
1816
- getRecurrencePreset(recurrenceRules),
1817
- );
1818
- setEditingField("recurrence");
1819
- }}
1820
- >
1821
- <IconRefresh className="h-4 w-4 shrink-0 text-muted-foreground" />
1822
- <span className="text-sm text-muted-foreground">
1823
- {recurrenceText}
1824
- </span>
1825
- {canEditRecurrence && (
1826
- <IconChevronRight className="ml-auto h-3.5 w-3.5 text-muted-foreground/50 opacity-0 transition-opacity group-hover:opacity-100" />
1827
- )}
1828
- </button>
1829
- ) : null}
1830
-
1831
1748
  {isWorkingLocation && (
1832
1749
  <WorkingLocationEditor
1833
1750
  event={event}
@@ -1851,15 +1768,6 @@ export function EventDetailPopover({
1851
1768
  canEditOptional={!isOverlay}
1852
1769
  onToggleOptional={handleToggleAttendeeOptional}
1853
1770
  />
1854
- ) : !isOverlay ? (
1855
- <div className="px-4 py-1">
1856
- <div className="flex items-start gap-3">
1857
- <IconUser className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
1858
- <span className="text-sm text-muted-foreground/60">
1859
- {t("eventForm.noGuests")}
1860
- </span>
1861
- </div>
1862
- </div>
1863
1771
  ) : null}
1864
1772
 
1865
1773
  {/* Add guest input */}
@@ -1872,7 +1780,7 @@ export function EventDetailPopover({
1872
1780
  (attendee) => attendee.email,
1873
1781
  )}
1874
1782
  onAdd={handleAddAttendee}
1875
- placeholder={t("eventForm.addGuests")}
1783
+ placeholder={t("eventForm.addGuest")}
1876
1784
  variant="inline"
1877
1785
  showChips={false}
1878
1786
  showAddButton
@@ -1897,7 +1805,6 @@ export function EventDetailPopover({
1897
1805
  <ExtensionSlot
1898
1806
  id="calendar.event-detail.bottom"
1899
1807
  context={buildEventDetailSlotContext(event)}
1900
- showEmptyAffordance
1901
1808
  />
1902
1809
  </div>
1903
1810
  </>
@@ -2269,18 +2176,6 @@ export function EventDetailPopover({
2269
2176
  <div className="flex items-start gap-3">
2270
2177
  <IconAlignLeft className="mt-1.5 h-4 w-4 shrink-0 text-muted-foreground" />
2271
2178
  <div className="min-w-0 flex-1">
2272
- {!isOverlay && (
2273
- <Button
2274
- type="button"
2275
- variant="ghost"
2276
- size="sm"
2277
- className="mb-1 h-6 gap-1 px-1.5 text-[11px] text-muted-foreground"
2278
- onClick={handleDraftDescription}
2279
- >
2280
- <IconMessage className="h-3 w-3" />
2281
- {t("eventForm.askAi")}
2282
- </Button>
2283
- )}
2284
2179
  {isOverlay ? (
2285
2180
  event.description ? (
2286
2181
  <RenderedDescription
@@ -2465,13 +2360,6 @@ export function EventDetailPopover({
2465
2360
  </Select>
2466
2361
  </div>
2467
2362
  </div>
2468
- <div className="mt-2 flex items-center gap-3">
2469
- <IconPalette className="h-4 w-4 shrink-0 text-muted-foreground" />
2470
- <EventColorSwatches
2471
- value={event.colorId}
2472
- onChange={handleColorChange}
2473
- />
2474
- </div>
2475
2363
  </div>
2476
2364
  </>
2477
2365
  ) : event.status || event.visibility ? (