@agent-native/core 0.84.17 → 0.84.20

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 (75) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/run-store.ts +180 -13
  5. package/corpus/core/src/client/AssistantChat.tsx +49 -0
  6. package/corpus/core/src/client/active-run-state.ts +30 -0
  7. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +41 -3
  8. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +133 -0
  9. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +2 -2
  10. package/corpus/core/src/client/session-replay.ts +2 -2
  11. package/corpus/core/src/extensions/html-shell.ts +17 -3
  12. package/corpus/core/src/extensions/theme.ts +39 -0
  13. package/corpus/templates/analytics/actions/provider-api-request.ts +8 -1
  14. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -1
  15. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +91 -33
  16. package/corpus/templates/analytics/changelog/2026-07-01-session-replay-recordings-upload-reliably-in-production.md +6 -0
  17. package/corpus/templates/analytics/server/handlers/session-replay.ts +46 -5
  18. package/corpus/templates/calendar/app/components/booking/TimeSlotPicker.tsx +10 -0
  19. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +15 -6
  20. package/corpus/templates/calendar/app/components/calendar/GoogleSetupWizard.tsx +22 -0
  21. package/corpus/templates/calendar/app/hooks/use-bookings.ts +6 -2
  22. package/corpus/templates/calendar/app/i18n/zh-TW.ts +4 -0
  23. package/corpus/templates/calendar/app/i18n-data.ts +107 -0
  24. package/corpus/templates/calendar/app/lib/google-oauth-setup.ts +17 -0
  25. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +9 -1
  26. package/corpus/templates/calendar/app/pages/BookingPage.tsx +40 -26
  27. package/corpus/templates/calendar/app/pages/Settings.tsx +3 -1
  28. package/corpus/templates/calendar/changelog/2026-07-01-booking-links-now-show-an-error-when-calendar-availability-c.md +6 -0
  29. package/corpus/templates/calendar/server/handlers/bookings.ts +78 -13
  30. package/corpus/templates/calendar/server/handlers/google-auth.ts +3 -2
  31. package/corpus/templates/calendar/server/lib/google-calendar.ts +16 -2
  32. package/corpus/templates/clips/changelog/2026-07-01-chrome-recording-uploads-now-serialize-chunks-to-avoid-resum.md +6 -0
  33. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  34. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -23
  35. package/corpus/templates/design/app/lib/design-system-preview.ts +69 -0
  36. package/corpus/templates/design/app/pages/DesignSystems.tsx +71 -50
  37. package/corpus/templates/design/changelog/2026-07-01-design-systems-no-longer-crash-on-responsive-tokens.md +6 -0
  38. package/dist/agent/run-store.d.ts +19 -6
  39. package/dist/agent/run-store.d.ts.map +1 -1
  40. package/dist/agent/run-store.js +163 -13
  41. package/dist/agent/run-store.js.map +1 -1
  42. package/dist/client/AssistantChat.d.ts +2 -0
  43. package/dist/client/AssistantChat.d.ts.map +1 -1
  44. package/dist/client/AssistantChat.js +36 -2
  45. package/dist/client/AssistantChat.js.map +1 -1
  46. package/dist/client/active-run-state.d.ts +3 -0
  47. package/dist/client/active-run-state.d.ts.map +1 -1
  48. package/dist/client/active-run-state.js +24 -0
  49. package/dist/client/active-run-state.js.map +1 -1
  50. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  51. package/dist/client/extensions/EmbeddedExtension.js +32 -3
  52. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  53. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  54. package/dist/client/extensions/ExtensionViewer.js +59 -2
  55. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  56. package/dist/client/extensions/ExtensionsSidebarSection.js +2 -2
  57. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  58. package/dist/client/session-replay.js +2 -2
  59. package/dist/client/session-replay.js.map +1 -1
  60. package/dist/collab/routes.d.ts +2 -2
  61. package/dist/extensions/html-shell.d.ts.map +1 -1
  62. package/dist/extensions/html-shell.js +17 -3
  63. package/dist/extensions/html-shell.js.map +1 -1
  64. package/dist/extensions/theme.d.ts +9 -0
  65. package/dist/extensions/theme.d.ts.map +1 -1
  66. package/dist/extensions/theme.js +38 -0
  67. package/dist/extensions/theme.js.map +1 -1
  68. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  69. package/dist/notifications/routes.d.ts +2 -2
  70. package/dist/observability/routes.d.ts +8 -8
  71. package/dist/progress/routes.d.ts +1 -1
  72. package/dist/resources/handlers.d.ts +3 -3
  73. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  74. package/dist/server/transcribe-voice.d.ts +1 -1
  75. package/package.json +1 -1
@@ -5,6 +5,7 @@ import {
5
5
  } from "@agent-native/core/client";
6
6
  import {
7
7
  IconExternalLink,
8
+ IconAlertTriangle,
8
9
  IconCheck,
9
10
  IconCircle,
10
11
  IconLoader2,
@@ -15,6 +16,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
15
16
  import { Button } from "@/components/ui/button";
16
17
  import { Input } from "@/components/ui/input";
17
18
  import { Label } from "@/components/ui/label";
19
+ import { shouldOfferGoogleOAuthSetup } from "@/lib/google-oauth-setup";
18
20
 
19
21
  interface EnvKeyStatus {
20
22
  key: string;
@@ -92,6 +94,26 @@ export function GoogleSetupWizard() {
92
94
  const allConfigured =
93
95
  envStatus.length > 0 && envStatus.every((k) => k.configured);
94
96
 
97
+ if (!shouldOfferGoogleOAuthSetup()) {
98
+ return (
99
+ <div className="rounded-lg border border-amber-500/25 bg-amber-500/[0.07] p-4 text-start">
100
+ <div className="flex items-start gap-3">
101
+ <div className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-amber-500/15 text-amber-300">
102
+ <IconAlertTriangle className="h-4 w-4" />
103
+ </div>
104
+ <div>
105
+ <p className="text-sm font-medium text-foreground">
106
+ {t("googleConnect.managedCredentialsUnavailable")}
107
+ </p>
108
+ <p className="mt-1 text-xs leading-relaxed text-muted-foreground">
109
+ {t("googleConnect.managedCredentialsUnavailableDescription")}
110
+ </p>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ );
115
+ }
116
+
95
117
  async function handleJsonUpload(file: File) {
96
118
  setSaving(true);
97
119
  setError(null);
@@ -30,7 +30,9 @@ export function useAvailableSlots(
30
30
  const res = await fetch(
31
31
  appApiPath(`/api/bookings/available-slots?${params}`),
32
32
  );
33
- if (!res.ok) throw new Error("Failed to fetch available slots");
33
+ if (!res.ok) {
34
+ throw new Error(await readErrorMessage(res, "Failed to fetch slots"));
35
+ }
34
36
  const data = await res.json();
35
37
  return Array.isArray(data) ? data : (data.slots ?? []);
36
38
  },
@@ -57,7 +59,9 @@ export function useAvailableDays(
57
59
  const res = await fetch(
58
60
  appApiPath(`/api/bookings/available-slots?${params}`),
59
61
  );
60
- if (!res.ok) throw new Error("Failed to fetch available days");
62
+ if (!res.ok) {
63
+ throw new Error(await readErrorMessage(res, "Failed to fetch days"));
64
+ }
61
65
  const data = await res.json();
62
66
  return Array.isArray(data?.dates) ? data.dates : [];
63
67
  },
@@ -375,6 +375,9 @@ const messages = {
375
375
  googleWarningAfterUnsafe: "完成連線。",
376
376
  accountConnectedElsewhere: "該帳戶已連線到另一個登入帳戶",
377
377
  googleConnectionFailed: "Google 連線失敗",
378
+ managedCredentialsUnavailable: "Google Calendar 暫時無法使用",
379
+ managedCredentialsUnavailableDescription:
380
+ "此部署尚未設定 Calendar 的 Google 連線。請稍後再試。",
378
381
  thatGoogleAccount: "那個 Google 帳戶",
379
382
  signOutThenSignIn: "登出,然後使用 {{account}} 登入。",
380
383
  signOut: "登出",
@@ -427,6 +430,7 @@ const messages = {
427
430
  availabilityDescription: "設定你的預約可用時間。",
428
431
  availabilitySaved: "已儲存可用性",
429
432
  availabilitySaveFailed: "無法儲存可用性",
433
+ availabilityUnavailable: "目前無法檢查此主辦人的行事曆可用性。請稍後再試。",
430
434
  availableTimes: "可用時間",
431
435
  allCount: "全部({{count}})",
432
436
  back: "返回",
@@ -398,6 +398,9 @@ const enUS = {
398
398
  googleWarningAfterUnsafe: "to finish connecting.",
399
399
  accountConnectedElsewhere: "This account is connected to another login",
400
400
  googleConnectionFailed: "Google connection failed",
401
+ managedCredentialsUnavailable: "Google Calendar is unavailable",
402
+ managedCredentialsUnavailableDescription:
403
+ "Calendar's Google connection is not configured for this deployment. Please try again later.",
401
404
  thatGoogleAccount: "that Google account",
402
405
  signOutThenSignIn: "Sign out, then sign in with {{account}}.",
403
406
  signOut: "Sign out",
@@ -452,6 +455,8 @@ const enUS = {
452
455
  availabilityDescription: "Set your available hours for bookings.",
453
456
  availabilitySaved: "Availability saved",
454
457
  availabilitySaveFailed: "Failed to save availability",
458
+ availabilityUnavailable:
459
+ "This host's calendar availability can't be checked right now. Please try again later.",
455
460
  availableTimes: "Available Times",
456
461
  allCount: "All ({{count}})",
457
462
  back: "Back",
@@ -9233,6 +9238,107 @@ const translatedCalendarRawBurnDown = {
9233
9238
  },
9234
9239
  } satisfies Partial<Record<LocaleCode, PartialMessages>>;
9235
9240
 
9241
+ const translatedCalendarAvailabilityFix = {
9242
+ "zh-CN": {
9243
+ googleConnect: {
9244
+ managedCredentialsUnavailable: "Google Calendar 暂不可用",
9245
+ managedCredentialsUnavailableDescription:
9246
+ "此部署尚未配置 Calendar 的 Google 连接。请稍后再试。",
9247
+ },
9248
+ bookingLinks: {
9249
+ availabilityUnavailable: "目前无法检查此主办人的日历可用性。请稍后再试。",
9250
+ },
9251
+ },
9252
+ "es-ES": {
9253
+ googleConnect: {
9254
+ managedCredentialsUnavailable: "Google Calendar no está disponible",
9255
+ managedCredentialsUnavailableDescription:
9256
+ "La conexión de Google de Calendar no está configurada para este despliegue. Inténtalo de nuevo más tarde.",
9257
+ },
9258
+ bookingLinks: {
9259
+ availabilityUnavailable:
9260
+ "No se puede comprobar ahora la disponibilidad del calendario de este anfitrión. Inténtalo de nuevo más tarde.",
9261
+ },
9262
+ },
9263
+ "fr-FR": {
9264
+ googleConnect: {
9265
+ managedCredentialsUnavailable: "Google Calendar est indisponible",
9266
+ managedCredentialsUnavailableDescription:
9267
+ "La connexion Google de Calendar n'est pas configurée pour ce déploiement. Réessayez plus tard.",
9268
+ },
9269
+ bookingLinks: {
9270
+ availabilityUnavailable:
9271
+ "La disponibilité du calendrier de cet hôte ne peut pas être vérifiée pour le moment. Réessayez plus tard.",
9272
+ },
9273
+ },
9274
+ "de-DE": {
9275
+ googleConnect: {
9276
+ managedCredentialsUnavailable: "Google Calendar ist nicht verfügbar",
9277
+ managedCredentialsUnavailableDescription:
9278
+ "Die Google-Verbindung von Calendar ist für diese Bereitstellung nicht konfiguriert. Bitte versuche es später erneut.",
9279
+ },
9280
+ bookingLinks: {
9281
+ availabilityUnavailable:
9282
+ "Die Kalenderverfügbarkeit dieses Gastgebers kann derzeit nicht geprüft werden. Bitte versuche es später erneut.",
9283
+ },
9284
+ },
9285
+ "ja-JP": {
9286
+ googleConnect: {
9287
+ managedCredentialsUnavailable: "Google Calendar を利用できません",
9288
+ managedCredentialsUnavailableDescription:
9289
+ "このデプロイでは Calendar の Google 接続が設定されていません。後でもう一度お試しください。",
9290
+ },
9291
+ bookingLinks: {
9292
+ availabilityUnavailable:
9293
+ "この主催者のカレンダーの空き状況を現在確認できません。後でもう一度お試しください。",
9294
+ },
9295
+ },
9296
+ "ko-KR": {
9297
+ googleConnect: {
9298
+ managedCredentialsUnavailable: "Google Calendar를 사용할 수 없습니다",
9299
+ managedCredentialsUnavailableDescription:
9300
+ "이 배포에는 Calendar의 Google 연결이 구성되어 있지 않습니다. 나중에 다시 시도하세요.",
9301
+ },
9302
+ bookingLinks: {
9303
+ availabilityUnavailable:
9304
+ "현재 이 주최자의 캘린더 가능 시간을 확인할 수 없습니다. 나중에 다시 시도하세요.",
9305
+ },
9306
+ },
9307
+ "pt-BR": {
9308
+ googleConnect: {
9309
+ managedCredentialsUnavailable: "Google Calendar indisponível",
9310
+ managedCredentialsUnavailableDescription:
9311
+ "A conexão do Calendar com o Google não está configurada para esta implantação. Tente novamente mais tarde.",
9312
+ },
9313
+ bookingLinks: {
9314
+ availabilityUnavailable:
9315
+ "Não é possível verificar a disponibilidade do calendário deste anfitrião agora. Tente novamente mais tarde.",
9316
+ },
9317
+ },
9318
+ "hi-IN": {
9319
+ googleConnect: {
9320
+ managedCredentialsUnavailable: "Google Calendar उपलब्ध नहीं है",
9321
+ managedCredentialsUnavailableDescription:
9322
+ "इस deployment के लिए Calendar का Google connection configured नहीं है। कृपया बाद में फिर कोशिश करें।",
9323
+ },
9324
+ bookingLinks: {
9325
+ availabilityUnavailable:
9326
+ "इस host की calendar availability अभी check नहीं की जा सकती। कृपया बाद में फिर कोशिश करें।",
9327
+ },
9328
+ },
9329
+ "ar-SA": {
9330
+ googleConnect: {
9331
+ managedCredentialsUnavailable: "Google Calendar غير متاح",
9332
+ managedCredentialsUnavailableDescription:
9333
+ "اتصال Google في Calendar غير مهيأ لهذا النشر. يُرجى المحاولة لاحقًا.",
9334
+ },
9335
+ bookingLinks: {
9336
+ availabilityUnavailable:
9337
+ "لا يمكن التحقق من توفر تقويم هذا المضيف الآن. يُرجى المحاولة لاحقًا.",
9338
+ },
9339
+ },
9340
+ } satisfies Partial<Record<LocaleCode, PartialMessages>>;
9341
+
9236
9342
  function applyTranslatedCalendarOverrides(
9237
9343
  translationSet: Partial<Record<LocaleCode, PartialMessages>>,
9238
9344
  ) {
@@ -9282,3 +9388,4 @@ applyTranslatedCalendarOverrides(translatedCalendarDebtTranslations);
9282
9388
  applyTranslatedCalendarOverrides(translatedCalendarRemainingRaw);
9283
9389
  applyTranslatedCalendarOverrides(translatedCalendarRawBurnDown);
9284
9390
  applyTranslatedCalendarOverrides(translatedCalendarExactCleanup);
9391
+ applyTranslatedCalendarOverrides(translatedCalendarAvailabilityFix);
@@ -0,0 +1,17 @@
1
+ function localHostname(hostname: string): boolean {
2
+ return (
3
+ hostname === "localhost" ||
4
+ hostname === "127.0.0.1" ||
5
+ hostname === "::1" ||
6
+ hostname.endsWith(".localhost") ||
7
+ hostname.endsWith(".local")
8
+ );
9
+ }
10
+
11
+ export function shouldOfferGoogleOAuthSetup(): boolean {
12
+ const env = (import.meta.env ?? {}) as Record<string, unknown>;
13
+ if (env.DEV === true) return true;
14
+ if (env.VITE_ENABLE_GOOGLE_OAUTH_SETUP === "true") return true;
15
+ if (typeof window === "undefined") return false;
16
+ return localHostname(window.location.hostname.toLowerCase());
17
+ }
@@ -135,6 +135,7 @@ const BRAND_ICON_LINK_CLASS =
135
135
  const BRAND_PILL_LINK_CLASS =
136
136
  "border-[#00B5FF]/35 bg-[#00B5FF]/10 font-semibold text-[#00B5FF] hover:border-[#00B5FF]/55 hover:bg-[#00B5FF]/15 hover:text-[#33C4FF]";
137
137
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
138
+ const BOOKING_SLOT_STEP_MINUTES = 30;
138
139
 
139
140
  type DraftLink = {
140
141
  id?: string;
@@ -1993,7 +1994,14 @@ function BookingPreview({
1993
1994
  const startMin = startH * 60 + startM;
1994
1995
  const endMin = endH * 60 + endM;
1995
1996
  const slots: string[] = [];
1996
- for (let m = startMin; m + dur <= endMin; m += dur) {
1997
+ const firstStart =
1998
+ Math.ceil(startMin / BOOKING_SLOT_STEP_MINUTES) *
1999
+ BOOKING_SLOT_STEP_MINUTES;
2000
+ for (
2001
+ let m = firstStart;
2002
+ m + dur <= endMin;
2003
+ m += BOOKING_SLOT_STEP_MINUTES
2004
+ ) {
1997
2005
  const h = Math.floor(m / 60);
1998
2006
  const mm = m % 60;
1999
2007
  const ampm = h >= 12 ? "PM" : "AM";
@@ -5,7 +5,7 @@ import {
5
5
  useT,
6
6
  } from "@agent-native/core/client";
7
7
  import type { Booking } from "@shared/api";
8
- import { IconCalendar } from "@tabler/icons-react";
8
+ import { IconAlertTriangle, IconCalendar } from "@tabler/icons-react";
9
9
  import {
10
10
  addMinutes,
11
11
  endOfMonth,
@@ -122,23 +122,26 @@ export default function BookingPage() {
122
122
  availability?.slotDurationMinutes ??
123
123
  settings?.defaultEventDuration ??
124
124
  30;
125
- const { data: slots = [], isLoading: slotsLoading } = useAvailableSlots(
126
- dateStr,
125
+ const {
126
+ data: slots = [],
127
+ isLoading: slotsLoading,
128
+ error: slotsError,
129
+ } = useAvailableSlots(dateStr, duration, slug);
130
+ const monthStart = format(startOfMonth(viewMonth), "yyyy-MM-dd");
131
+ const monthEnd = format(endOfMonth(viewMonth), "yyyy-MM-dd");
132
+ const {
133
+ data: availableDates = [],
134
+ isLoading: availableDatesLoading,
135
+ error: availableDatesError,
136
+ } = useAvailableDays(
137
+ monthStart,
138
+ monthEnd,
127
139
  duration,
128
140
  slug,
141
+ step === "date" &&
142
+ !!availability &&
143
+ (!hasDurationChoice || selectedDuration !== null),
129
144
  );
130
- const monthStart = format(startOfMonth(viewMonth), "yyyy-MM-dd");
131
- const monthEnd = format(endOfMonth(viewMonth), "yyyy-MM-dd");
132
- const { data: availableDates = [], isLoading: availableDatesLoading } =
133
- useAvailableDays(
134
- monthStart,
135
- monthEnd,
136
- duration,
137
- slug,
138
- step === "date" &&
139
- !!availability &&
140
- (!hasDurationChoice || selectedDuration !== null),
141
- );
142
145
  const createBooking = useCreateBooking();
143
146
  const selectedSlotRange = selectedSlot
144
147
  ? {
@@ -236,6 +239,7 @@ export default function BookingPage() {
236
239
  const pageTitle = bookingLink?.title || title;
237
240
  const pageDescription = bookingLink?.description || description;
238
241
  const requiredHostCount = (bookingLink?.hosts?.length ?? 0) + 1;
242
+ const availabilityErrorMessage = t("bookingLinks.availabilityUnavailable");
239
243
 
240
244
  useEffect(() => {
241
245
  if (hasDurationChoice && step === "date" && selectedDuration === null) {
@@ -404,17 +408,26 @@ export default function BookingPage() {
404
408
  <h3 className="mb-4 text-sm font-medium text-center">
405
409
  {t("bookingLinks.selectDate")}
406
410
  </h3>
407
- <div className="flex justify-center">
408
- <DatePicker
409
- selectedDate={selectedDate}
410
- onSelect={handleDateSelect}
411
- availability={availability}
412
- availableDates={availableDates}
413
- availabilityLoading={availableDatesLoading}
414
- viewMonth={viewMonth}
415
- onViewMonthChange={setViewMonth}
416
- />
417
- </div>
411
+ {availableDatesError ? (
412
+ <div className="rounded-lg border border-destructive/30 bg-destructive/[0.06] px-3 py-3 text-sm text-destructive">
413
+ <div className="flex items-start gap-2">
414
+ <IconAlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
415
+ <p>{availabilityErrorMessage}</p>
416
+ </div>
417
+ </div>
418
+ ) : (
419
+ <div className="flex justify-center">
420
+ <DatePicker
421
+ selectedDate={selectedDate}
422
+ onSelect={handleDateSelect}
423
+ availability={availability}
424
+ availableDates={availableDates}
425
+ availabilityLoading={availableDatesLoading}
426
+ viewMonth={viewMonth}
427
+ onViewMonthChange={setViewMonth}
428
+ />
429
+ </div>
430
+ )}
418
431
  </div>
419
432
  )}
420
433
 
@@ -443,6 +456,7 @@ export default function BookingPage() {
443
456
  selectedSlot={selectedSlot}
444
457
  onSelect={handleSlotSelect}
445
458
  loading={slotsLoading}
459
+ errorMessage={slotsError ? availabilityErrorMessage : undefined}
446
460
  />
447
461
  </div>
448
462
  )}
@@ -47,6 +47,7 @@ import {
47
47
  useDisconnectZoom,
48
48
  useZoomStatus,
49
49
  } from "@/hooks/use-zoom-auth";
50
+ import { shouldOfferGoogleOAuthSetup } from "@/lib/google-oauth-setup";
50
51
 
51
52
  import changelog from "../../CHANGELOG.md?raw";
52
53
 
@@ -70,6 +71,7 @@ export default function Settings() {
70
71
  const disconnectZoom = useDisconnectZoom();
71
72
  const [wantAuthUrl, setWantAuthUrl] = useState(false);
72
73
  const authUrl = useGoogleAuthUrl(wantAuthUrl);
74
+ const canOfferGoogleOAuthSetup = shouldOfferGoogleOAuthSetup();
73
75
 
74
76
  const [timezone, setTimezone] = useState("");
75
77
  const [bookingTitle, setBookingTitle] = useState("");
@@ -333,7 +335,7 @@ export default function Settings() {
333
335
  </Card>
334
336
 
335
337
  {/* Google Setup Wizard */}
336
- {!googleStatus.data?.connected && (
338
+ {!googleStatus.data?.connected && canOfferGoogleOAuthSetup && (
337
339
  <Card>
338
340
  <CardHeader>
339
341
  <CardTitle className="text-lg">
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Booking links now show an error when calendar availability cannot be checked and use 30-minute start intervals.
@@ -163,6 +163,7 @@ type ConflictItem = { start: string; end: string };
163
163
  type ConflictResult = { items: ConflictItem[]; unavailableReason?: string };
164
164
  type BookingLinkRow = typeof schema.bookingLinks.$inferSelect;
165
165
  type ConflictDb = Pick<ReturnType<typeof getDb>, "select">;
166
+ const BOOKING_SLOT_STEP_MINUTES = 30;
166
167
 
167
168
  type LocalDateTimeParts = {
168
169
  year: number;
@@ -341,6 +342,27 @@ function dateEndIso(date: string, timezone: string): string {
341
342
  ).toISOString();
342
343
  }
343
344
 
345
+ function formatAvailabilityUnavailableReason(email?: string): string {
346
+ return email
347
+ ? `Calendar availability unavailable for ${email}`
348
+ : "Calendar availability unavailable";
349
+ }
350
+
351
+ function unavailableAvailabilityResponse(event: H3Event) {
352
+ setResponseStatus(event, 503);
353
+ return {
354
+ error:
355
+ "The host's calendar availability could not be checked. Please try again later.",
356
+ code: "calendar_availability_unavailable",
357
+ };
358
+ }
359
+
360
+ function formatLocalTime(totalMinutes: number): string {
361
+ const hour = Math.floor(totalMinutes / 60);
362
+ const minute = totalMinutes % 60;
363
+ return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
364
+ }
365
+
344
366
  async function resolveAvailabilityContext({
345
367
  slug,
346
368
  db = getDb(),
@@ -397,7 +419,7 @@ async function resolveAvailabilityContext({
397
419
  };
398
420
  }
399
421
 
400
- async function getConflictItems({
422
+ export async function getConflictItems({
401
423
  db = getDb(),
402
424
  ownerEmail,
403
425
  hostEmails,
@@ -424,7 +446,18 @@ async function getConflictItems({
424
446
  );
425
447
  const freeBusyResolvedHosts = new Set<string>();
426
448
 
427
- if (await googleCalendar.isConnected(ownerEmail)) {
449
+ const ownerConnected = ownerEmail
450
+ ? await googleCalendar.isConnected(ownerEmail)
451
+ : false;
452
+
453
+ if (ownerEmail && !ownerConnected) {
454
+ return {
455
+ items: [],
456
+ unavailableReason: formatAvailabilityUnavailableReason(ownerEmail),
457
+ };
458
+ }
459
+
460
+ if (ownerConnected) {
428
461
  try {
429
462
  if (requiredHosts.length > 0) {
430
463
  const freeBusy = await googleCalendar.getFreeBusy(
@@ -434,6 +467,14 @@ async function getConflictItems({
434
467
  ownerEmail,
435
468
  timezone,
436
469
  );
470
+ if (freeBusy.errors.length > 0) {
471
+ return {
472
+ items: [],
473
+ unavailableReason: formatAvailabilityUnavailableReason(
474
+ freeBusy.errors[0]?.email || ownerEmail,
475
+ ),
476
+ };
477
+ }
437
478
  for (const [email, calendar] of Object.entries(freeBusy.calendars)) {
438
479
  const normalizedEmail = email.toLowerCase();
439
480
  if (!calendar.errors || calendar.errors.length === 0) {
@@ -448,11 +489,16 @@ async function getConflictItems({
448
489
  }
449
490
  }
450
491
 
451
- const { events: googleEvents } = await googleCalendar.listEvents(
452
- rangeStartIso,
453
- rangeEndIso,
454
- ownerEmail,
455
- );
492
+ const { events: googleEvents, errors: googleEventErrors } =
493
+ await googleCalendar.listEvents(rangeStartIso, rangeEndIso, ownerEmail);
494
+ if (googleEventErrors.length > 0) {
495
+ return {
496
+ items: [],
497
+ unavailableReason: formatAvailabilityUnavailableReason(
498
+ googleEventErrors[0]?.email || ownerEmail,
499
+ ),
500
+ };
501
+ }
456
502
  conflictItems.push(
457
503
  ...googleEvents.filter(eventBlocksAvailability).map((event) => ({
458
504
  start: event.start,
@@ -460,7 +506,10 @@ async function getConflictItems({
460
506
  })),
461
507
  );
462
508
  } catch {
463
- // Continue without Google events if API fails
509
+ return {
510
+ items: [],
511
+ unavailableReason: formatAvailabilityUnavailableReason(ownerEmail),
512
+ };
464
513
  }
465
514
  }
466
515
 
@@ -501,7 +550,7 @@ async function getConflictItems({
501
550
  return { items: conflictItems };
502
551
  }
503
552
 
504
- function generateAvailableSlotsForDate({
553
+ export function generateAvailableSlotsForDate({
505
554
  date,
506
555
  duration,
507
556
  config,
@@ -571,7 +620,17 @@ function generateAvailableSlotsForDate({
571
620
  const slotEnd = zonedTimeToUtc(date, scheduleSlot.end, timezone);
572
621
  if (slotEnd <= slotStart) continue;
573
622
 
574
- let current = new Date(slotStart);
623
+ const scheduleStartMinutes = startHour * 60 + startMin;
624
+ const firstSlotStartMinutes =
625
+ Math.ceil(scheduleStartMinutes / BOOKING_SLOT_STEP_MINUTES) *
626
+ BOOKING_SLOT_STEP_MINUTES;
627
+ if (firstSlotStartMinutes >= 24 * 60) continue;
628
+
629
+ let current = zonedTimeToUtc(
630
+ date,
631
+ formatLocalTime(firstSlotStartMinutes),
632
+ timezone,
633
+ );
575
634
 
576
635
  while (current.getTime() + slotDuration * 60 * 1000 <= slotEnd.getTime()) {
577
636
  const candidateStart = new Date(current);
@@ -598,7 +657,9 @@ function generateAvailableSlotsForDate({
598
657
  });
599
658
  }
600
659
 
601
- current = new Date(current.getTime() + slotDuration * 60 * 1000);
660
+ current = new Date(
661
+ current.getTime() + BOOKING_SLOT_STEP_MINUTES * 60 * 1000,
662
+ );
602
663
  }
603
664
  }
604
665
 
@@ -1147,7 +1208,9 @@ export const getAvailableSlots = defineEventHandler(async (event: H3Event) => {
1147
1208
  rangeEndIso: dateEndIso(rangeEnd, timezone),
1148
1209
  timezone,
1149
1210
  });
1150
- if (conflictResult.unavailableReason) return { dates: [] };
1211
+ if (conflictResult.unavailableReason) {
1212
+ return unavailableAvailabilityResponse(event);
1213
+ }
1151
1214
  const dates: string[] = [];
1152
1215
  for (
1153
1216
  let cursor = new Date(from!);
@@ -1177,7 +1240,9 @@ export const getAvailableSlots = defineEventHandler(async (event: H3Event) => {
1177
1240
  rangeEndIso: dateEndIso(date, timezone),
1178
1241
  timezone,
1179
1242
  });
1180
- if (conflictResult.unavailableReason) return { slots: [] };
1243
+ if (conflictResult.unavailableReason) {
1244
+ return unavailableAvailabilityResponse(event);
1245
+ }
1181
1246
  const availableSlots = generateAvailableSlotsForDate({
1182
1247
  date,
1183
1248
  duration,
@@ -5,6 +5,7 @@ import {
5
5
  isElectron,
6
6
  getAppUrl,
7
7
  resolveGoogleSignInCredentials,
8
+ resolveGoogleProviderCredentials,
8
9
  resolveOAuthRedirectUri,
9
10
  encodeOAuthState,
10
11
  decodeOAuthState,
@@ -74,8 +75,8 @@ async function resolveCalendarOAuthCredentials(event: H3Event) {
74
75
  clientSecret: await resolveSecret("GOOGLE_CLIENT_SECRET"),
75
76
  }),
76
77
  );
77
- if (!clientId || !clientSecret) return null;
78
- return { clientId, clientSecret };
78
+ if (clientId && clientSecret) return { clientId, clientSecret };
79
+ return resolveGoogleProviderCredentials();
79
80
  }
80
81
 
81
82
  function isCalendarConnectRequest(
@@ -10,6 +10,8 @@ import {
10
10
  getRequestOrgId,
11
11
  resolveSecret,
12
12
  runWithRequestContext,
13
+ resolveGoogleProviderCredentials,
14
+ resolveGoogleLegacyProviderCredentials,
13
15
  } from "@agent-native/core/server";
14
16
 
15
17
  import type {
@@ -95,8 +97,20 @@ async function readCredentialPair(
95
97
  resolveSecret(clientIdKey),
96
98
  resolveSecret(clientSecretKey),
97
99
  ]);
98
- if (!clientId || !clientSecret) return null;
99
- return { clientId, clientSecret };
100
+ if (clientId && clientSecret) return { clientId, clientSecret };
101
+ if (
102
+ clientIdKey === "GOOGLE_CLIENT_ID" &&
103
+ clientSecretKey === "GOOGLE_CLIENT_SECRET"
104
+ ) {
105
+ return resolveGoogleProviderCredentials();
106
+ }
107
+ if (
108
+ clientIdKey === "GOOGLE_LEGACY_CLIENT_ID" &&
109
+ clientSecretKey === "GOOGLE_LEGACY_CLIENT_SECRET"
110
+ ) {
111
+ return resolveGoogleLegacyProviderCredentials();
112
+ }
113
+ return null;
100
114
  }
101
115
 
102
116
  async function resolveGoogleProviderCredentialCandidates(
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Chrome recording uploads now serialize chunks to avoid resumable upload offset races.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "Agent-Native Clips",
4
4
  "short_name": "Clips",
5
- "version": "0.1.8",
5
+ "version": "0.1.9",
6
6
  "description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub.",
7
7
  "minimum_chrome_version": "116",
8
8
  "action": {