@agent-native/core 0.0.0-beta-20260819165421 → 0.0.0-beta-20260819172516

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 (63) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
  3. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
  4. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
  5. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
  6. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
  7. package/corpus/templates/calendar/app/i18n-data.ts +120 -0
  8. package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
  9. package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
  10. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  11. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  12. package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  13. package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  14. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +1 -1
  15. package/corpus/templates/clips/app/routes/share.$shareId.tsx +3 -0
  16. package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +5 -0
  17. package/corpus/templates/clips/desktop/src/app.tsx +78 -16
  18. package/corpus/templates/clips/server/lib/slack-unfurls.ts +7 -13
  19. package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  20. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
  21. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  22. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
  23. package/corpus/templates/clips/shared/share-meta.ts +67 -3
  24. package/corpus/templates/factory/AGENTS.md +1 -1
  25. package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
  26. package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
  27. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
  28. package/corpus/templates/factory/app/i18n/en-US.ts +4 -0
  29. package/corpus/templates/factory/app/routes/factory.tsx +209 -46
  30. package/corpus/templates/factory/server/connectors/slack.ts +44 -2
  31. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -7
  32. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
  33. package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
  34. package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
  35. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
  36. package/corpus/templates/mail/server/handlers/google-auth.ts +105 -2
  37. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  38. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  39. package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  40. package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  41. package/dist/cli/code-agent-schedules.d.ts +11 -0
  42. package/dist/cli/code-agent-schedules.js +44 -5
  43. package/dist/collab/awareness.d.ts +2 -2
  44. package/dist/collab/struct-routes.d.ts +1 -1
  45. package/dist/jobs/background-automation-runner.js +111 -9
  46. package/dist/notifications/routes.d.ts +3 -3
  47. package/dist/progress/routes.d.ts +1 -1
  48. package/dist/secrets/routes.d.ts +3 -3
  49. package/dist/server/auth.d.ts +17 -1
  50. package/dist/server/auth.js +369 -153
  51. package/dist/server/google-auth-plugin.js +3 -1
  52. package/dist/server/google-oauth.d.ts +10 -0
  53. package/dist/server/google-oauth.js +25 -1
  54. package/dist/server/index.d.ts +1 -1
  55. package/dist/server/index.js +1 -1
  56. package/dist/server/onboarding-html.js +79 -30
  57. package/docs/content/drop-in-agent.mdx +4 -4
  58. package/docs/content/faq.mdx +15 -15
  59. package/docs/content/getting-started.mdx +173 -41
  60. package/docs/content/key-concepts.mdx +10 -10
  61. package/docs/content/using-your-agent.mdx +2 -2
  62. package/docs/content/what-is-agent-native.mdx +79 -228
  63. package/package.json +3 -3
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8446
34
+ - template files: 8457
@@ -27,6 +27,7 @@ import {
27
27
  ReminderControls,
28
28
  } from "@/components/calendar/EventOptionControls";
29
29
  import { FindTimeTakeover } from "@/components/calendar/FindTimePanel";
30
+ import { RepeatPicker } from "@/components/calendar/InlineEventPickers";
30
31
  import { TimezoneCombobox } from "@/components/TimezoneCombobox";
31
32
  import { Button } from "@/components/ui/button";
32
33
  import {
@@ -71,6 +72,7 @@ import { getGoogleEventColorHex } from "@/lib/event-colors";
71
72
  import { buildEventFormInitializationKey } from "@/lib/event-form-initialization";
72
73
  import {
73
74
  attachmentsToDrafts,
75
+ buildCustomRecurrenceRules,
74
76
  buildRecurrenceRules,
75
77
  buildReminderPayload,
76
78
  createAttachmentDraft,
@@ -78,9 +80,11 @@ import {
78
80
  dateTimeInTimezoneToIso,
79
81
  getEventEndValidationMessage,
80
82
  getRecurrencePreset,
83
+ parseCustomRecurrence,
81
84
  remindersToDraftState,
82
85
  resolveEventTimezone,
83
86
  type AttachmentDraft,
87
+ type CustomRecurrenceDraft,
84
88
  type RecurrencePreset,
85
89
  type ReminderDraft,
86
90
  type ReminderMode,
@@ -270,6 +274,10 @@ export function CreateEventPopover({
270
274
  const [visibility, setVisibility] = useState<Visibility>("default");
271
275
  const [recurrencePreset, setRecurrencePreset] =
272
276
  useState<RecurrencePreset>("none");
277
+ const [customRecurrence, setCustomRecurrence] =
278
+ useState<CustomRecurrenceDraft>(() =>
279
+ parseCustomRecurrence(undefined, defaultDateStr),
280
+ );
273
281
  const [timezone, setTimezone] = useState(defaultTimezone);
274
282
  const [colorId, setColorId] = useState<string | undefined>();
275
283
  const [reminderMode, setReminderMode] = useState<ReminderMode>("default");
@@ -377,6 +385,9 @@ export function CreateEventPopover({
377
385
  setAvailability(draft.transparency ?? "opaque");
378
386
  setVisibility(draft.visibility ?? "default");
379
387
  setRecurrencePreset(getRecurrencePreset(draft.recurrence));
388
+ setCustomRecurrence(
389
+ parseCustomRecurrence(draft.recurrence, draft.start || nextDate),
390
+ );
380
391
  setTimezone(draftTimezone);
381
392
  setColorId(draft.colorId);
382
393
  setReminderMode(reminderState.mode);
@@ -416,6 +427,7 @@ export function CreateEventPopover({
416
427
  setAvailability("opaque");
417
428
  setVisibility("default");
418
429
  setRecurrencePreset("none");
430
+ setCustomRecurrence(parseCustomRecurrence(undefined, nextDate));
419
431
  setTimezone(defaultTimezone);
420
432
  setColorId(undefined);
421
433
  setReminderMode("default");
@@ -478,11 +490,14 @@ export function CreateEventPopover({
478
490
  : dateTimeInTimezoneToIso(endDate, endTime, eventTimezone);
479
491
  const attachmentResult = validateAttachmentDrafts(attachments);
480
492
  const reminderPatch = buildReminderPayload(reminderMode, reminders);
481
- const recurrence = buildRecurrenceRules(
482
- recurrencePreset,
483
- effectiveAllDay ? date : startValue,
484
- eventTimezone,
485
- );
493
+ const recurrence =
494
+ recurrencePreset === "custom"
495
+ ? buildCustomRecurrenceRules(customRecurrence)
496
+ : buildRecurrenceRules(
497
+ recurrencePreset,
498
+ effectiveAllDay ? date : startValue,
499
+ eventTimezone,
500
+ );
486
501
  const nextDraft: CalendarEventDraft = {
487
502
  id: draftId,
488
503
  createdAt: draft?.createdAt,
@@ -569,6 +584,7 @@ export function CreateEventPopover({
569
584
  availability,
570
585
  visibility,
571
586
  recurrencePreset,
587
+ customRecurrence,
572
588
  eventTimezone,
573
589
  colorId,
574
590
  reminderMode,
@@ -767,11 +783,14 @@ export function CreateEventPopover({
767
783
  ...trailingAttendees,
768
784
  ]);
769
785
  const reminderPatch = buildReminderPayload(reminderMode, reminders);
770
- const recurrence = buildRecurrenceRules(
771
- recurrencePreset,
772
- effectiveAllDay ? date : startValue,
773
- eventTimezone,
774
- );
786
+ const recurrence =
787
+ recurrencePreset === "custom"
788
+ ? buildCustomRecurrenceRules(customRecurrence)
789
+ : buildRecurrenceRules(
790
+ recurrencePreset,
791
+ effectiveAllDay ? date : startValue,
792
+ eventTimezone,
793
+ );
775
794
  const statusPatch =
776
795
  eventType === "default"
777
796
  ? {}
@@ -1435,44 +1454,14 @@ export function CreateEventPopover({
1435
1454
  <Label htmlFor="event-recurrence" className="text-xs">
1436
1455
  {t("eventForm.repeats")}
1437
1456
  </Label>
1438
- <Select
1439
- value={recurrencePreset}
1440
- onValueChange={(value) =>
1441
- setRecurrencePreset(value as RecurrencePreset)
1457
+ <RepeatPicker
1458
+ preset={recurrencePreset}
1459
+ referenceDate={
1460
+ effectiveAllDay ? date : currentStartISO || date
1442
1461
  }
1443
- >
1444
- <SelectTrigger
1445
- id="event-recurrence"
1446
- className="h-8 text-sm"
1447
- >
1448
- <SelectValue />
1449
- </SelectTrigger>
1450
- <SelectContent>
1451
- <SelectItem value="none">
1452
- {t("eventForm.doesNotRepeat")}
1453
- </SelectItem>
1454
- <SelectItem value="daily">
1455
- {t("eventForm.daily")}
1456
- </SelectItem>
1457
- <SelectItem value="weekdays">
1458
- {t("eventForm.everyWeekday")}
1459
- </SelectItem>
1460
- <SelectItem value="weekly">
1461
- {t("eventForm.weekly")}
1462
- </SelectItem>
1463
- <SelectItem value="monthly">
1464
- {t("eventForm.monthly")}
1465
- </SelectItem>
1466
- <SelectItem value="yearly">
1467
- {t("eventForm.yearly")}
1468
- </SelectItem>
1469
- {recurrencePreset === "custom" && (
1470
- <SelectItem value="custom" disabled>
1471
- {t("eventForm.customSchedule")}
1472
- </SelectItem>
1473
- )}
1474
- </SelectContent>
1475
- </Select>
1462
+ onChange={setRecurrencePreset}
1463
+ onCustomChange={setCustomRecurrence}
1464
+ />
1476
1465
  </div>
1477
1466
 
1478
1467
  {(!allDay || isOutOfOffice) && (
@@ -441,6 +441,7 @@ const DayEventCard = memo(function DayEventCard({
441
441
  onDelete={onDeleteEvent}
442
442
  isDraft={isDraft}
443
443
  defaultOpen={defaultOpen}
444
+ popoverSide="bottom"
444
445
  onTitleSave={onQuickEditSave}
445
446
  onDismissNew={onQuickEditCancel}
446
447
  onDraftUpdate={onDraftUpdate}
@@ -810,6 +811,7 @@ export const DayView = memo(function DayView({
810
811
  onDelete={onDeleteEvent}
811
812
  isDraft={draftEventIds.includes(event.id)}
812
813
  defaultOpen={quickEditEventId === event.id}
814
+ popoverSide="bottom"
813
815
  onTitleSave={onQuickEditSave}
814
816
  onDismissNew={onQuickEditCancel}
815
817
  onDraftUpdate={onDraftUpdate}
@@ -887,6 +889,7 @@ export const DayView = memo(function DayView({
887
889
  onDelete={onDeleteEvent}
888
890
  isDraft={draftEventIds.includes(event.id)}
889
891
  defaultOpen={quickEditEventId === event.id}
892
+ popoverSide="bottom"
890
893
  onTitleSave={onQuickEditSave}
891
894
  onDismissNew={onQuickEditCancel}
892
895
  onDraftUpdate={onDraftUpdate}
@@ -82,6 +82,7 @@ import {
82
82
  } from "@/lib/calendar-timezone";
83
83
  import {
84
84
  attachmentsToDrafts,
85
+ buildCustomRecurrenceRules,
85
86
  buildRecurrenceRules,
86
87
  buildReminderPayload,
87
88
  dateTimeInTimezoneToIso,
@@ -90,6 +91,7 @@ import {
90
91
  getEventEndValidationMessage,
91
92
  getLocalTimezone,
92
93
  getRecurrencePreset,
94
+ type CustomRecurrenceDraft,
93
95
  remindersToDraftState,
94
96
  resolveTimeEditScope,
95
97
  type AttachmentDraft,
@@ -468,6 +470,8 @@ interface EventDetailPopoverProps {
468
470
  onDismissNew?: (eventId: string, accountEmail?: string) => void;
469
471
  /** Called after the popover's visible open state changes through its normal lifecycle. */
470
472
  onOpenChange?: (open: boolean) => void;
473
+ /** Prefer a placement that keeps Day-view detail controls inside the grid. */
474
+ popoverSide?: "top" | "right" | "bottom" | "left";
471
475
  onDraftUpdate?: (
472
476
  eventId: string,
473
477
  updates: Partial<CalendarEvent> & {
@@ -500,6 +504,7 @@ export function EventDetailPopover({
500
504
  onDraftUpdate,
501
505
  onDraftCreate,
502
506
  onDraftDiscard,
507
+ popoverSide,
503
508
  }: EventDetailPopoverProps) {
504
509
  const t = useT();
505
510
  const workingLocationLabels = createWorkingLocationDisplayLabels(t);
@@ -1307,6 +1312,16 @@ export function EventDetailPopover({
1307
1312
  ],
1308
1313
  );
1309
1314
 
1315
+ const handleSaveCustomRecurrence = useCallback(
1316
+ (draft: CustomRecurrenceDraft) => {
1317
+ saveField({
1318
+ recurrence: buildCustomRecurrenceRules(draft),
1319
+ scope: isRecurringEvent ? "all" : "single",
1320
+ });
1321
+ },
1322
+ [isRecurringEvent, saveField],
1323
+ );
1324
+
1310
1325
  const handleAddAttendee = useCallback(
1311
1326
  (attendee: AttendeeRecipient) => {
1312
1327
  const email = attendee.email.trim().toLowerCase();
@@ -1521,7 +1536,7 @@ export function EventDetailPopover({
1521
1536
  </PopoverTrigger>
1522
1537
  <PopoverContent
1523
1538
  align={isMobile ? "center" : "start"}
1524
- side={isMobile ? "bottom" : "right"}
1539
+ side={isMobile ? "bottom" : (popoverSide ?? "right")}
1525
1540
  sideOffset={isMobile ? 6 : 8}
1526
1541
  collisionPadding={12}
1527
1542
  className="flex max-h-[var(--radix-popover-content-available-height)] w-[min(420px,calc(100vw-2rem))] flex-col overflow-hidden p-0"
@@ -1790,7 +1805,9 @@ export function EventDetailPopover({
1790
1805
  <RepeatPicker
1791
1806
  preset={getRecurrencePreset(recurrenceRules)}
1792
1807
  referenceDate={event.start}
1808
+ recurrence={recurrenceRules}
1793
1809
  onChange={handleSaveRecurrence}
1810
+ onCustomChange={handleSaveCustomRecurrence}
1794
1811
  />
1795
1812
  )}
1796
1813
 
@@ -16,9 +16,18 @@ import {
16
16
  PopoverContent,
17
17
  PopoverTrigger,
18
18
  } from "@/components/ui/popover";
19
+ import {
20
+ Select,
21
+ SelectContent,
22
+ SelectItem,
23
+ SelectTrigger,
24
+ SelectValue,
25
+ } from "@/components/ui/select";
19
26
  import {
20
27
  formatTimezoneLabel,
28
+ parseCustomRecurrence,
21
29
  type RecurrencePreset,
30
+ type CustomRecurrenceDraft,
22
31
  } from "@/lib/event-form-utils";
23
32
  import { cn } from "@/lib/utils";
24
33
 
@@ -247,14 +256,22 @@ export function TimezonePickerPopover({
247
256
  export function RepeatPicker({
248
257
  preset,
249
258
  referenceDate,
259
+ recurrence,
250
260
  onChange,
261
+ onCustomChange,
251
262
  }: {
252
263
  preset: RecurrencePreset;
253
264
  referenceDate: string;
265
+ recurrence?: string[];
254
266
  onChange: (preset: RecurrencePreset) => void;
267
+ onCustomChange?: (draft: CustomRecurrenceDraft) => void;
255
268
  }) {
256
269
  const t = useT();
257
270
  const [open, setOpen] = useState(false);
271
+ const [customOpen, setCustomOpen] = useState(false);
272
+ const [customDraft, setCustomDraft] = useState(() =>
273
+ parseCustomRecurrence(recurrence, referenceDate),
274
+ );
258
275
  const reference = new Date(referenceDate);
259
276
  const weekday = Number.isNaN(reference.getTime())
260
277
  ? ""
@@ -297,11 +314,7 @@ export function RepeatPicker({
297
314
  ? t("eventForm.onDate", { date: format(reference, "MMM d") })
298
315
  : undefined,
299
316
  },
300
- {
301
- value: "custom",
302
- label: t("eventForm.customSchedule"),
303
- disabled: true,
304
- },
317
+ { value: "custom", label: t("eventForm.customSchedule") },
305
318
  ];
306
319
 
307
320
  const selectedOption = options.find((option) => option.value === preset);
@@ -309,7 +322,17 @@ export function RepeatPicker({
309
322
  preset === "none" ? t("eventForm.repeat") : selectedOption?.label;
310
323
 
311
324
  return (
312
- <Popover open={open} onOpenChange={setOpen}>
325
+ <Popover
326
+ open={open}
327
+ onOpenChange={(nextOpen) => {
328
+ setOpen(nextOpen);
329
+ if (nextOpen && preset === "custom") {
330
+ setCustomDraft(parseCustomRecurrence(recurrence, referenceDate));
331
+ setCustomOpen(true);
332
+ }
333
+ if (!nextOpen) setCustomOpen(false);
334
+ }}
335
+ >
313
336
  <PopoverTrigger asChild>
314
337
  <Button
315
338
  type="button"
@@ -325,42 +348,237 @@ export function RepeatPicker({
325
348
  <PopoverContent
326
349
  align="start"
327
350
  sideOffset={6}
328
- className="w-64 p-1"
351
+ className={cn(
352
+ "p-1",
353
+ customOpen ? "w-[min(22rem,calc(100vw-2rem))]" : "w-64",
354
+ )}
329
355
  data-time-picker-popover
330
356
  >
331
- {options.map((option) => {
332
- const selected = option.value === preset;
333
- return (
334
- <button
335
- key={option.value}
336
- type="button"
337
- disabled={option.disabled}
338
- className={cn(
339
- "flex w-full items-center justify-between rounded-md px-2.5 py-2 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground",
340
- selected && "bg-accent text-accent-foreground",
341
- option.disabled &&
342
- "cursor-not-allowed opacity-50 hover:bg-transparent",
343
- )}
344
- onClick={() => {
345
- if (option.disabled) return;
346
- onChange(option.value);
347
- setOpen(false);
348
- }}
349
- >
350
- <span>{option.label}</span>
351
- <span className="ml-3 flex items-center gap-2 text-muted-foreground">
352
- {option.meta}
353
- <span
354
- aria-hidden="true"
355
- className="flex size-3.5 shrink-0 items-center justify-center"
356
- >
357
- {selected && <IconCheck className="size-3.5" />}
357
+ {customOpen ? (
358
+ <CustomRecurrenceEditor
359
+ draft={customDraft}
360
+ onChange={setCustomDraft}
361
+ onCancel={() => {
362
+ setCustomOpen(false);
363
+ setOpen(false);
364
+ }}
365
+ onSave={() => {
366
+ onCustomChange?.(customDraft);
367
+ onChange("custom");
368
+ setCustomOpen(false);
369
+ setOpen(false);
370
+ }}
371
+ />
372
+ ) : (
373
+ options.map((option) => {
374
+ const selected = option.value === preset;
375
+ return (
376
+ <button
377
+ key={option.value}
378
+ type="button"
379
+ className={cn(
380
+ "flex w-full items-center justify-between rounded-md px-2.5 py-2 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground",
381
+ selected && "bg-accent text-accent-foreground",
382
+ )}
383
+ onClick={() => {
384
+ if (option.value === "custom") {
385
+ setCustomDraft(
386
+ parseCustomRecurrence(recurrence, referenceDate),
387
+ );
388
+ setCustomOpen(true);
389
+ return;
390
+ }
391
+ onChange(option.value);
392
+ setOpen(false);
393
+ }}
394
+ >
395
+ <span>{option.label}</span>
396
+ <span className="ml-3 flex items-center gap-2 text-muted-foreground">
397
+ {option.meta}
398
+ <span
399
+ aria-hidden="true"
400
+ className="flex size-3.5 shrink-0 items-center justify-center"
401
+ >
402
+ {selected && <IconCheck className="size-3.5" />}
403
+ </span>
358
404
  </span>
359
- </span>
360
- </button>
361
- );
362
- })}
405
+ </button>
406
+ );
407
+ })
408
+ )}
363
409
  </PopoverContent>
364
410
  </Popover>
365
411
  );
366
412
  }
413
+
414
+ const CUSTOM_WEEKDAYS = [
415
+ ["SU", "S"],
416
+ ["MO", "M"],
417
+ ["TU", "T"],
418
+ ["WE", "W"],
419
+ ["TH", "T"],
420
+ ["FR", "F"],
421
+ ["SA", "S"],
422
+ ] as const;
423
+
424
+ function CustomRecurrenceEditor({
425
+ draft,
426
+ onChange,
427
+ onCancel,
428
+ onSave,
429
+ }: {
430
+ draft: CustomRecurrenceDraft;
431
+ onChange: (draft: CustomRecurrenceDraft) => void;
432
+ onCancel: () => void;
433
+ onSave: () => void;
434
+ }) {
435
+ const t = useT();
436
+ const update = (changes: Partial<CustomRecurrenceDraft>) =>
437
+ onChange({ ...draft, ...changes });
438
+ const toggleDay = (day: string) => {
439
+ const days = draft.days.includes(day)
440
+ ? draft.days.filter((value) => value !== day)
441
+ : [...draft.days, day];
442
+ update({ days });
443
+ };
444
+
445
+ return (
446
+ <div className="max-h-[min(32rem,var(--radix-popover-content-available-height))] overflow-y-auto p-3">
447
+ <div className="mb-3 text-base font-semibold text-foreground">
448
+ {t("eventForm.customRecurrenceTitle")}
449
+ </div>
450
+ <div className="flex items-center gap-2">
451
+ <span className="text-sm text-muted-foreground">
452
+ {t("eventForm.repeatEvery")}
453
+ </span>
454
+ <Input
455
+ type="number"
456
+ min={1}
457
+ max={999}
458
+ value={draft.interval}
459
+ onChange={(event) =>
460
+ update({ interval: Math.max(1, Number(event.target.value) || 1) })
461
+ }
462
+ className="h-9 w-16 text-center"
463
+ aria-label={t("eventForm.repeatEvery")}
464
+ />
465
+ <Select
466
+ value={draft.unit}
467
+ onValueChange={(unit) =>
468
+ update({
469
+ unit: unit as CustomRecurrenceDraft["unit"],
470
+ days: unit === "week" ? draft.days : [],
471
+ })
472
+ }
473
+ >
474
+ <SelectTrigger className="h-9 flex-1">
475
+ <SelectValue />
476
+ </SelectTrigger>
477
+ <SelectContent>
478
+ <SelectItem value="day">{t("eventForm.day")}</SelectItem>
479
+ <SelectItem value="week">{t("eventForm.week")}</SelectItem>
480
+ <SelectItem value="month">{t("eventForm.month")}</SelectItem>
481
+ <SelectItem value="year">{t("eventForm.year")}</SelectItem>
482
+ </SelectContent>
483
+ </Select>
484
+ </div>
485
+
486
+ {draft.unit === "week" && (
487
+ <div className="mt-4">
488
+ <div className="mb-2 text-sm text-muted-foreground">
489
+ {t("eventForm.repeatOn")}
490
+ </div>
491
+ <div className="flex justify-between gap-1">
492
+ {CUSTOM_WEEKDAYS.map(([value, label]) => {
493
+ const selected = draft.days.includes(value);
494
+ return (
495
+ <Button
496
+ key={value}
497
+ type="button"
498
+ variant={selected ? "default" : "secondary"}
499
+ size="icon"
500
+ className="size-8 rounded-full text-xs"
501
+ aria-pressed={selected}
502
+ onClick={() => toggleDay(value)}
503
+ >
504
+ {label}
505
+ </Button>
506
+ );
507
+ })}
508
+ </div>
509
+ </div>
510
+ )}
511
+
512
+ <fieldset className="mt-5 space-y-2">
513
+ <legend className="mb-2 text-sm text-muted-foreground">
514
+ {t("eventForm.ends")}
515
+ </legend>
516
+ <label className="flex items-center gap-2 text-sm text-foreground">
517
+ <input
518
+ type="radio"
519
+ name="custom-recurrence-end"
520
+ checked={draft.endMode === "never"}
521
+ onChange={() => update({ endMode: "never" })}
522
+ />
523
+ {t("eventForm.never")}
524
+ </label>
525
+ <label className="flex items-center gap-2 text-sm text-foreground">
526
+ <input
527
+ type="radio"
528
+ name="custom-recurrence-end"
529
+ checked={draft.endMode === "date"}
530
+ onChange={() => update({ endMode: "date" })}
531
+ />
532
+ <span>{t("eventForm.on")}</span>
533
+ <Input
534
+ type="date"
535
+ value={draft.endDate}
536
+ onChange={(event) =>
537
+ update({ endMode: "date", endDate: event.target.value })
538
+ }
539
+ className="h-8 min-w-0 flex-1"
540
+ />
541
+ </label>
542
+ <label className="flex items-center gap-2 text-sm text-foreground">
543
+ <input
544
+ type="radio"
545
+ name="custom-recurrence-end"
546
+ checked={draft.endMode === "count"}
547
+ onChange={() => update({ endMode: "count" })}
548
+ />
549
+ <span>{t("eventForm.after")}</span>
550
+ <Input
551
+ type="number"
552
+ min={1}
553
+ max={999}
554
+ value={draft.count}
555
+ onChange={(event) =>
556
+ update({
557
+ endMode: "count",
558
+ count: Math.max(1, Number(event.target.value) || 1),
559
+ })
560
+ }
561
+ className="h-8 w-20"
562
+ />
563
+ <span className="text-muted-foreground">
564
+ {t("eventForm.occurrences")}
565
+ </span>
566
+ </label>
567
+ </fieldset>
568
+
569
+ <div className="mt-5 flex justify-end gap-2 border-t border-border pt-3">
570
+ <Button type="button" variant="ghost" size="sm" onClick={onCancel}>
571
+ {t("eventForm.cancel")}
572
+ </Button>
573
+ <Button
574
+ type="button"
575
+ size="sm"
576
+ disabled={draft.unit === "week" && draft.days.length === 0}
577
+ onClick={onSave}
578
+ >
579
+ {t("eventForm.save")}
580
+ </Button>
581
+ </div>
582
+ </div>
583
+ );
584
+ }
@@ -45,6 +45,25 @@ const DESKTOP_AUTH_POLL_ABORT_MS = Math.max(
45
45
  DESKTOP_AUTH_POLL_INTERVAL_MS * 4,
46
46
  );
47
47
 
48
+ function newDesktopOAuthVerifier(): string | null {
49
+ const cryptoApi = globalThis.crypto;
50
+ const randomUuid = cryptoApi?.randomUUID;
51
+ if (typeof randomUuid === "function") {
52
+ return `${randomUuid.call(cryptoApi)}${randomUuid.call(cryptoApi)}`;
53
+ }
54
+ if (typeof cryptoApi?.getRandomValues === "function") {
55
+ const bytes = new Uint8Array(32);
56
+ cryptoApi.getRandomValues(bytes);
57
+ let binary = "";
58
+ for (const byte of bytes) binary += String.fromCharCode(byte);
59
+ return btoa(binary)
60
+ .replace(/\+/g, "-")
61
+ .replace(/\//g, "_")
62
+ .replace(/=+$/, "");
63
+ }
64
+ return null;
65
+ }
66
+
48
67
  function bodyError(
49
68
  body: any,
50
69
  raw: string | undefined,
@@ -229,6 +248,15 @@ export function useGoogleDesktopAuth(options: DesktopAuthOptions = {}) {
229
248
  const flowId =
230
249
  globalThis.crypto?.randomUUID?.() ||
231
250
  Math.random().toString(36).slice(2) + Date.now().toString(36);
251
+ const verifier = newDesktopOAuthVerifier();
252
+ if (!verifier) {
253
+ setIsPending(false);
254
+ onError?.({
255
+ code: "desktop_auth_start_failed",
256
+ message: "Secure OAuth verifier generation is unavailable.",
257
+ });
258
+ return true;
259
+ }
232
260
  const redirectUri = oauthRedirectUri("/_agent-native/google/callback");
233
261
  const params = new URLSearchParams({
234
262
  redirect_uri: redirectUri,
@@ -285,7 +313,13 @@ export function useGoogleDesktopAuth(options: DesktopAuthOptions = {}) {
285
313
  : "/_agent-native/google/auth-url";
286
314
  const { url } = await fetchJson<{ url: string }>(
287
315
  agentNativePath(`${path}?${params.toString()}`),
288
- { credentials: "include" },
316
+ {
317
+ method: "POST",
318
+ credentials: "include",
319
+ headers: {
320
+ "X-Agent-Native-Desktop-Verifier": verifier,
321
+ },
322
+ },
289
323
  );
290
324
  openAuthUrl(url);
291
325
  } catch (err) {
@@ -305,9 +339,15 @@ export function useGoogleDesktopAuth(options: DesktopAuthOptions = {}) {
305
339
  try {
306
340
  const exchangeRes = await fetch(
307
341
  agentNativePath(
308
- `/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
342
+ `/_agent-native/auth/desktop-exchange?flow_id=${encodeURIComponent(flowId)}`,
309
343
  ),
310
- { credentials: "include", signal: controller.signal },
344
+ {
345
+ credentials: "include",
346
+ headers: {
347
+ "X-Agent-Native-Desktop-Verifier": verifier,
348
+ },
349
+ signal: controller.signal,
350
+ },
311
351
  );
312
352
  const exchange = await exchangeRes.json();
313
353
  if (exchange?.error) {