@agent-native/core 0.91.0 → 0.91.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/blueprints/action/crud.md +1 -1
  2. package/blueprints/channel/discord.md +1 -1
  3. package/blueprints/provider/stripe.md +1 -1
  4. package/blueprints/sandbox/docker.md +1 -1
  5. package/corpus/README.md +1 -1
  6. package/corpus/core/CHANGELOG.md +26 -0
  7. package/corpus/core/blueprints/action/crud.md +1 -1
  8. package/corpus/core/blueprints/channel/discord.md +1 -1
  9. package/corpus/core/blueprints/provider/stripe.md +1 -1
  10. package/corpus/core/blueprints/sandbox/docker.md +1 -1
  11. package/corpus/core/package.json +6 -5
  12. package/corpus/core/src/agent/run-manager.ts +23 -2
  13. package/corpus/core/src/agent/run-store.ts +2 -2
  14. package/corpus/core/src/cli/add.ts +1 -1
  15. package/corpus/core/src/cli/create.ts +8 -3
  16. package/corpus/core/src/cli/design-connect.ts +53 -9
  17. package/corpus/core/src/cli/index.ts +5 -4
  18. package/corpus/core/src/client/extensions/ExtensionSlot.tsx +7 -0
  19. package/corpus/core/src/client/session-replay.ts +97 -3
  20. package/corpus/core/src/jobs/scheduler.ts +30 -1
  21. package/corpus/core/src/localization/default-messages.ts +21 -0
  22. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +21 -0
  23. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +21 -0
  24. package/corpus/core/src/templates/default/app/i18n/en-US.ts +21 -0
  25. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +21 -0
  26. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
  27. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +20 -0
  28. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +21 -0
  29. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +20 -0
  30. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +21 -0
  31. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +20 -0
  32. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
  33. package/corpus/core/src/templates/default/package.json +2 -2
  34. package/corpus/core/src/templates/headless/package.json +3 -3
  35. package/corpus/core/src/templates/workspace-core/package.json +6 -6
  36. package/corpus/core/src/templates/workspace-root/package.json +2 -2
  37. package/corpus/core/src/triggers/dispatcher.ts +30 -2
  38. package/corpus/core/src/vite/client.ts +96 -1
  39. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +21 -2
  40. package/corpus/templates/analytics/app/global.css +26 -10
  41. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  42. package/corpus/templates/analytics/app/i18n-data.ts +25 -0
  43. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +548 -237
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +285 -294
  45. package/corpus/templates/analytics/changelog/2026-07-08-console-details-expand-inline-under-each-row-and-the-event-t.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-clamps-ultra-wide-viewports-while-keeping-the.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-keeps-a-usable-stage-when-dev-tools-is-open-a.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-resumes-after-scrubbing-and-expands-console-r.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-08-session-replays-wait-for-full-load-keep-a-normal-viewport-an.md +6 -0
  50. package/corpus/templates/analytics/package.json +2 -2
  51. package/corpus/templates/assets/package.json +2 -2
  52. package/corpus/templates/brain/package.json +2 -2
  53. package/corpus/templates/calendar/AGENTS.md +10 -0
  54. package/corpus/templates/calendar/actions/create-event.ts +5 -1
  55. package/corpus/templates/calendar/actions/event-action-helpers.ts +41 -0
  56. package/corpus/templates/calendar/actions/get-attendee-timezones.ts +28 -0
  57. package/corpus/templates/calendar/actions/set-attendee-timezone.ts +68 -0
  58. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +150 -11
  59. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +36 -1
  60. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +32 -0
  61. package/corpus/templates/calendar/app/hooks/use-attendee-timezones.ts +24 -0
  62. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  63. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  64. package/corpus/templates/calendar/app/lib/attendee-local-time.ts +122 -0
  65. package/corpus/templates/calendar/changelog/2026-07-08-event-details-now-support-extension-widgets-and-you-can-see-.md +6 -0
  66. package/corpus/templates/calendar/changelog/2026-07-08-when-you-invite-guests-to-a-new-event-you-now-appear-in.md +6 -0
  67. package/corpus/templates/calendar/package.json +2 -2
  68. package/corpus/templates/calendar/server/handlers/events.ts +5 -1
  69. package/corpus/templates/calendar/shared/api.ts +6 -0
  70. package/corpus/templates/chat/package.json +2 -2
  71. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +6 -2
  72. package/corpus/templates/clips/actions/create-meeting.ts +8 -3
  73. package/corpus/templates/clips/actions/list-meetings.ts +31 -3
  74. package/corpus/templates/clips/changelog/2026-07-08-after-finishing-a-desktop-recording-reopening-clips-restor.md +6 -0
  75. package/corpus/templates/clips/changelog/2026-07-08-clips-now-pops-up-to-take-notes-when-you-join-an-adhoc-zoom-.md +6 -0
  76. package/corpus/templates/clips/changelog/2026-07-08-desktop-clip-recordings-are-louder-after-capture-mic-only.md +6 -0
  77. package/corpus/templates/clips/changelog/2026-07-08-meeting-reminders-now-appear-1-minute-before-start-stay-unti.md +6 -0
  78. package/corpus/templates/clips/changelog/2026-07-08-the-desktop-meetings-panel-has-room-for-its-drop-shadow-and-.md +6 -0
  79. package/corpus/templates/clips/chrome-extension/package.json +2 -2
  80. package/corpus/templates/clips/desktop/package.json +2 -2
  81. package/corpus/templates/clips/desktop/src/app.tsx +91 -34
  82. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +15 -5
  83. package/corpus/templates/clips/desktop/src/lib/meeting-notification-timing.ts +83 -0
  84. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +21 -0
  85. package/corpus/templates/clips/desktop/src/lib/recorder.ts +98 -20
  86. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +174 -50
  87. package/corpus/templates/clips/desktop/src/styles.css +128 -53
  88. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +364 -0
  89. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +1 -1
  90. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +5 -0
  91. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +91 -11
  92. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +69 -19
  93. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +36 -1
  94. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +62 -10
  95. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +1 -24
  96. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +36 -1
  97. package/corpus/templates/clips/package.json +2 -2
  98. package/corpus/templates/content/actions/_content-database-personal-view.ts +80 -0
  99. package/corpus/templates/content/actions/get-content-database-personal-view.ts +27 -0
  100. package/corpus/templates/content/actions/update-content-database-personal-view.ts +31 -0
  101. package/corpus/templates/content/actions/update-content-database-view.ts +3 -0
  102. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
  103. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2584 -546
  104. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +220 -18
  105. package/corpus/templates/content/app/hooks/use-content-database.ts +32 -0
  106. package/corpus/templates/content/app/i18n/zh-TW.ts +16 -0
  107. package/corpus/templates/content/app/i18n-data.ts +154 -0
  108. package/corpus/templates/content/changelog/2026-07-08-database-filters-now-support-notion-style-reset-save-for-eve.md +6 -0
  109. package/corpus/templates/content/changelog/2026-07-08-date-filters-can-now-target-a-between-range.md +6 -0
  110. package/corpus/templates/content/package.json +2 -2
  111. package/corpus/templates/content/parity/matrix.md +1 -1
  112. package/corpus/templates/content/parity/matrix.ts +2 -0
  113. package/corpus/templates/content/shared/api.ts +24 -0
  114. package/corpus/templates/design/AGENTS.md +9 -0
  115. package/corpus/templates/design/actions/request-localhost-write-consent.ts +141 -0
  116. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +13 -19
  117. package/corpus/templates/design/package.json +2 -2
  118. package/corpus/templates/design/server/lib/verify-write-grant.ts +4 -2
  119. package/corpus/templates/dispatch/app/i18n/zh-TW.ts +34 -1
  120. package/corpus/templates/dispatch/app/i18n-data.ts +387 -10
  121. package/corpus/templates/dispatch/app/routes/automations.tsx +1 -0
  122. package/corpus/templates/dispatch/changelog/2026-07-08-added-an-automations-page-to-review-pause-and-create.md +6 -0
  123. package/corpus/templates/dispatch/changelog/2026-07-08-destinations-now-shows-outbound-delivery-queue-health.md +6 -0
  124. package/corpus/templates/dispatch/package.json +2 -2
  125. package/corpus/templates/forms/package.json +2 -2
  126. package/corpus/templates/macros/package.json +2 -2
  127. package/corpus/templates/mail/app/hooks/use-emails.ts +56 -44
  128. package/corpus/templates/mail/app/lib/gmail-mutation-queue.ts +325 -0
  129. package/corpus/templates/mail/changelog/2026-07-08-rapid-archive-and-mark-read-now-batch-gmail-updates-so-the-i.md +6 -0
  130. package/corpus/templates/mail/package.json +2 -2
  131. package/corpus/templates/plan/package.json +2 -2
  132. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +46 -29
  133. package/corpus/templates/slides/app/context/DeckContext.tsx +45 -18
  134. package/corpus/templates/slides/app/i18n/ar-SA.ts +4 -0
  135. package/corpus/templates/slides/app/i18n/de-DE.ts +4 -0
  136. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  137. package/corpus/templates/slides/app/i18n/es-ES.ts +4 -0
  138. package/corpus/templates/slides/app/i18n/fr-FR.ts +4 -0
  139. package/corpus/templates/slides/app/i18n/hi-IN.ts +4 -0
  140. package/corpus/templates/slides/app/i18n/ja-JP.ts +4 -0
  141. package/corpus/templates/slides/app/i18n/ko-KR.ts +4 -0
  142. package/corpus/templates/slides/app/i18n/pt-BR.ts +4 -0
  143. package/corpus/templates/slides/app/i18n/zh-CN.ts +4 -0
  144. package/corpus/templates/slides/app/i18n/zh-TW.ts +4 -0
  145. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +115 -0
  146. package/corpus/templates/slides/app/pages/DeckEditor.tsx +22 -13
  147. package/corpus/templates/slides/changelog/2026-07-08-dropping-an-image-onto-a-slide-and-sending-it-to-the-agent-w.md +6 -0
  148. package/corpus/templates/slides/changelog/2026-07-08-undo-works-after-the-agent-edits-a-slide-so-you-can-reverse.md +6 -0
  149. package/corpus/templates/slides/package.json +2 -2
  150. package/dist/agent/run-manager.d.ts.map +1 -1
  151. package/dist/agent/run-manager.js +23 -2
  152. package/dist/agent/run-manager.js.map +1 -1
  153. package/dist/agent/run-store.js +2 -2
  154. package/dist/agent/run-store.js.map +1 -1
  155. package/dist/catalog.json +2 -2
  156. package/dist/cli/add.js +1 -1
  157. package/dist/cli/add.js.map +1 -1
  158. package/dist/cli/create.d.ts.map +1 -1
  159. package/dist/cli/create.js +8 -3
  160. package/dist/cli/create.js.map +1 -1
  161. package/dist/cli/design-connect.d.ts.map +1 -1
  162. package/dist/cli/design-connect.js +40 -10
  163. package/dist/cli/design-connect.js.map +1 -1
  164. package/dist/cli/index.js +5 -4
  165. package/dist/cli/index.js.map +1 -1
  166. package/dist/client/extensions/ExtensionSlot.js +6 -0
  167. package/dist/client/extensions/ExtensionSlot.js.map +1 -1
  168. package/dist/client/session-replay.d.ts +8 -0
  169. package/dist/client/session-replay.d.ts.map +1 -1
  170. package/dist/client/session-replay.js +80 -3
  171. package/dist/client/session-replay.js.map +1 -1
  172. package/dist/collab/routes.d.ts +1 -1
  173. package/dist/jobs/scheduler.js +26 -1
  174. package/dist/jobs/scheduler.js.map +1 -1
  175. package/dist/localization/default-messages.d.ts +19 -0
  176. package/dist/localization/default-messages.d.ts.map +1 -1
  177. package/dist/localization/default-messages.js +19 -0
  178. package/dist/localization/default-messages.js.map +1 -1
  179. package/dist/notifications/routes.d.ts +1 -1
  180. package/dist/observability/routes.d.ts +1 -1
  181. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  182. package/dist/resources/handlers.d.ts +1 -1
  183. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  184. package/dist/templates/default/app/i18n/ar-SA.ts +21 -0
  185. package/dist/templates/default/app/i18n/de-DE.ts +21 -0
  186. package/dist/templates/default/app/i18n/en-US.ts +21 -0
  187. package/dist/templates/default/app/i18n/es-ES.ts +21 -0
  188. package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
  189. package/dist/templates/default/app/i18n/hi-IN.ts +20 -0
  190. package/dist/templates/default/app/i18n/ja-JP.ts +21 -0
  191. package/dist/templates/default/app/i18n/ko-KR.ts +20 -0
  192. package/dist/templates/default/app/i18n/pt-BR.ts +21 -0
  193. package/dist/templates/default/app/i18n/zh-CN.ts +20 -0
  194. package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
  195. package/dist/templates/default/package.json +2 -2
  196. package/dist/templates/headless/package.json +3 -3
  197. package/dist/templates/workspace-core/package.json +6 -6
  198. package/dist/templates/workspace-root/package.json +2 -2
  199. package/dist/triggers/dispatcher.js +27 -2
  200. package/dist/triggers/dispatcher.js.map +1 -1
  201. package/dist/vite/client.d.ts.map +1 -1
  202. package/dist/vite/client.js +70 -1
  203. package/dist/vite/client.js.map +1 -1
  204. package/package.json +7 -6
  205. package/src/templates/default/app/i18n/ar-SA.ts +21 -0
  206. package/src/templates/default/app/i18n/de-DE.ts +21 -0
  207. package/src/templates/default/app/i18n/en-US.ts +21 -0
  208. package/src/templates/default/app/i18n/es-ES.ts +21 -0
  209. package/src/templates/default/app/i18n/fr-FR.ts +21 -0
  210. package/src/templates/default/app/i18n/hi-IN.ts +20 -0
  211. package/src/templates/default/app/i18n/ja-JP.ts +21 -0
  212. package/src/templates/default/app/i18n/ko-KR.ts +20 -0
  213. package/src/templates/default/app/i18n/pt-BR.ts +21 -0
  214. package/src/templates/default/app/i18n/zh-CN.ts +20 -0
  215. package/src/templates/default/app/i18n/zh-TW.ts +19 -0
  216. package/src/templates/default/package.json +2 -2
  217. package/src/templates/headless/package.json +3 -3
  218. package/src/templates/workspace-core/package.json +6 -6
  219. package/src/templates/workspace-root/package.json +2 -2
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-08
4
+ ---
5
+
6
+ Console details expand inline under each row, and the event timeline highlights, auto-scrolls, and supports search
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-08
4
+ ---
5
+
6
+ Session replay clamps ultra-wide viewports while keeping the recorded page height instead of forcing a default size
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-08
4
+ ---
5
+
6
+ Session replay keeps a usable stage when Dev Tools is open and shows the event timeline on medium screens
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-08
4
+ ---
5
+
6
+ Session replay resumes after scrubbing and expands console rows without jumping the playhead
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-08
4
+ ---
5
+
6
+ Session replays wait for full load, keep a normal viewport, and scrub the timeline reliably
@@ -73,7 +73,6 @@
73
73
  "@types/react": "catalog:",
74
74
  "@types/react-dom": "catalog:",
75
75
  "@types/three": "^0.176.0",
76
- "@typescript/native-preview": "catalog:",
77
76
  "@vitejs/plugin-react-swc": "^4.0.0",
78
77
  "chart.js": "^4.5.1",
79
78
  "chartjs-node-canvas": "^5.0.0",
@@ -101,7 +100,8 @@
101
100
  "tailwindcss": "catalog:",
102
101
  "three": "^0.176.0",
103
102
  "tsx": "^4.20.3",
104
- "typescript": "^6.0.3",
103
+ "typescript": "catalog:",
104
+ "typescript-7": "catalog:",
105
105
  "vaul": "^1.1.2",
106
106
  "vite": "catalog:",
107
107
  "vitest": "catalog:"
@@ -55,7 +55,6 @@
55
55
  "@types/node": "^24.2.1",
56
56
  "@types/react": "catalog:",
57
57
  "@types/react-dom": "catalog:",
58
- "@typescript/native-preview": "catalog:",
59
58
  "class-variance-authority": "^0.7.1",
60
59
  "clsx": "^2.1.1",
61
60
  "next-themes": "^0.4.6",
@@ -67,7 +66,8 @@
67
66
  "tailwind-merge": "^3.5.0",
68
67
  "tailwindcss": "catalog:",
69
68
  "tsx": "^4.20.3",
70
- "typescript": "^6.0.3",
69
+ "typescript": "catalog:",
70
+ "typescript-7": "catalog:",
71
71
  "vite": "catalog:",
72
72
  "vitest": "catalog:"
73
73
  }
@@ -48,7 +48,6 @@
48
48
  "@types/node": "^25.8.0",
49
49
  "@types/react": "catalog:",
50
50
  "@types/react-dom": "catalog:",
51
- "@typescript/native-preview": "catalog:",
52
51
  "@vitejs/plugin-react-swc": "^4.3.1",
53
52
  "class-variance-authority": "^0.7.1",
54
53
  "clsx": "^2.1.1",
@@ -58,7 +57,8 @@
58
57
  "sonner": "^2.0.7",
59
58
  "tailwind-merge": "^3.6.0",
60
59
  "tailwindcss": "catalog:",
61
- "typescript": "^6.0.3",
60
+ "typescript": "catalog:",
61
+ "typescript-7": "catalog:",
62
62
  "vite": "catalog:",
63
63
  "vitest": "catalog:"
64
64
  }
@@ -43,6 +43,16 @@ Detailed event, availability, booking, storage, and UI rules live in
43
43
  co-hosts to the created Google Calendar event.
44
44
  - Keep scheduling answers concrete: exact dates, time zones, conflicts, and
45
45
  assumptions.
46
+ - Event detail (panel and popover) exposes `calendar.event-detail.bottom` as an
47
+ `ExtensionSlot`. Extensions render as widgets there with `slotContext`
48
+ (eventId, title, start/end, timezones, location, attendees, accountEmail).
49
+ For inline adornments next to each guest email (e.g. local times), prefer the
50
+ first-party attendee timezone UI / `set-attendee-timezone` settings, or a
51
+ source edit — do not claim the slot can inject per-row UI.
52
+ - Use `get-attendee-timezones` / `set-attendee-timezone` to read or save
53
+ per-guest IANA timezone overrides (`attendee-timezones` user setting). The UI
54
+ shows each guest's local event-start time when a timezone is known (self from
55
+ the event/browser zone; others from `attendee.timeZone` or the override map).
46
56
  - Use `rsvp-event` for invitation responses. Pass `note` when the user wants a
47
57
  visible RSVP comment on a declined or tentative response; pass an empty note to
48
58
  clear an existing RSVP comment.
@@ -19,6 +19,7 @@ import {
19
19
  cliBoolean,
20
20
  eventTypeInput,
21
21
  googleColorIdInput,
22
+ ensureOrganizerInAttendees,
22
23
  normalizeAttendees,
23
24
  reminderMethodInput,
24
25
  reminderMinutesInput,
@@ -140,7 +141,10 @@ export default defineAction({
140
141
  acctEmail = args.accountEmail;
141
142
  }
142
143
 
143
- const attendees = normalizeAttendees(args.attendees);
144
+ const attendees = ensureOrganizerInAttendees(
145
+ normalizeAttendees(args.attendees),
146
+ acctEmail,
147
+ );
144
148
  const reminderFields = buildReminderOverrides({
145
149
  reminders: args.reminders,
146
150
  reminderMinutes: args.reminderMinutes,
@@ -112,6 +112,47 @@ export function normalizeAttendees(
112
112
  }));
113
113
  }
114
114
 
115
+ /**
116
+ * Google Calendar's UI always lists the organizer in Guests when inviting
117
+ * others. The insert API does not — unless we include the organizer/self
118
+ * email in `attendees`. Call this when creating/publishing an event that
119
+ * already has guests so AN matches GCal.
120
+ */
121
+ export function ensureOrganizerInAttendees(
122
+ attendees: NormalizedAttendee[] | undefined,
123
+ organizerEmail: string,
124
+ ): NormalizedAttendee[] | undefined {
125
+ if (!attendees || attendees.length === 0) return attendees;
126
+ const organizer = organizerEmail.trim().toLowerCase();
127
+ if (!organizer.includes("@")) return attendees;
128
+
129
+ const existing = attendees.find(
130
+ (attendee) => attendee.email.trim().toLowerCase() === organizer,
131
+ );
132
+ if (existing) {
133
+ return attendees.map((attendee) =>
134
+ attendee.email.trim().toLowerCase() === organizer
135
+ ? {
136
+ ...attendee,
137
+ organizer: true,
138
+ self: true,
139
+ responseStatus: attendee.responseStatus ?? "accepted",
140
+ }
141
+ : attendee,
142
+ );
143
+ }
144
+
145
+ return [
146
+ {
147
+ email: organizerEmail.trim(),
148
+ organizer: true,
149
+ self: true,
150
+ responseStatus: "accepted",
151
+ },
152
+ ...attendees,
153
+ ];
154
+ }
155
+
115
156
  export function requireActionUserEmail(): string {
116
157
  const email = getRequestUserEmail();
117
158
  if (!email) throw new Error("no authenticated user");
@@ -0,0 +1,28 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { getRequestUserEmail } from "@agent-native/core/server";
3
+ import { getUserSetting } from "@agent-native/core/settings";
4
+ import { z } from "zod";
5
+
6
+ export type AttendeeTimezones = Record<string, string>;
7
+
8
+ export default defineAction({
9
+ description:
10
+ "Get saved per-attendee IANA timezone overrides (email → timezone). Used to show each guest's local time on event details.",
11
+ schema: z.object({}),
12
+ http: { method: "GET" },
13
+ run: async () => {
14
+ const email = getRequestUserEmail();
15
+ if (!email) throw new Error("no authenticated user");
16
+ const data = await getUserSetting(email, "attendee-timezones");
17
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
18
+ return {} as AttendeeTimezones;
19
+ }
20
+ const result: AttendeeTimezones = {};
21
+ for (const [key, value] of Object.entries(data)) {
22
+ if (typeof value === "string" && value.trim()) {
23
+ result[key.trim().toLowerCase()] = value.trim();
24
+ }
25
+ }
26
+ return result;
27
+ },
28
+ });
@@ -0,0 +1,68 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { getRequestUserEmail } from "@agent-native/core/server";
3
+ import { getUserSetting, putUserSetting } from "@agent-native/core/settings";
4
+ import { z } from "zod";
5
+
6
+ import type { AttendeeTimezones } from "./get-attendee-timezones.js";
7
+
8
+ function isValidIanaTimeZone(timeZone: string): boolean {
9
+ try {
10
+ new Intl.DateTimeFormat("en-US", { timeZone }).format();
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ export default defineAction({
18
+ description:
19
+ "Save or clear a per-attendee IANA timezone override so event details can show that guest's local time. Pass timeZone to set; omit or pass empty to clear.",
20
+ schema: z.object({
21
+ email: z
22
+ .string()
23
+ .email()
24
+ .describe("Attendee email whose timezone override to set"),
25
+ timeZone: z
26
+ .string()
27
+ .optional()
28
+ .describe(
29
+ "IANA timezone (e.g. America/New_York). Omit or pass empty string to clear the override.",
30
+ ),
31
+ }),
32
+ http: { method: "PUT" },
33
+ run: async (args) => {
34
+ const ownerEmail = getRequestUserEmail();
35
+ if (!ownerEmail) throw new Error("no authenticated user");
36
+
37
+ const attendeeEmail = args.email.trim().toLowerCase();
38
+ const nextZone = args.timeZone?.trim() || "";
39
+ if (nextZone && !isValidIanaTimeZone(nextZone)) {
40
+ throw new Error(`Invalid IANA timezone: ${nextZone}`);
41
+ }
42
+
43
+ const existing =
44
+ ((await getUserSetting(
45
+ ownerEmail,
46
+ "attendee-timezones",
47
+ )) as AttendeeTimezones | null) ?? {};
48
+ const updated: AttendeeTimezones = {};
49
+ for (const [key, value] of Object.entries(existing)) {
50
+ if (typeof value === "string" && value.trim()) {
51
+ updated[key.trim().toLowerCase()] = value.trim();
52
+ }
53
+ }
54
+
55
+ if (nextZone) {
56
+ updated[attendeeEmail] = nextZone;
57
+ } else {
58
+ delete updated[attendeeEmail];
59
+ }
60
+
61
+ await putUserSetting(
62
+ ownerEmail,
63
+ "attendee-timezones",
64
+ updated as unknown as Record<string, unknown>,
65
+ );
66
+ return updated;
67
+ },
68
+ });
@@ -4,6 +4,7 @@ import { IconDots, IconMessageCircle, IconUser } from "@tabler/icons-react";
4
4
  import { useEffect, useId, useMemo, useState } from "react";
5
5
 
6
6
  import { AttendeeApolloPopover } from "@/components/calendar/ApolloPanel";
7
+ import { TimezoneCombobox } from "@/components/TimezoneCombobox";
7
8
  import { Button } from "@/components/ui/button";
8
9
  import {
9
10
  DropdownMenu,
@@ -21,7 +22,16 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
21
22
  import { Separator } from "@/components/ui/separator";
22
23
  import { Textarea } from "@/components/ui/textarea";
23
24
  import { useAttendeePhotos } from "@/hooks/use-attendee-photos";
25
+ import {
26
+ useAttendeeTimezones,
27
+ useSetAttendeeTimezone,
28
+ } from "@/hooks/use-attendee-timezones";
24
29
  import { useRsvpEvent } from "@/hooks/use-events";
30
+ import {
31
+ getAttendeeLocalTimeLabel,
32
+ resolveAttendeeTimeZone,
33
+ } from "@/lib/attendee-local-time";
34
+ import { getLocalTimezone } from "@/lib/event-form-utils";
25
35
  import {
26
36
  canInlineRsvp,
27
37
  RsvpStatusIcon,
@@ -351,9 +361,14 @@ function AttendeeRow({
351
361
  isRecurring,
352
362
  canEditOptional,
353
363
  onToggleOptional,
364
+ timezoneOverrides,
365
+ onSetTimezone,
354
366
  }: {
355
367
  attendee: Attendee;
356
- event: Pick<CalendarEvent, "id" | "accountEmail">;
368
+ event: Pick<
369
+ CalendarEvent,
370
+ "id" | "accountEmail" | "start" | "startTimeZone" | "allDay"
371
+ >;
357
372
  photoUrl?: string;
358
373
  inlineRsvp?: boolean;
359
374
  currentStatus?: RsvpStatus;
@@ -362,14 +377,42 @@ function AttendeeRow({
362
377
  isRecurring?: boolean;
363
378
  canEditOptional?: boolean;
364
379
  onToggleOptional?: (email: string, optional: boolean) => void;
380
+ timezoneOverrides?: Record<string, string>;
381
+ onSetTimezone?: (email: string, timeZone: string | undefined) => void;
365
382
  }) {
366
383
  const t = useT();
384
+ const [timezonePickerOpen, setTimezonePickerOpen] = useState(false);
385
+ const resolvedZone =
386
+ resolveAttendeeTimeZone({
387
+ attendee,
388
+ accountEmail: event.accountEmail,
389
+ eventStartTimeZone: event.startTimeZone,
390
+ overrides: timezoneOverrides,
391
+ }) ?? getLocalTimezone();
392
+ const [draftTimezone, setDraftTimezone] = useState(resolvedZone);
367
393
  const displayStatus = inlineRsvp ? currentStatus : attendee.responseStatus;
368
394
  const statusLabel =
369
395
  getLocalizedRsvpStatusLabel(t, displayStatus) ?? t("eventForm.awaiting");
370
396
  const comment = (inlineRsvp ? currentNote : attendee.comment)?.trim();
371
397
  const showOptionalMenu =
372
398
  canEditOptional && onToggleOptional && !attendee.organizer;
399
+ const showMenu = showOptionalMenu || !!onSetTimezone;
400
+ const localTimeLabel =
401
+ !event.allDay && event.start
402
+ ? getAttendeeLocalTimeLabel({
403
+ attendee,
404
+ accountEmail: event.accountEmail,
405
+ eventStartTimeZone: event.startTimeZone,
406
+ overrides: timezoneOverrides,
407
+ startIso: event.start,
408
+ })
409
+ : null;
410
+
411
+ useEffect(() => {
412
+ if (timezonePickerOpen) {
413
+ setDraftTimezone(resolvedZone);
414
+ }
415
+ }, [timezonePickerOpen, resolvedZone]);
373
416
 
374
417
  return (
375
418
  <div className="group rounded-xl px-1 py-1 transition-colors hover:bg-muted/40">
@@ -400,15 +443,25 @@ function AttendeeRow({
400
443
  {attendee.displayName && (
401
444
  <div className="truncate text-[11px] text-muted-foreground/60">
402
445
  {attendee.email}
446
+ {localTimeLabel ? (
447
+ <span className="ms-1.5 text-muted-foreground/50">
448
+ · {localTimeLabel}
449
+ </span>
450
+ ) : null}
403
451
  </div>
404
452
  )}
453
+ {!attendee.displayName && localTimeLabel ? (
454
+ <div className="truncate text-[11px] text-muted-foreground/50">
455
+ {localTimeLabel}
456
+ </div>
457
+ ) : null}
405
458
  <div className="mt-0.5 text-[11px] text-muted-foreground/70">
406
459
  {inlineRsvp
407
460
  ? t("eventForm.yourResponse", { status: statusLabel })
408
461
  : statusLabel}
409
462
  </div>
410
463
  </div>
411
- {showOptionalMenu && (
464
+ {showMenu && (
412
465
  <DropdownMenu>
413
466
  <DropdownMenuTrigger asChild>
414
467
  <Button
@@ -428,15 +481,27 @@ function AttendeeRow({
428
481
  align="end"
429
482
  onClick={(event) => event.stopPropagation()}
430
483
  >
431
- <DropdownMenuItem
432
- onSelect={() =>
433
- onToggleOptional(attendee.email, !attendee.optional)
434
- }
435
- >
436
- {attendee.optional
437
- ? t("attendees.markRequired")
438
- : t("attendees.markOptional")}
439
- </DropdownMenuItem>
484
+ {showOptionalMenu && (
485
+ <DropdownMenuItem
486
+ onSelect={() =>
487
+ onToggleOptional(attendee.email, !attendee.optional)
488
+ }
489
+ >
490
+ {attendee.optional
491
+ ? t("attendees.markRequired")
492
+ : t("attendees.markOptional")}
493
+ </DropdownMenuItem>
494
+ )}
495
+ {onSetTimezone && (
496
+ <DropdownMenuItem
497
+ onSelect={(event) => {
498
+ event.preventDefault();
499
+ setTimezonePickerOpen(true);
500
+ }}
501
+ >
502
+ {t("attendees.setTimezone")}
503
+ </DropdownMenuItem>
504
+ )}
440
505
  </DropdownMenuContent>
441
506
  </DropdownMenu>
442
507
  )}
@@ -448,6 +513,67 @@ function AttendeeRow({
448
513
  </div>
449
514
  )}
450
515
  </AttendeeApolloPopover>
516
+ {onSetTimezone && (
517
+ <Popover open={timezonePickerOpen} onOpenChange={setTimezonePickerOpen}>
518
+ <PopoverTrigger asChild>
519
+ <button type="button" className="sr-only" tabIndex={-1} />
520
+ </PopoverTrigger>
521
+ <PopoverContent
522
+ align="end"
523
+ side="left"
524
+ className="w-72 p-3"
525
+ onClick={(e) => e.stopPropagation()}
526
+ onPointerDownCapture={(e) => e.stopPropagation()}
527
+ >
528
+ <div className="space-y-2">
529
+ <p className="text-sm font-medium">
530
+ {t("attendees.setTimezone")}
531
+ </p>
532
+ <p className="text-[11px] text-muted-foreground">
533
+ {attendee.displayName || attendee.email}
534
+ </p>
535
+ <TimezoneCombobox
536
+ id={`attendee-tz-${attendee.email}`}
537
+ value={draftTimezone}
538
+ onChange={setDraftTimezone}
539
+ />
540
+ <div className="flex justify-end gap-1.5 pt-1">
541
+ {timezoneOverrides?.[attendee.email.trim().toLowerCase()] ? (
542
+ <Button
543
+ variant="ghost"
544
+ size="sm"
545
+ className="h-7 text-xs me-auto"
546
+ onClick={() => {
547
+ onSetTimezone(attendee.email, undefined);
548
+ setTimezonePickerOpen(false);
549
+ }}
550
+ >
551
+ {t("attendees.clearTimezone")}
552
+ </Button>
553
+ ) : null}
554
+ <Button
555
+ variant="ghost"
556
+ size="sm"
557
+ className="h-7 text-xs"
558
+ onClick={() => setTimezonePickerOpen(false)}
559
+ >
560
+ {t("eventForm.cancel")}
561
+ </Button>
562
+ <Button
563
+ size="sm"
564
+ className="h-7 text-xs"
565
+ onClick={() => {
566
+ onSetTimezone(attendee.email, draftTimezone);
567
+ setTimezonePickerOpen(false);
568
+ }}
569
+ >
570
+ {t("eventForm.save")}
571
+ </Button>
572
+ </div>
573
+ </div>
574
+ </PopoverContent>
575
+ </Popover>
576
+ )}
451
577
  {inlineRsvp && currentStatus && onResponseChange && (
452
578
  <RsvpControls
453
579
  eventId={event.id}
@@ -486,6 +612,9 @@ export function EventAttendeesSection({
486
612
  | "responseStatus"
487
613
  | "source"
488
614
  | "recurringEventId"
615
+ | "start"
616
+ | "startTimeZone"
617
+ | "allDay"
489
618
  >;
490
619
  canEditOptional?: boolean;
491
620
  onToggleOptional?: (email: string, optional: boolean) => void;
@@ -501,6 +630,8 @@ export function EventAttendeesSection({
501
630
  );
502
631
  const emails = attendees.map((attendee) => attendee.email);
503
632
  const { data: photos } = useAttendeePhotos(emails);
633
+ const { data: timezoneOverrides } = useAttendeeTimezones();
634
+ const setAttendeeTimezone = useSetAttendeeTimezone();
504
635
 
505
636
  const sorted = useMemo(() => sortAttendees(attendees), [attendees]);
506
637
  const canRsvpInline = canInlineRsvp(event);
@@ -522,6 +653,10 @@ export function EventAttendeesSection({
522
653
  setSelfNote(note);
523
654
  };
524
655
 
656
+ const handleSetTimezone = (email: string, timeZone: string | undefined) => {
657
+ setAttendeeTimezone.mutate({ email, timeZone });
658
+ };
659
+
525
660
  const shouldTruncate = attendees.length > ATTENDEE_TRUNCATE_THRESHOLD;
526
661
  const visibleOthers =
527
662
  shouldTruncate && !expanded
@@ -578,6 +713,8 @@ export function EventAttendeesSection({
578
713
  photoUrl={photos?.[attendee.email.toLowerCase()]}
579
714
  canEditOptional={canEditOptional}
580
715
  onToggleOptional={onToggleOptional}
716
+ timezoneOverrides={timezoneOverrides}
717
+ onSetTimezone={handleSetTimezone}
581
718
  />
582
719
  ))}
583
720
 
@@ -614,6 +751,8 @@ export function EventAttendeesSection({
614
751
  isRecurring={!!event.recurringEventId}
615
752
  canEditOptional={canEditOptional}
616
753
  onToggleOptional={onToggleOptional}
754
+ timezoneOverrides={timezoneOverrides}
755
+ onSetTimezone={handleSetTimezone}
617
756
  />
618
757
  </>
619
758
  )}
@@ -1,4 +1,5 @@
1
1
  import { useT } from "@agent-native/core/client";
2
+ import { ExtensionSlot } from "@agent-native/core/client/extensions";
2
3
  import type { CalendarEvent } from "@shared/api";
3
4
  import {
4
5
  IconX,
@@ -12,7 +13,7 @@ import {
12
13
  IconVideo,
13
14
  } from "@tabler/icons-react";
14
15
  import { format, parseISO, differenceInMinutes } from "date-fns";
15
- import { useState, useRef, useEffect, useCallback } from "react";
16
+ import { useState, useRef, useEffect, useCallback, useMemo } from "react";
16
17
  import { toast } from "sonner";
17
18
 
18
19
  import { ResearchMeetingButton } from "@/components/calendar/ApolloPanel";
@@ -34,6 +35,28 @@ import { useUpdateEvent } from "@/hooks/use-events";
34
35
  import { useViewPreferences } from "@/hooks/use-view-preferences";
35
36
  import { cn } from "@/lib/utils";
36
37
 
38
+ function buildEventDetailSlotContext(event: CalendarEvent) {
39
+ return {
40
+ eventId: event.id,
41
+ title: event.title,
42
+ start: event.start,
43
+ end: event.end,
44
+ startTimeZone: event.startTimeZone,
45
+ endTimeZone: event.endTimeZone,
46
+ location: event.location,
47
+ accountEmail: event.accountEmail,
48
+ attendees: (event.attendees ?? []).map((attendee) => ({
49
+ email: attendee.email,
50
+ displayName: attendee.displayName,
51
+ responseStatus: attendee.responseStatus,
52
+ organizer: attendee.organizer,
53
+ optional: attendee.optional,
54
+ timeZone: attendee.timeZone,
55
+ self: attendee.self,
56
+ })),
57
+ };
58
+ }
59
+
37
60
  interface EventDetailPanelProps {
38
61
  event: CalendarEvent | null;
39
62
  onClose: () => void;
@@ -105,6 +128,10 @@ export function EventDetailPanel({
105
128
  const lastSavedDescriptionRef = useRef(event?.description || "");
106
129
  const meetingLink = event ? extractMeetingLink(event) : null;
107
130
  const ownerLabel = event?.ownerName || event?.overlayEmail;
131
+ const eventDetailSlotContext = useMemo(
132
+ () => (event ? buildEventDetailSlotContext(event) : null),
133
+ [event],
134
+ );
108
135
 
109
136
  // Reset editing state when event changes
110
137
  useEffect(() => {
@@ -460,6 +487,14 @@ export function EventDetailPanel({
460
487
  {event.attendees && event.attendees.length > 0 && (
461
488
  <ResearchMeetingButton event={event} />
462
489
  )}
490
+
491
+ {eventDetailSlotContext && (
492
+ <ExtensionSlot
493
+ id="calendar.event-detail.bottom"
494
+ context={eventDetailSlotContext}
495
+ showEmptyAffordance
496
+ />
497
+ )}
463
498
  </div>
464
499
 
465
500
  {/* Actions */}
@@ -1,4 +1,5 @@
1
1
  import { sendToAgentChat, useT } from "@agent-native/core/client";
2
+ import { ExtensionSlot } from "@agent-native/core/client/extensions";
2
3
  import type {
3
4
  CalendarEvent,
4
5
  FindTimeSlot,
@@ -96,6 +97,28 @@ import { shortcutModifierLabel } from "@/lib/utils";
96
97
  const ZOOM_AFTER_CONNECT_EVENT_ID_KEY = "calendar.zoomAfterConnectEventId";
97
98
  const ZOOM_AFTER_CONNECT_MAX_AGE_MS = 10 * 60 * 1000;
98
99
 
100
+ function buildEventDetailSlotContext(event: CalendarEvent) {
101
+ return {
102
+ eventId: event.id,
103
+ title: event.title,
104
+ start: event.start,
105
+ end: event.end,
106
+ startTimeZone: event.startTimeZone,
107
+ endTimeZone: event.endTimeZone,
108
+ location: event.location,
109
+ accountEmail: event.accountEmail,
110
+ attendees: (event.attendees ?? []).map((attendee) => ({
111
+ email: attendee.email,
112
+ displayName: attendee.displayName,
113
+ responseStatus: attendee.responseStatus,
114
+ organizer: attendee.organizer,
115
+ optional: attendee.optional,
116
+ timeZone: attendee.timeZone,
117
+ self: attendee.self,
118
+ })),
119
+ };
120
+ }
121
+
99
122
  function getStoredZoomAfterConnectEventId(): string | null {
100
123
  if (typeof window === "undefined") return null;
101
124
  try {
@@ -1681,6 +1704,15 @@ export function EventDetailPopover({
1681
1704
  </>
1682
1705
  )}
1683
1706
 
1707
+ <div className="mx-4 my-2 border-t border-border/50" />
1708
+ <div className="px-4 py-1">
1709
+ <ExtensionSlot
1710
+ id="calendar.event-detail.bottom"
1711
+ context={buildEventDetailSlotContext(event)}
1712
+ showEmptyAffordance
1713
+ />
1714
+ </div>
1715
+
1684
1716
  {/* Meeting link */}
1685
1717
  {meetingLink ? (
1686
1718
  <>