@agent-native/core 0.90.3 → 0.90.5

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 (256) hide show
  1. package/bin/agent-native.js +25 -3
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +17 -0
  4. package/corpus/core/bin/agent-native.js +25 -3
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/agent/durable-background.ts +18 -18
  7. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +8 -2
  8. package/corpus/core/src/agent/engine/anthropic-engine.ts +11 -1
  9. package/corpus/core/src/agent/engine/index.ts +1 -0
  10. package/corpus/core/src/agent/engine/registry.ts +63 -2
  11. package/corpus/core/src/agent/production-agent.ts +22 -11
  12. package/corpus/core/src/agent/run-manager.ts +17 -6
  13. package/corpus/core/src/agent/run-store.ts +16 -11
  14. package/corpus/core/src/cli/create.ts +35 -0
  15. package/corpus/core/src/cli/design-connect.ts +263 -1
  16. package/corpus/core/src/cli/index.ts +84 -7
  17. package/corpus/core/src/cli/workspacify.ts +34 -0
  18. package/corpus/core/src/client/KeepTabOpenNotice.tsx +20 -15
  19. package/corpus/core/src/client/RunStuckBanner.tsx +14 -13
  20. package/corpus/core/src/client/agent-chat-adapter.ts +28 -17
  21. package/corpus/core/src/client/history/VersionHistoryPanel.tsx +77 -47
  22. package/corpus/core/src/client/history/index.ts +3 -0
  23. package/corpus/core/src/client/history/use-history.ts +28 -0
  24. package/corpus/core/src/client/index.ts +8 -0
  25. package/corpus/core/src/client/review/index.ts +5 -0
  26. package/corpus/core/src/client/review/use-review.ts +40 -2
  27. package/corpus/core/src/client/use-run-stuck-detection.ts +8 -5
  28. package/corpus/core/src/history/actions/create-resource-version.ts +11 -8
  29. package/corpus/core/src/history/index.ts +0 -1
  30. package/corpus/core/src/history/registry.ts +24 -12
  31. package/corpus/core/src/history/store.ts +84 -53
  32. package/corpus/core/src/review/actions/consume-review-feedback.ts +15 -5
  33. package/corpus/core/src/review/actions/delete-review-comment.ts +15 -3
  34. package/corpus/core/src/review/actions/resolve-review-thread.ts +12 -5
  35. package/corpus/core/src/review/index.ts +0 -5
  36. package/corpus/core/src/review/registry.ts +24 -12
  37. package/corpus/core/src/review/store.ts +10 -7
  38. package/corpus/core/src/server/agent-chat-plugin.ts +6 -0
  39. package/corpus/core/src/server/credential-provider.ts +13 -3
  40. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +11 -11
  41. package/corpus/templates/assets/actions/generate-image.ts +64 -28
  42. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +1 -1
  43. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +91 -74
  44. package/corpus/templates/assets/server/lib/generation.ts +29 -4
  45. package/corpus/templates/assets/server/lib/image-processing.ts +131 -0
  46. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +24 -2
  47. package/corpus/templates/calendar/AGENTS.md +3 -0
  48. package/corpus/templates/calendar/actions/create-event.ts +8 -34
  49. package/corpus/templates/calendar/actions/event-action-helpers.ts +52 -0
  50. package/corpus/templates/calendar/actions/manage-event-draft.ts +7 -34
  51. package/corpus/templates/calendar/actions/update-event.ts +10 -23
  52. package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +47 -0
  53. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +23 -0
  54. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +59 -2
  55. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +40 -1
  56. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +34 -1
  57. package/corpus/templates/calendar/app/hooks/use-events.ts +6 -0
  58. package/corpus/templates/calendar/app/i18n/zh-TW.ts +4 -0
  59. package/corpus/templates/calendar/app/i18n-data.ts +40 -0
  60. package/corpus/templates/calendar/changelog/2026-07-07-you-can-mark-event-guests-as-optional-while-inviting-them-or.md +6 -0
  61. package/corpus/templates/calendar/server/handlers/events.ts +1 -0
  62. package/corpus/templates/calendar/server/lib/google-calendar.ts +4 -0
  63. package/corpus/templates/calendar/server/plugins/agent-chat.ts +1 -1
  64. package/corpus/templates/calendar/shared/api.ts +2 -0
  65. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +29 -3
  66. package/corpus/templates/clips/actions/generate-workflow.ts +19 -8
  67. package/corpus/templates/clips/actions/get-clips-ai-prefs.ts +36 -0
  68. package/corpus/templates/clips/actions/lib/clips-ai-prefs.ts +28 -0
  69. package/corpus/templates/clips/actions/regenerate-chapters.ts +21 -7
  70. package/corpus/templates/clips/actions/regenerate-summary.ts +26 -8
  71. package/corpus/templates/clips/actions/regenerate-title.ts +44 -7
  72. package/corpus/templates/clips/actions/update-clips-ai-prefs.ts +50 -0
  73. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +7 -2
  74. package/corpus/templates/clips/app/hooks/use-auto-title.ts +25 -10
  75. package/corpus/templates/clips/app/i18n/ar-SA.ts +28 -6
  76. package/corpus/templates/clips/app/i18n/de-DE.ts +28 -6
  77. package/corpus/templates/clips/app/i18n/en-US.ts +29 -6
  78. package/corpus/templates/clips/app/i18n/es-ES.ts +29 -6
  79. package/corpus/templates/clips/app/i18n/fr-FR.ts +30 -6
  80. package/corpus/templates/clips/app/i18n/hi-IN.ts +26 -6
  81. package/corpus/templates/clips/app/i18n/ja-JP.ts +27 -6
  82. package/corpus/templates/clips/app/i18n/ko-KR.ts +28 -6
  83. package/corpus/templates/clips/app/i18n/pt-BR.ts +28 -6
  84. package/corpus/templates/clips/app/i18n/zh-CN.ts +26 -6
  85. package/corpus/templates/clips/app/i18n/zh-TW.ts +25 -6
  86. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +73 -0
  87. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -0
  88. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -2
  89. package/corpus/templates/clips/changelog/2026-07-07-ai-tools-can-include-the-full-video-not-just-the-transcript-.md +6 -0
  90. package/corpus/templates/clips/changelog/2026-07-07-clips-desktop-now-surfaces-meetings-and-dictate-directly-in-.md +6 -0
  91. package/corpus/templates/clips/changelog/2026-07-07-meeting-notes-now-explain-exactly-how-to-start-granola-style.md +6 -0
  92. package/corpus/templates/clips/changelog/2026-07-07-meeting-start-alerts-now-include-separate-start-notes-and-jo.md +6 -0
  93. package/corpus/templates/clips/changelog/2026-07-07-recordings-start-more-predictably-and-newly-saved-clips-refr.md +6 -0
  94. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  95. package/corpus/templates/clips/desktop/src/app.tsx +435 -105
  96. package/corpus/templates/clips/desktop/src/lib/recorder.ts +27 -21
  97. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +23 -22
  98. package/corpus/templates/clips/desktop/src/styles.css +167 -2
  99. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
  100. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +3 -7
  101. package/corpus/templates/clips/desktop/src-tauri/src/tray_meetings.rs +5 -4
  102. package/corpus/templates/clips/server/routes/_agent-native/clips/user-prefs.put.ts +16 -8
  103. package/corpus/templates/clips/shared/clips-ai-prefs.ts +81 -0
  104. package/corpus/templates/content/AGENTS.md +1 -0
  105. package/corpus/templates/content/actions/_database-source-utils.ts +73 -8
  106. package/corpus/templates/content/actions/_database-utils.ts +2 -1
  107. package/corpus/templates/content/actions/prepare-builder-source-review.ts +20 -1
  108. package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +663 -0
  109. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +33 -7
  110. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +53 -4
  111. package/corpus/templates/content/app/components/editor/database/settings.tsx +24 -0
  112. package/corpus/templates/content/app/hooks/use-content-database.ts +70 -1
  113. package/corpus/templates/content/app/hooks/use-document-properties.ts +63 -5
  114. package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
  115. package/corpus/templates/content/app/i18n-data.ts +31 -0
  116. package/corpus/templates/content/changelog/2026-07-01-builder-source-rows-can-be-staged-in-bulk-with-a-reviewable.md +6 -0
  117. package/corpus/templates/content/changelog/2026-07-07-builder-backed-database-fields-now-edit-like-normal-content-fields.md +6 -0
  118. package/corpus/templates/content/parity/matrix.md +24 -24
  119. package/corpus/templates/content/parity/matrix.ts +2 -0
  120. package/corpus/templates/content/shared/api.ts +59 -4
  121. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +16 -1
  122. package/corpus/templates/design/actions/add-localhost-screens.ts +1 -0
  123. package/corpus/templates/design/actions/connect-localhost.ts +26 -3
  124. package/corpus/templates/design/actions/open-visual-edit.ts +4 -37
  125. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +362 -12
  126. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +89 -14
  127. package/corpus/templates/design/app/components/design/bridge-security.ts +7 -1
  128. package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -1
  129. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -0
  130. package/corpus/templates/design/app/i18n-data.ts +41 -0
  131. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-keeps-bridge-access-when-opening-previousl.md +6 -0
  132. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-now-keeps-the-live-app-connection-authenti.md +6 -0
  133. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-uses-live-app-iframes-for-click-to-edit-pr.md +6 -0
  134. package/dist/agent/durable-background.d.ts +12 -12
  135. package/dist/agent/durable-background.js +18 -18
  136. package/dist/agent/durable-background.js.map +1 -1
  137. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  138. package/dist/agent/engine/ai-sdk-engine.js +8 -2
  139. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  140. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  141. package/dist/agent/engine/anthropic-engine.js +11 -1
  142. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  143. package/dist/agent/engine/index.d.ts +1 -1
  144. package/dist/agent/engine/index.d.ts.map +1 -1
  145. package/dist/agent/engine/index.js +1 -1
  146. package/dist/agent/engine/index.js.map +1 -1
  147. package/dist/agent/engine/registry.d.ts +14 -0
  148. package/dist/agent/engine/registry.d.ts.map +1 -1
  149. package/dist/agent/engine/registry.js +56 -2
  150. package/dist/agent/engine/registry.js.map +1 -1
  151. package/dist/agent/production-agent.d.ts +2 -2
  152. package/dist/agent/production-agent.d.ts.map +1 -1
  153. package/dist/agent/production-agent.js +18 -11
  154. package/dist/agent/production-agent.js.map +1 -1
  155. package/dist/agent/run-manager.d.ts +7 -1
  156. package/dist/agent/run-manager.d.ts.map +1 -1
  157. package/dist/agent/run-manager.js +6 -1
  158. package/dist/agent/run-manager.js.map +1 -1
  159. package/dist/agent/run-store.d.ts +1 -1
  160. package/dist/agent/run-store.d.ts.map +1 -1
  161. package/dist/agent/run-store.js +15 -10
  162. package/dist/agent/run-store.js.map +1 -1
  163. package/dist/cli/create.d.ts.map +1 -1
  164. package/dist/cli/create.js +31 -0
  165. package/dist/cli/create.js.map +1 -1
  166. package/dist/cli/design-connect.d.ts.map +1 -1
  167. package/dist/cli/design-connect.js +197 -1
  168. package/dist/cli/design-connect.js.map +1 -1
  169. package/dist/cli/index.js +76 -9
  170. package/dist/cli/index.js.map +1 -1
  171. package/dist/cli/workspacify.d.ts.map +1 -1
  172. package/dist/cli/workspacify.js +30 -0
  173. package/dist/cli/workspacify.js.map +1 -1
  174. package/dist/client/KeepTabOpenNotice.d.ts +9 -10
  175. package/dist/client/KeepTabOpenNotice.d.ts.map +1 -1
  176. package/dist/client/KeepTabOpenNotice.js +8 -4
  177. package/dist/client/KeepTabOpenNotice.js.map +1 -1
  178. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  179. package/dist/client/RunStuckBanner.js +14 -13
  180. package/dist/client/RunStuckBanner.js.map +1 -1
  181. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  182. package/dist/client/agent-chat-adapter.js +25 -17
  183. package/dist/client/agent-chat-adapter.js.map +1 -1
  184. package/dist/client/history/VersionHistoryPanel.d.ts +1 -1
  185. package/dist/client/history/VersionHistoryPanel.d.ts.map +1 -1
  186. package/dist/client/history/VersionHistoryPanel.js +12 -5
  187. package/dist/client/history/VersionHistoryPanel.js.map +1 -1
  188. package/dist/client/history/index.d.ts +1 -1
  189. package/dist/client/history/index.d.ts.map +1 -1
  190. package/dist/client/history/index.js +1 -1
  191. package/dist/client/history/index.js.map +1 -1
  192. package/dist/client/history/use-history.d.ts +12 -0
  193. package/dist/client/history/use-history.d.ts.map +1 -1
  194. package/dist/client/history/use-history.js +6 -0
  195. package/dist/client/history/use-history.js.map +1 -1
  196. package/dist/client/index.d.ts +2 -2
  197. package/dist/client/index.d.ts.map +1 -1
  198. package/dist/client/index.js +2 -2
  199. package/dist/client/index.js.map +1 -1
  200. package/dist/client/review/index.d.ts +1 -1
  201. package/dist/client/review/index.d.ts.map +1 -1
  202. package/dist/client/review/index.js +1 -1
  203. package/dist/client/review/index.js.map +1 -1
  204. package/dist/client/review/use-review.d.ts +23 -0
  205. package/dist/client/review/use-review.d.ts.map +1 -1
  206. package/dist/client/review/use-review.js +8 -0
  207. package/dist/client/review/use-review.js.map +1 -1
  208. package/dist/client/use-run-stuck-detection.d.ts +1 -1
  209. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  210. package/dist/client/use-run-stuck-detection.js +6 -4
  211. package/dist/client/use-run-stuck-detection.js.map +1 -1
  212. package/dist/collab/struct-routes.d.ts +1 -1
  213. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  214. package/dist/history/actions/create-resource-version.js +7 -4
  215. package/dist/history/actions/create-resource-version.js.map +1 -1
  216. package/dist/history/index.d.ts +1 -1
  217. package/dist/history/index.d.ts.map +1 -1
  218. package/dist/history/index.js +1 -1
  219. package/dist/history/index.js.map +1 -1
  220. package/dist/history/registry.d.ts.map +1 -1
  221. package/dist/history/registry.js +14 -12
  222. package/dist/history/registry.js.map +1 -1
  223. package/dist/history/store.d.ts.map +1 -1
  224. package/dist/history/store.js +74 -49
  225. package/dist/history/store.js.map +1 -1
  226. package/dist/notifications/routes.d.ts +1 -1
  227. package/dist/resources/handlers.d.ts +3 -3
  228. package/dist/review/actions/consume-review-feedback.d.ts +1 -0
  229. package/dist/review/actions/consume-review-feedback.js +8 -2
  230. package/dist/review/actions/consume-review-feedback.js.map +1 -1
  231. package/dist/review/actions/delete-review-comment.d.ts +2 -1
  232. package/dist/review/actions/delete-review-comment.js +11 -3
  233. package/dist/review/actions/delete-review-comment.js.map +1 -1
  234. package/dist/review/actions/resolve-review-thread.d.ts +2 -1
  235. package/dist/review/actions/resolve-review-thread.js +5 -2
  236. package/dist/review/actions/resolve-review-thread.js.map +1 -1
  237. package/dist/review/index.d.ts +1 -1
  238. package/dist/review/index.d.ts.map +1 -1
  239. package/dist/review/index.js +1 -1
  240. package/dist/review/index.js.map +1 -1
  241. package/dist/review/registry.d.ts.map +1 -1
  242. package/dist/review/registry.js +14 -12
  243. package/dist/review/registry.js.map +1 -1
  244. package/dist/review/store.d.ts +3 -3
  245. package/dist/review/store.d.ts.map +1 -1
  246. package/dist/review/store.js +7 -4
  247. package/dist/review/store.js.map +1 -1
  248. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  249. package/dist/server/agent-chat-plugin.js +5 -0
  250. package/dist/server/agent-chat-plugin.js.map +1 -1
  251. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  252. package/dist/server/credential-provider.d.ts +2 -0
  253. package/dist/server/credential-provider.d.ts.map +1 -1
  254. package/dist/server/credential-provider.js +13 -3
  255. package/dist/server/credential-provider.js.map +1 -1
  256. package/package.json +1 -1
@@ -60,6 +60,58 @@ export const workingLocationTypeInput = z
60
60
  .enum(["homeOffice", "officeLocation", "customLocation"])
61
61
  .optional();
62
62
 
63
+ export const attendeeObjectInput = z.object({
64
+ email: z.string(),
65
+ displayName: z.string().optional(),
66
+ optional: cliBoolean.optional(),
67
+ comment: z.string().optional(),
68
+ responseStatus: z
69
+ .enum(["accepted", "declined", "tentative", "needsAction"])
70
+ .optional(),
71
+ organizer: cliBoolean.optional(),
72
+ self: cliBoolean.optional(),
73
+ });
74
+
75
+ export const attendeesInput = z.union([
76
+ z.array(attendeeObjectInput),
77
+ z.string(),
78
+ ]);
79
+
80
+ export type NormalizedAttendee = {
81
+ email: string;
82
+ displayName?: string;
83
+ optional?: boolean;
84
+ comment?: string;
85
+ responseStatus?: "accepted" | "declined" | "tentative" | "needsAction";
86
+ organizer?: boolean;
87
+ self?: boolean;
88
+ };
89
+
90
+ export function normalizeAttendees(
91
+ input: z.infer<typeof attendeesInput> | undefined,
92
+ ): NormalizedAttendee[] | undefined {
93
+ if (input === undefined) return undefined;
94
+ if (typeof input === "string") {
95
+ const emails = input
96
+ .split(/[\s,;]+/)
97
+ .map((s) => s.trim())
98
+ .filter((s) => s.length > 0 && s.includes("@"));
99
+ if (emails.length === 0) return [];
100
+ return emails.map((email) => ({ email }));
101
+ }
102
+ return input
103
+ .filter((a) => a.email && a.email.includes("@"))
104
+ .map((a) => ({
105
+ email: a.email,
106
+ ...(a.displayName ? { displayName: a.displayName } : {}),
107
+ ...(a.optional === true ? { optional: true } : {}),
108
+ ...(a.comment ? { comment: a.comment } : {}),
109
+ ...(a.responseStatus ? { responseStatus: a.responseStatus } : {}),
110
+ ...(a.organizer === true ? { organizer: true } : {}),
111
+ ...(a.self === true ? { self: true } : {}),
112
+ }));
113
+ }
114
+
63
115
  export function requireActionUserEmail(): string {
64
116
  const email = getRequestUserEmail();
65
117
  if (!email) throw new Error("no authenticated user");
@@ -11,11 +11,13 @@ import { z } from "zod";
11
11
  import type { CalendarEventDraft } from "../shared/api.js";
12
12
  import {
13
13
  attachmentsInput,
14
+ attendeesInput,
14
15
  availabilityInput,
15
16
  buildReminderOverrides,
16
17
  cliBoolean,
17
18
  eventTypeInput,
18
19
  googleColorIdInput,
20
+ normalizeAttendees,
19
21
  reminderMethodInput,
20
22
  reminderMinutesInput,
21
23
  remindersInput,
@@ -25,18 +27,6 @@ import {
25
27
 
26
28
  const DRAFT_PREFIX = "calendar-draft-";
27
29
 
28
- const attendeesInput = z
29
- .union([
30
- z.array(
31
- z.object({
32
- email: z.string(),
33
- displayName: z.string().optional(),
34
- }),
35
- ),
36
- z.string(),
37
- ])
38
- .optional();
39
-
40
30
  function sanitizeDraftId(id: string): string | null {
41
31
  return /^[a-zA-Z0-9_-]{1,64}$/.test(id) ? id : null;
42
32
  }
@@ -45,25 +35,6 @@ function draftKey(id: string) {
45
35
  return `${DRAFT_PREFIX}${id}`;
46
36
  }
47
37
 
48
- function normalizeAttendees(
49
- input: z.infer<typeof attendeesInput>,
50
- ): CalendarEventDraft["attendees"] | undefined {
51
- if (input === undefined) return undefined;
52
- if (typeof input === "string") {
53
- const emails = input
54
- .split(/[\s,;]+/)
55
- .map((s) => s.trim())
56
- .filter((s) => s.length > 0 && s.includes("@"));
57
- return emails.map((email) => ({ email }));
58
- }
59
- return input
60
- .filter((a) => a.email && a.email.includes("@"))
61
- .map((a) => ({
62
- email: a.email,
63
- ...(a.displayName ? { displayName: a.displayName } : {}),
64
- }));
65
- }
66
-
67
38
  /**
68
39
  * Deep link that reopens an unsent calendar event draft.
69
40
  *
@@ -166,9 +137,11 @@ export default defineAction({
166
137
  .optional()
167
138
  .describe("Preselect Google Meet for this draft"),
168
139
  addZoom: cliBoolean.optional().describe("Preselect Zoom for this draft"),
169
- attendees: attendeesInput.describe(
170
- "Invitees — either an array of {email, displayName?} or a comma-separated string of emails",
171
- ),
140
+ attendees: attendeesInput
141
+ .optional()
142
+ .describe(
143
+ "Invitees — either an array of {email, displayName?, optional?} or a comma-separated string of emails. Set optional:true to mark a guest optional.",
144
+ ),
172
145
  accountEmail: z
173
146
  .string()
174
147
  .optional()
@@ -11,9 +11,11 @@ import type { CalendarEvent } from "../shared/api.js";
11
11
  import {
12
12
  availabilityInput,
13
13
  attachmentsInput,
14
+ attendeesInput,
14
15
  buildReminderOverrides,
15
16
  cliBoolean,
16
17
  googleColorIdInput,
18
+ normalizeAttendees,
17
19
  normalizeGoogleEventId,
18
20
  normalizeRecurrence,
19
21
  reminderMethodInput,
@@ -24,27 +26,6 @@ import {
24
26
  visibilityInput,
25
27
  } from "./event-action-helpers.js";
26
28
 
27
- const attendeeInput = z.object({
28
- email: z.string(),
29
- displayName: z.string().optional(),
30
- });
31
-
32
- const attendeesInput = z.union([z.array(attendeeInput), z.string()]);
33
-
34
- function normalizeAttendees(
35
- input: z.infer<typeof attendeesInput> | undefined,
36
- ): CalendarEvent["attendees"] | undefined {
37
- if (input === undefined) return undefined;
38
- if (typeof input === "string") {
39
- const emails = input
40
- .split(/[\s,;]+/)
41
- .map((s) => s.trim())
42
- .filter((s) => s.length > 0 && s.includes("@"));
43
- return emails.map((email) => ({ email }));
44
- }
45
- return input.filter((a) => a.email && a.email.includes("@"));
46
- }
47
-
48
29
  function mergeAttendees(
49
30
  existing: CalendarEvent["attendees"] | undefined,
50
31
  additions: CalendarEvent["attendees"] | undefined,
@@ -71,6 +52,12 @@ function mergeAttendees(
71
52
  email,
72
53
  displayName: attendee.displayName ?? current?.displayName,
73
54
  photoUrl: attendee.photoUrl ?? current?.photoUrl,
55
+ optional:
56
+ attendee.optional === true
57
+ ? true
58
+ : attendee.optional === false
59
+ ? undefined
60
+ : current?.optional,
74
61
  });
75
62
  }
76
63
 
@@ -157,12 +144,12 @@ export default defineAction({
157
144
  attendees: attendeesInput
158
145
  .optional()
159
146
  .describe(
160
- "Replace the event's attendee list. Accepts an array of {email, displayName?} or a comma-separated string of emails. Pass an empty array to clear all attendees.",
147
+ "Replace the event's attendee list. Accepts an array of {email, displayName?, optional?} or a comma-separated string of emails. Pass an empty array to clear all attendees. Set optional:true to mark a guest optional.",
161
148
  ),
162
149
  addAttendees: attendeesInput
163
150
  .optional()
164
151
  .describe(
165
- "Add invitees without dropping or resetting existing attendees. Accepts an array of {email, displayName?} or a comma-separated string of emails.",
152
+ "Add invitees without dropping or resetting existing attendees. Accepts an array of {email, displayName?, optional?} or a comma-separated string of emails. Set optional:true to mark a newly added guest optional.",
166
153
  ),
167
154
  sendUpdates: z
168
155
  .enum(["all", "none"])
@@ -1,6 +1,7 @@
1
1
  import { useT } from "@agent-native/core/client";
2
2
  import {
3
3
  IconAddressBook,
4
+ IconDots,
4
5
  IconLoader2,
5
6
  IconUserCircle,
6
7
  IconUsersGroup,
@@ -18,6 +19,12 @@ import {
18
19
  } from "react";
19
20
 
20
21
  import { Button } from "@/components/ui/button";
22
+ import {
23
+ DropdownMenu,
24
+ DropdownMenuContent,
25
+ DropdownMenuItem,
26
+ DropdownMenuTrigger,
27
+ } from "@/components/ui/dropdown-menu";
21
28
  import {
22
29
  Popover,
23
30
  PopoverAnchor,
@@ -39,6 +46,7 @@ export interface AttendeeRecipient {
39
46
  email: string;
40
47
  displayName?: string;
41
48
  photoUrl?: string;
49
+ optional?: boolean;
42
50
  }
43
51
 
44
52
  export interface AttendeeAutocompleteHandle {
@@ -50,6 +58,7 @@ interface AttendeeAutocompleteProps {
50
58
  selectedEmails?: string[];
51
59
  onAdd: (attendee: AttendeeRecipient) => void;
52
60
  onRemove?: (email: string) => void;
61
+ onToggleOptional?: (email: string, optional: boolean) => void;
53
62
  inputId?: string;
54
63
  placeholder?: string;
55
64
  autoFocus?: boolean;
@@ -116,6 +125,7 @@ export const AttendeeAutocomplete = forwardRef<
116
125
  selectedEmails,
117
126
  onAdd,
118
127
  onRemove,
128
+ onToggleOptional,
119
129
  inputId,
120
130
  placeholder = "Add guests",
121
131
  autoFocus,
@@ -208,6 +218,8 @@ export const AttendeeAutocomplete = forwardRef<
208
218
  email,
209
219
  displayName,
210
220
  photoUrl: person.photoUrl,
221
+ optional:
222
+ "optional" in person && person.optional === true ? true : undefined,
211
223
  });
212
224
  setInputValue("");
213
225
  setOpen(false);
@@ -348,6 +360,41 @@ export const AttendeeAutocomplete = forwardRef<
348
360
  <span className="truncate">
349
361
  {attendee.displayName || attendee.email}
350
362
  </span>
363
+ {attendee.optional && (
364
+ <span className="shrink-0 text-[10px] text-muted-foreground">
365
+ {t("attendees.optionalBadge")}
366
+ </span>
367
+ )}
368
+ {onToggleOptional && (
369
+ <DropdownMenu>
370
+ <DropdownMenuTrigger asChild>
371
+ <button
372
+ type="button"
373
+ onClick={(event) => event.stopPropagation()}
374
+ className="text-muted-foreground hover:text-foreground"
375
+ aria-label={t("attendees.guestOptions", {
376
+ email: attendee.email,
377
+ })}
378
+ >
379
+ <IconDots className="h-3 w-3" />
380
+ </button>
381
+ </DropdownMenuTrigger>
382
+ <DropdownMenuContent
383
+ align="start"
384
+ onClick={(event) => event.stopPropagation()}
385
+ >
386
+ <DropdownMenuItem
387
+ onSelect={() =>
388
+ onToggleOptional(attendee.email, !attendee.optional)
389
+ }
390
+ >
391
+ {attendee.optional
392
+ ? t("attendees.markRequired")
393
+ : t("attendees.markOptional")}
394
+ </DropdownMenuItem>
395
+ </DropdownMenuContent>
396
+ </DropdownMenu>
397
+ )}
351
398
  {onRemove && (
352
399
  <button
353
400
  type="button"
@@ -111,6 +111,12 @@ function uniqueAttendees(attendees: AttendeeRecipient[]) {
111
111
  email,
112
112
  displayName: existing?.displayName ?? attendee.displayName,
113
113
  photoUrl: existing?.photoUrl ?? attendee.photoUrl,
114
+ optional:
115
+ attendee.optional === true
116
+ ? true
117
+ : existing?.optional === true
118
+ ? true
119
+ : undefined,
114
120
  });
115
121
  }
116
122
  return Array.from(byEmail.values());
@@ -304,6 +310,7 @@ export function CreateEventPopover({
304
310
  email: attendee.email,
305
311
  displayName: attendee.displayName,
306
312
  photoUrl: attendee.photoUrl,
313
+ optional: attendee.optional === true ? true : undefined,
307
314
  })),
308
315
  ),
309
316
  );
@@ -393,6 +400,7 @@ export function CreateEventPopover({
393
400
  ? attendees.map((attendee) => ({
394
401
  email: attendee.email,
395
402
  displayName: attendee.displayName,
403
+ ...(attendee.optional === true ? { optional: true } : {}),
396
404
  }))
397
405
  : undefined,
398
406
  addGoogleMeet: videoProvider === "google_meet",
@@ -494,6 +502,19 @@ export function CreateEventPopover({
494
502
  );
495
503
  }
496
504
 
505
+ function toggleAttendeeOptional(email: string, optional: boolean) {
506
+ setAttendees((prev) =>
507
+ prev.map((attendee) =>
508
+ attendee.email.toLowerCase() === email.toLowerCase()
509
+ ? {
510
+ ...attendee,
511
+ optional: optional ? true : undefined,
512
+ }
513
+ : attendee,
514
+ ),
515
+ );
516
+ }
517
+
497
518
  const effectiveAllDay = allDay && !timedOnlyStatus;
498
519
  const currentStartISO =
499
520
  !effectiveAllDay && date && startTime
@@ -639,6 +660,7 @@ export function CreateEventPopover({
639
660
  ? finalAttendees.map((attendee) => ({
640
661
  email: attendee.email,
641
662
  displayName: attendee.displayName,
663
+ ...(attendee.optional === true ? { optional: true } : {}),
642
664
  }))
643
665
  : undefined,
644
666
  };
@@ -925,6 +947,7 @@ export function CreateEventPopover({
925
947
  attendees={attendees}
926
948
  onAdd={addAttendee}
927
949
  onRemove={removeAttendee}
950
+ onToggleOptional={toggleAttendeeOptional}
928
951
  inputId="event-attendees"
929
952
  placeholder={t("eventForm.attendeesPlaceholder")}
930
953
  onEmptyEnter={() => formRef.current?.requestSubmit()}
@@ -1,10 +1,16 @@
1
1
  import { useT } from "@agent-native/core/client";
2
2
  import type { CalendarEvent } from "@shared/api";
3
- import { IconMessageCircle, IconUser } from "@tabler/icons-react";
3
+ 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
7
  import { Button } from "@/components/ui/button";
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuTrigger,
13
+ } from "@/components/ui/dropdown-menu";
8
14
  import { Label } from "@/components/ui/label";
9
15
  import {
10
16
  Popover,
@@ -343,6 +349,8 @@ function AttendeeRow({
343
349
  currentNote,
344
350
  onResponseChange,
345
351
  isRecurring,
352
+ canEditOptional,
353
+ onToggleOptional,
346
354
  }: {
347
355
  attendee: Attendee;
348
356
  event: Pick<CalendarEvent, "id" | "accountEmail">;
@@ -352,15 +360,19 @@ function AttendeeRow({
352
360
  currentNote?: string;
353
361
  onResponseChange?: (status: RsvpStatus, note: string) => void;
354
362
  isRecurring?: boolean;
363
+ canEditOptional?: boolean;
364
+ onToggleOptional?: (email: string, optional: boolean) => void;
355
365
  }) {
356
366
  const t = useT();
357
367
  const displayStatus = inlineRsvp ? currentStatus : attendee.responseStatus;
358
368
  const statusLabel =
359
369
  getLocalizedRsvpStatusLabel(t, displayStatus) ?? t("eventForm.awaiting");
360
370
  const comment = (inlineRsvp ? currentNote : attendee.comment)?.trim();
371
+ const showOptionalMenu =
372
+ canEditOptional && onToggleOptional && !attendee.organizer;
361
373
 
362
374
  return (
363
- <div className="rounded-xl px-1 py-1 transition-colors hover:bg-muted/40">
375
+ <div className="group rounded-xl px-1 py-1 transition-colors hover:bg-muted/40">
364
376
  <AttendeeApolloPopover attendee={attendee}>
365
377
  <div className="flex items-center gap-2.5">
366
378
  <div className="relative shrink-0">
@@ -379,6 +391,11 @@ function AttendeeRow({
379
391
  {t("eventForm.organizer")}
380
392
  </span>
381
393
  )}
394
+ {attendee.optional && !attendee.organizer && (
395
+ <span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
396
+ {t("attendees.optionalBadge")}
397
+ </span>
398
+ )}
382
399
  </div>
383
400
  {attendee.displayName && (
384
401
  <div className="truncate text-[11px] text-muted-foreground/60">
@@ -391,6 +408,38 @@ function AttendeeRow({
391
408
  : statusLabel}
392
409
  </div>
393
410
  </div>
411
+ {showOptionalMenu && (
412
+ <DropdownMenu>
413
+ <DropdownMenuTrigger asChild>
414
+ <Button
415
+ type="button"
416
+ variant="ghost"
417
+ size="icon"
418
+ className="h-7 w-7 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 data-[state=open]:opacity-100"
419
+ aria-label={t("attendees.guestOptions", {
420
+ email: attendee.email,
421
+ })}
422
+ onClick={(event) => event.stopPropagation()}
423
+ >
424
+ <IconDots className="h-3.5 w-3.5" />
425
+ </Button>
426
+ </DropdownMenuTrigger>
427
+ <DropdownMenuContent
428
+ align="end"
429
+ onClick={(event) => event.stopPropagation()}
430
+ >
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>
440
+ </DropdownMenuContent>
441
+ </DropdownMenu>
442
+ )}
394
443
  </div>
395
444
  {comment && (
396
445
  <div className="ml-10 mt-1 flex items-start gap-1.5 rounded-md bg-muted/40 px-2 py-1 text-[11px] leading-relaxed text-muted-foreground">
@@ -425,6 +474,8 @@ function sortAttendees(attendees: Attendee[]) {
425
474
 
426
475
  export function EventAttendeesSection({
427
476
  event,
477
+ canEditOptional = false,
478
+ onToggleOptional,
428
479
  }: {
429
480
  event: Pick<
430
481
  CalendarEvent,
@@ -436,6 +487,8 @@ export function EventAttendeesSection({
436
487
  | "source"
437
488
  | "recurringEventId"
438
489
  >;
490
+ canEditOptional?: boolean;
491
+ onToggleOptional?: (email: string, optional: boolean) => void;
439
492
  }) {
440
493
  const t = useT();
441
494
  const attendees = event.attendees ?? [];
@@ -523,6 +576,8 @@ export function EventAttendeesSection({
523
576
  attendee={attendee}
524
577
  event={event}
525
578
  photoUrl={photos?.[attendee.email.toLowerCase()]}
579
+ canEditOptional={canEditOptional}
580
+ onToggleOptional={onToggleOptional}
526
581
  />
527
582
  ))}
528
583
 
@@ -557,6 +612,8 @@ export function EventAttendeesSection({
557
612
  currentNote={selfNote}
558
613
  onResponseChange={handleSelfResponseChange}
559
614
  isRecurring={!!event.recurringEventId}
615
+ canEditOptional={canEditOptional}
616
+ onToggleOptional={onToggleOptional}
560
617
  />
561
618
  </>
562
619
  )}
@@ -185,6 +185,41 @@ export function EventDetailPanel({
185
185
  })();
186
186
  }, [event, promptGuestNotification, updateEvent]);
187
187
 
188
+ const handleToggleAttendeeOptional = useCallback(
189
+ (email: string, optional: boolean) => {
190
+ if (!event || updateEvent.isPending) return;
191
+ const existing = event.attendees || [];
192
+ const key = email.trim().toLowerCase();
193
+ if (!existing.some((attendee) => attendee.email.toLowerCase() === key)) {
194
+ return;
195
+ }
196
+ const attendees = existing.map((attendee) =>
197
+ attendee.email.toLowerCase() === key
198
+ ? {
199
+ ...attendee,
200
+ optional: optional ? true : undefined,
201
+ }
202
+ : attendee,
203
+ );
204
+ void (async () => {
205
+ const updates = { attendees };
206
+ const guestNotification = await promptGuestNotification({
207
+ event,
208
+ action: "update",
209
+ updates,
210
+ });
211
+ if (!guestNotification) return;
212
+ updateEvent.mutate({
213
+ id: event.id,
214
+ accountEmail: event.accountEmail,
215
+ ...updates,
216
+ ...guestNotification,
217
+ });
218
+ })();
219
+ },
220
+ [event, promptGuestNotification, updateEvent],
221
+ );
222
+
188
223
  return (
189
224
  <TooltipProvider>
190
225
  {isOpen && (
@@ -414,7 +449,11 @@ export function EventDetailPanel({
414
449
 
415
450
  {/* Attendees */}
416
451
  {event.attendees && event.attendees.length > 0 && (
417
- <EventAttendeesSection event={event} />
452
+ <EventAttendeesSection
453
+ event={event}
454
+ canEditOptional={!isOverlay}
455
+ onToggleOptional={handleToggleAttendeeOptional}
456
+ />
418
457
  )}
419
458
 
420
459
  {/* Research Meeting */}
@@ -294,6 +294,12 @@ function mergeAttendeesForPrompt(
294
294
  email,
295
295
  displayName: attendee.displayName ?? current?.displayName,
296
296
  photoUrl: attendee.photoUrl ?? current?.photoUrl,
297
+ optional:
298
+ attendee.optional === true
299
+ ? true
300
+ : attendee.optional === false
301
+ ? undefined
302
+ : current?.optional,
297
303
  });
298
304
  }
299
305
 
@@ -939,6 +945,7 @@ export function EventDetailPopover({
939
945
  email: attendee.email,
940
946
  displayName: attendee.displayName,
941
947
  photoUrl: attendee.photoUrl,
948
+ optional: attendee.optional === true ? true : undefined,
942
949
  })),
943
950
  [event.accountEmail, event.attendees],
944
951
  );
@@ -1004,6 +1011,7 @@ export function EventDetailPopover({
1004
1011
  email,
1005
1012
  displayName: attendee.displayName,
1006
1013
  photoUrl: attendee.photoUrl,
1014
+ ...(attendee.optional === true ? { optional: true as const } : {}),
1007
1015
  };
1008
1016
 
1009
1017
  if (isDraft) {
@@ -1015,6 +1023,27 @@ export function EventDetailPopover({
1015
1023
  [event.attendees, isDraft, saveField],
1016
1024
  );
1017
1025
 
1026
+ const handleToggleAttendeeOptional = useCallback(
1027
+ (email: string, optional: boolean) => {
1028
+ const existing = event.attendees || [];
1029
+ const key = email.trim().toLowerCase();
1030
+ if (!existing.some((attendee) => attendee.email.toLowerCase() === key)) {
1031
+ return;
1032
+ }
1033
+ saveField({
1034
+ attendees: existing.map((attendee) =>
1035
+ attendee.email.toLowerCase() === key
1036
+ ? {
1037
+ ...attendee,
1038
+ optional: optional ? true : undefined,
1039
+ }
1040
+ : attendee,
1041
+ ),
1042
+ });
1043
+ },
1044
+ [event.attendees, saveField],
1045
+ );
1046
+
1018
1047
  const handleSaveMeetingLink = useCallback(() => {
1019
1048
  const url = editMeetingLink.trim();
1020
1049
  let saved = false;
@@ -1606,7 +1635,11 @@ export function EventDetailPopover({
1606
1635
 
1607
1636
  {/* Attendees — always shown */}
1608
1637
  {event.attendees && event.attendees.length > 0 ? (
1609
- <EventAttendeesSection event={event} />
1638
+ <EventAttendeesSection
1639
+ event={event}
1640
+ canEditOptional={!isOverlay}
1641
+ onToggleOptional={handleToggleAttendeeOptional}
1642
+ />
1610
1643
  ) : !isOverlay ? (
1611
1644
  <div className="px-4 py-1">
1612
1645
  <div className="flex items-start gap-3">
@@ -136,6 +136,12 @@ export function mergeAttendeeLists(
136
136
  email,
137
137
  displayName: attendee.displayName ?? current?.displayName,
138
138
  photoUrl: attendee.photoUrl ?? current?.photoUrl,
139
+ optional:
140
+ attendee.optional === true
141
+ ? true
142
+ : attendee.optional === false
143
+ ? undefined
144
+ : current?.optional,
139
145
  });
140
146
  }
141
147
 
@@ -126,7 +126,11 @@ const messages = {
126
126
  contacts: "聯絡人",
127
127
  contactsReconnect: "需要重新連線聯絡人或目錄存取權限。",
128
128
  directory: "目錄",
129
+ guestOptions: "{{email}} 的來賓選項",
129
130
  inviteEmail: "邀請 {{email}}",
131
+ markOptional: "標為可選",
132
+ markRequired: "標為必選",
133
+ optionalBadge: "可選",
130
134
  otherContacts: "其他聯絡人",
131
135
  removeAttendee: "移除 {{email}}",
132
136
  searchingPeople: "正在搜尋人員",