@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
- package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
- package/corpus/templates/calendar/app/i18n-data.ts +120 -0
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
- package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +1 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +3 -0
- package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +5 -0
- package/corpus/templates/clips/desktop/src/app.tsx +78 -16
- package/corpus/templates/clips/server/lib/slack-unfurls.ts +7 -13
- package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
- package/corpus/templates/clips/shared/share-meta.ts +67 -3
- package/corpus/templates/factory/AGENTS.md +1 -1
- package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
- package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
- package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
- package/corpus/templates/factory/app/i18n/en-US.ts +4 -0
- package/corpus/templates/factory/app/routes/factory.tsx +209 -46
- package/corpus/templates/factory/server/connectors/slack.ts +44 -2
- package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -7
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
- package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
- package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
- package/corpus/templates/mail/server/handlers/google-auth.ts +105 -2
- package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/dist/cli/code-agent-schedules.d.ts +11 -0
- package/dist/cli/code-agent-schedules.js +44 -5
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/jobs/background-automation-runner.js +111 -9
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/auth.d.ts +17 -1
- package/dist/server/auth.js +369 -153
- package/dist/server/google-auth-plugin.js +3 -1
- package/dist/server/google-oauth.d.ts +10 -0
- package/dist/server/google-oauth.js +25 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/onboarding-html.js +79 -30
- package/docs/content/drop-in-agent.mdx +4 -4
- package/docs/content/faq.mdx +15 -15
- package/docs/content/getting-started.mdx +173 -41
- package/docs/content/key-concepts.mdx +10 -10
- package/docs/content/using-your-agent.mdx +2 -2
- package/docs/content/what-is-agent-native.mdx +79 -228
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -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 =
|
|
482
|
-
recurrencePreset
|
|
483
|
-
|
|
484
|
-
|
|
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 =
|
|
771
|
-
recurrencePreset
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
<
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1457
|
+
<RepeatPicker
|
|
1458
|
+
preset={recurrencePreset}
|
|
1459
|
+
referenceDate={
|
|
1460
|
+
effectiveAllDay ? date : currentStartISO || date
|
|
1442
1461
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
|
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=
|
|
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
|
-
{
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
</
|
|
360
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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) {
|