@agent-native/core 0.160.1 → 0.161.0

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 (92) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/bigquery.ts +57 -0
  3. package/corpus/templates/analytics/actions/restore-analysis-revision.ts +5 -2
  4. package/corpus/templates/analytics/actions/restore-dashboard-revision.ts +5 -2
  5. package/corpus/templates/analytics/app/components/analysis/AnalysisHistoryPanel.tsx +14 -3
  6. package/corpus/templates/analytics/app/components/dashboard/DashboardHistoryPanel.tsx +14 -3
  7. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +3 -0
  8. package/corpus/templates/analytics/app/hooks/use-analysis-revisions.ts +1 -0
  9. package/corpus/templates/analytics/app/hooks/use-dashboard-revisions.ts +16 -1
  10. package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +19 -0
  11. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +0 -1
  12. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +14 -0
  13. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +19 -0
  14. package/corpus/templates/analytics/changelog/2026-08-17-fixed-dashboard-editing-focus-history-recovery-panel-setup-f.md +6 -0
  15. package/corpus/templates/calendar/actions/create-event.ts +7 -3
  16. package/corpus/templates/calendar/actions/manage-event-draft.ts +8 -0
  17. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +64 -0
  18. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +5 -1
  19. package/corpus/templates/calendar/app/hooks/use-events.ts +1 -0
  20. package/corpus/templates/calendar/changelog/2026-08-17-booking-links-now-respect-each-required-host-s-saved-availab.md +6 -0
  21. package/corpus/templates/calendar/server/lib/booking-host-availability.ts +76 -0
  22. package/corpus/templates/calendar/server/lib/google-calendar.ts +2 -0
  23. package/corpus/templates/calendar/shared/api.ts +1 -1
  24. package/corpus/templates/clips/app/components/player/signed-out-share-actions.tsx +47 -0
  25. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +3 -1
  26. package/corpus/templates/clips/app/routes/share.$shareId.tsx +27 -19
  27. package/corpus/templates/clips/changelog/2026-08-17-shared-clips-sign-in-comment-no-audio.md +6 -0
  28. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -5
  29. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +13 -0
  30. package/corpus/templates/content/AGENTS.md +2 -0
  31. package/corpus/templates/content/actions/_blocks-field-identity.ts +17 -0
  32. package/corpus/templates/content/actions/_database-block-actions.ts +958 -0
  33. package/corpus/templates/content/actions/_database-row-mutation.ts +7 -7
  34. package/corpus/templates/content/actions/list-content-database-blocks.ts +31 -0
  35. package/corpus/templates/content/actions/mutate-content-database-block.ts +49 -0
  36. package/corpus/templates/content/changelog/2026-08-10-agents-can-safely-insert-update-upsert-delete-and-reorder-on.md +6 -0
  37. package/corpus/templates/content/docs/product/capabilities/content.object.block.md +6 -2
  38. package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +2 -1
  39. package/corpus/templates/content/parity/matrix.md +30 -30
  40. package/corpus/templates/content/parity/matrix.ts +4 -1
  41. package/corpus/templates/content/shared/blocks-field-identity.ts +114 -37
  42. package/corpus/templates/content/shared/database-block-actions.ts +82 -0
  43. package/corpus/templates/content/shared/database-block-mutations.ts +366 -0
  44. package/corpus/templates/content/shared/nfm.ts +3 -2
  45. package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +24 -0
  46. package/corpus/templates/mail/actions/get-mail-settings.ts +1 -1
  47. package/corpus/templates/mail/actions/update-mail-settings.ts +7 -3
  48. package/corpus/templates/mail/app/i18n/en-US.ts +3 -1
  49. package/corpus/templates/mail/app/pages/SettingsPage.tsx +63 -0
  50. package/corpus/templates/mail/changelog/2026-08-17-gmail-signatures-now-preserve-images-and-support-paste-or-up.md +6 -0
  51. package/corpus/templates/mail/changelog/2026-08-17-mail-now-updates-durable-drafting-preferences-without-openin.md +6 -0
  52. package/corpus/templates/mail/server/plugins/agent-chat.ts +19 -0
  53. package/corpus/templates/mail/shared/gmail-signature.ts +14 -2
  54. package/corpus/templates/mail/shared/signature.ts +1 -10
  55. package/corpus/templates/slides/actions/generate-slides-ai.ts +2 -0
  56. package/corpus/templates/slides/actions/patch-deck.ts +1 -1
  57. package/corpus/templates/slides/app/components/ThemeToggle.tsx +1 -0
  58. package/corpus/templates/slides/app/components/editor/SlideThumbnailContextMenu.tsx +59 -0
  59. package/corpus/templates/slides/app/global.css +1 -0
  60. package/corpus/templates/slides/app/lib/normalize-slide-padding.ts +40 -11
  61. package/corpus/templates/slides/changelog/2026-08-17-stable-layout-and-slide-editing.md +6 -0
  62. package/corpus/templates/slides/shared/deck-title.ts +12 -4
  63. package/dist/agent/production-agent.js +17 -4
  64. package/dist/agent/tool-approval-migrations.d.ts +1 -0
  65. package/dist/agent/tool-approval-migrations.js +10 -0
  66. package/dist/agent/tool-approval-store.d.ts +12 -0
  67. package/dist/agent/tool-approval-store.js +46 -1
  68. package/dist/agent/types.d.ts +3 -1
  69. package/dist/client/AssistantChat.js +3 -1
  70. package/dist/localization/core-messages/ar-SA.js +1 -0
  71. package/dist/localization/core-messages/de-DE.js +1 -0
  72. package/dist/localization/core-messages/en-US.d.ts +1 -0
  73. package/dist/localization/core-messages/en-US.js +1 -0
  74. package/dist/localization/core-messages/es-ES.js +1 -0
  75. package/dist/localization/core-messages/fr-FR.js +1 -0
  76. package/dist/localization/core-messages/hi-IN.js +1 -0
  77. package/dist/localization/core-messages/ja-JP.js +1 -0
  78. package/dist/localization/core-messages/ko-KR.js +1 -0
  79. package/dist/localization/core-messages/pt-BR.js +1 -0
  80. package/dist/localization/core-messages/zh-CN.js +1 -0
  81. package/dist/localization/core-messages/zh-TW.js +1 -0
  82. package/dist/localization/core-messages.d.ts +1 -0
  83. package/dist/resources/handlers.d.ts +1 -1
  84. package/dist/scripts/resources/read.js +4 -4
  85. package/dist/scripts/resources/save-memory.js +15 -2
  86. package/dist/scripts/resources/write.js +3 -3
  87. package/dist/server/agent-chat/script-entries.d.ts +1 -0
  88. package/dist/server/agent-chat/script-entries.js +8 -0
  89. package/dist/server/realtime-token.d.ts +1 -1
  90. package/dist/server/transcribe-voice.d.ts +1 -1
  91. package/docs/content/onboarding.mdx +13 -2
  92. 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: 8273
34
+ - template files: 8289
@@ -1,5 +1,6 @@
1
1
  import { AgentActionStopError, defineAction } from "@agent-native/core";
2
2
  import type { ActionRunContext } from "@agent-native/core/action";
3
+ import { getRequestRunContext } from "@agent-native/core/server";
3
4
  import { z } from "zod";
4
5
 
5
6
  import { runQuery } from "../server/lib/bigquery";
@@ -67,6 +68,59 @@ function stopForBigQueryCancellation(): never {
67
68
  });
68
69
  }
69
70
 
71
+ function normalizeSqlForRepeat(sql: string): string {
72
+ return sql.trim().replace(/\s+/g, " ");
73
+ }
74
+
75
+ function hasPriorFailedBigQueryCall(sql: string): boolean {
76
+ const runContext = getRequestRunContext();
77
+ const priorCalls = runContext?.toolCalls ?? [];
78
+ const priorResults = runContext?.toolResults ?? [];
79
+ const normalizedSql = normalizeSqlForRepeat(sql);
80
+
81
+ return priorCalls.some((call, index) => {
82
+ if (call.name !== "bigquery") return false;
83
+ const input = call.input;
84
+ if (
85
+ !input ||
86
+ typeof input !== "object" ||
87
+ normalizeSqlForRepeat(String((input as { sql?: unknown }).sql ?? "")) !==
88
+ normalizedSql
89
+ ) {
90
+ return false;
91
+ }
92
+ const result = priorResults[index];
93
+ if (!result || result.name !== "bigquery") return false;
94
+ try {
95
+ const content = JSON.parse(result.content) as { error?: unknown };
96
+ return (
97
+ content.error === "bigquery_query_failed" ||
98
+ content.error === "bigquery_query_timeout"
99
+ );
100
+ } catch {
101
+ // coercion-ok: malformed prior tool output is not evidence of a failed query.
102
+ return false;
103
+ }
104
+ });
105
+ }
106
+
107
+ function stopForRepeatedBigQueryQuery(): never {
108
+ const message =
109
+ "I stopped the repeated BigQuery request because the same SQL already failed in this turn. Change the SQL using the provider error or schema result before trying again.";
110
+ throw new AgentActionStopError(message, {
111
+ errorCode: "bigquery_repeated_query",
112
+ toolResult: JSON.stringify(
113
+ {
114
+ error: "bigquery_repeated_query",
115
+ message,
116
+ recoverable: false,
117
+ },
118
+ null,
119
+ 2,
120
+ ),
121
+ });
122
+ }
123
+
70
124
  export default defineAction({
71
125
  description:
72
126
  "Query the user-configured BigQuery data warehouse. Use this when the user asks for warehouse SQL, BigQuery, or a data-dictionary metric/table that lives in BigQuery. If the user names a provider action such as Jira or Pylon, use that provider action first and do not use BigQuery unless the user explicitly asks for a warehouse copy. For a named customer or organization ID, resolve the canonical CRM/contract identity first and verify the returned rows carry the same customer and org/root-org identifiers. For account health, distinguish completed-month usage from current partial snapshots, contract metrics from similarly named platform metrics, total distinct contracted users from DAU/WAU, and actual usage from contracted capacity. Pass standard SQL via the `sql` arg. Do NOT use `db-query` for warehouse data (it only reaches the app's own SQL database). If a query fails with a schema or SQL error (unknown dataset/table/column, syntax), treat it as a normal debugging signal: inspect the real schema with `search-bigquery-schema` (or query INFORMATION_SCHEMA), correct the query based on the error, and run it again — a few corrective attempts are expected. Surface the error to the user only if it still fails after a few attempts or is non-recoverable (missing credentials, permission, quota). Never rerun identical failing SQL, and never substitute made-up numbers for data you could not query.",
@@ -77,6 +131,9 @@ export default defineAction({
77
131
  toolCallable: true,
78
132
  grounding: true,
79
133
  run: async (args, context?: ActionRunContext) => {
134
+ if (hasPriorFailedBigQueryCall(args.sql)) {
135
+ stopForRepeatedBigQueryQuery();
136
+ }
80
137
  try {
81
138
  return await runQuery(args.sql, { signal: context?.signal });
82
139
  } catch (err) {
@@ -29,8 +29,11 @@ export default defineAction({
29
29
  resolveScope(),
30
30
  );
31
31
  if (!analysis) {
32
- throw new Error(
33
- `Analysis revision "${args.revisionId}" was not found for analysis "${args.analysisId}".`,
32
+ throw Object.assign(
33
+ new Error(
34
+ `Analysis revision "${args.revisionId}" was not found for analysis "${args.analysisId}".`,
35
+ ),
36
+ { statusCode: 404 },
34
37
  );
35
38
  }
36
39
  return {
@@ -35,8 +35,11 @@ export default defineAction({
35
35
  args.expectedUpdatedAt,
36
36
  );
37
37
  if (!restored) {
38
- throw new Error(
39
- `Dashboard revision "${args.revisionId}" was not found for dashboard "${args.dashboardId}".`,
38
+ throw Object.assign(
39
+ new Error(
40
+ `Dashboard revision "${args.revisionId}" was not found for dashboard "${args.dashboardId}".`,
41
+ ),
42
+ { statusCode: 404 },
40
43
  );
41
44
  }
42
45
  const { dashboard, snapshotRevisionId } = restored;
@@ -3,6 +3,7 @@ import { IconHistory, IconLoader2, IconRotate } from "@tabler/icons-react";
3
3
  import { useState } from "react";
4
4
  import { toast } from "sonner";
5
5
 
6
+ import { ResourceLoadError } from "@/components/ResourceLoadError";
6
7
  import {
7
8
  AlertDialog,
8
9
  AlertDialogAction,
@@ -54,9 +55,12 @@ export function AnalysisHistoryPanel({
54
55
  canRestore?: boolean;
55
56
  }) {
56
57
  const t = useT();
57
- const { data: revisions, isLoading } = useAnalysisRevisions(
58
- open ? analysisId : null,
59
- );
58
+ const {
59
+ data: revisions,
60
+ isLoading,
61
+ isError,
62
+ refetch,
63
+ } = useAnalysisRevisions(open ? analysisId : null);
60
64
  const restoreRevision = useRestoreAnalysisRevision(analysisId);
61
65
  const [pendingRestore, setPendingRestore] = useState<AnalysisRevision | null>(
62
66
  null,
@@ -98,6 +102,13 @@ export function AnalysisHistoryPanel({
98
102
  className="animate-spin text-muted-foreground"
99
103
  />
100
104
  </div>
105
+ ) : isError ? (
106
+ <ResourceLoadError
107
+ inline
108
+ message={t("sidebar.analysesLoadFailed")}
109
+ retryLabel={t("sidebar.retry")}
110
+ onRetry={() => void refetch()}
111
+ />
101
112
  ) : !revisions?.length ? (
102
113
  <div className="px-6 py-12 text-center text-xs text-muted-foreground">
103
114
  {t("analyses.historyEmpty")}
@@ -3,6 +3,7 @@ import { IconHistory, IconLoader2, IconRotate } from "@tabler/icons-react";
3
3
  import { useState } from "react";
4
4
  import { toast } from "sonner";
5
5
 
6
+ import { ResourceLoadError } from "@/components/ResourceLoadError";
6
7
  import {
7
8
  AlertDialog,
8
9
  AlertDialogAction,
@@ -58,9 +59,12 @@ export function DashboardHistoryPanel({
58
59
  onRestored,
59
60
  }: DashboardHistoryPanelProps) {
60
61
  const t = useT();
61
- const { data: revisions, isLoading } = useDashboardRevisions(
62
- open ? dashboardId : null,
63
- );
62
+ const {
63
+ data: revisions,
64
+ isLoading,
65
+ isError,
66
+ refetch,
67
+ } = useDashboardRevisions(open ? dashboardId : null);
64
68
  const restoreRevision = useRestoreDashboardRevision(dashboardId);
65
69
  const [pendingRestore, setPendingRestore] =
66
70
  useState<DashboardRevision | null>(null);
@@ -104,6 +108,13 @@ export function DashboardHistoryPanel({
104
108
  className="animate-spin text-muted-foreground"
105
109
  />
106
110
  </div>
111
+ ) : isError ? (
112
+ <ResourceLoadError
113
+ inline
114
+ message={t("sidebar.dashboardsLoadFailed")}
115
+ retryLabel={t("sidebar.retry")}
116
+ onRetry={() => void refetch()}
117
+ />
107
118
  ) : !revisions?.length ? (
108
119
  <div className="px-6 py-12 text-center text-xs text-muted-foreground">
109
120
  {t("dashboard.historyEmpty")}
@@ -150,6 +150,7 @@ import {
150
150
  type PrefetchSnapshot,
151
151
  } from "@/lib/prefetch-keys";
152
152
  import type { ResourceAccess } from "@/lib/resource-access";
153
+ import { useAutoFocusSelect } from "@/lib/use-auto-focus-select";
153
154
 
154
155
  import { resolveAskNavigationAction } from "./layout-route-policy";
155
156
  import { NewDashboardDialog } from "./NewDashboardDialog";
@@ -492,6 +493,7 @@ function SortableRow({
492
493
  useState(false);
493
494
  const [isRenaming, setIsRenaming] = useState(false);
494
495
  const [renameValue, setRenameValue] = useState(name);
496
+ const renameInputRef = useAutoFocusSelect<HTMLInputElement>(isRenaming);
495
497
 
496
498
  useEffect(() => {
497
499
  if (!isRenaming) setRenameValue(name);
@@ -650,6 +652,7 @@ function SortableRow({
650
652
  >
651
653
  {isRenaming ? (
652
654
  <input
655
+ ref={renameInputRef}
653
656
  autoFocus
654
657
  value={renameValue}
655
658
  onChange={(e) => setRenameValue(e.target.value)}
@@ -24,6 +24,7 @@ export function useAnalysisRevisions(analysisId: string | null) {
24
24
  return Array.isArray(revisions) ? revisions : [];
25
25
  },
26
26
  placeholderData: (prev: any) => prev,
27
+ retry: false,
27
28
  } as any,
28
29
  );
29
30
  }
@@ -5,7 +5,10 @@ import {
5
5
  } from "@agent-native/core/client/hooks";
6
6
  import { useQuery, useQueryClient } from "@tanstack/react-query";
7
7
 
8
- import { dashboardCacheScope } from "@/lib/prefetch-keys";
8
+ import {
9
+ dashboardCacheScope,
10
+ sqlDashboardPrefetchKey,
11
+ } from "@/lib/prefetch-keys";
9
12
 
10
13
  export interface DashboardRevision {
11
14
  id: string;
@@ -39,10 +42,13 @@ export function useDashboardRevisions(dashboardId: string | null) {
39
42
  const rows = revisions ?? data;
40
43
  return (Array.isArray(rows) ? rows : []) as DashboardRevision[];
41
44
  },
45
+ retry: false,
42
46
  });
43
47
  }
44
48
 
45
49
  export function useRestoreDashboardRevision(dashboardId: string) {
50
+ const { session } = useSession();
51
+ const scope = dashboardCacheScope(session);
46
52
  const queryClient = useQueryClient();
47
53
  return useActionMutation<
48
54
  {
@@ -67,6 +73,15 @@ export function useRestoreDashboardRevision(dashboardId: string) {
67
73
  queryClient.invalidateQueries({
68
74
  queryKey: ["data", "sql-dashboard", dashboardId],
69
75
  });
76
+ queryClient.removeQueries({
77
+ queryKey: sqlDashboardPrefetchKey(dashboardId, scope),
78
+ });
79
+ queryClient.invalidateQueries({
80
+ queryKey: ["sql-dashboards-sidebar", scope],
81
+ });
82
+ queryClient.invalidateQueries({
83
+ queryKey: ["sql-dashboards-palette", scope],
84
+ });
70
85
  },
71
86
  });
72
87
  }
@@ -0,0 +1,19 @@
1
+ import { useEffect, useRef, type RefObject } from "react";
2
+
3
+ /** Focus an input and select its existing value after the next paint. */
4
+ export function useAutoFocusSelect<T extends HTMLInputElement>(
5
+ enabled: boolean,
6
+ ): RefObject<T | null> {
7
+ const inputRef = useRef<T | null>(null);
8
+
9
+ useEffect(() => {
10
+ if (!enabled) return;
11
+ const frame = window.requestAnimationFrame(() => {
12
+ inputRef.current?.focus();
13
+ inputRef.current?.select();
14
+ });
15
+ return () => window.cancelAnimationFrame(frame);
16
+ }, [enabled]);
17
+
18
+ return inputRef;
19
+ }
@@ -756,7 +756,6 @@ function PanelEditorContent({
756
756
  <div className="grid gap-3">
757
757
  <Label>{t("panelEditor.whatToChart")}</Label>
758
758
  <PromptComposer
759
- autoFocus
760
759
  disabled={isGenerating}
761
760
  placeholder={t("panelEditor.promptPlaceholder")}
762
761
  draftScope="analytics:add-panel"
@@ -23,6 +23,20 @@ export function dashboardUpdatedAtMs(value: string | null | undefined) {
23
23
  return Number.isFinite(ms) ? ms : null;
24
24
  }
25
25
 
26
+ export function shouldShowDashboardLoadError({
27
+ dashboardId,
28
+ isError,
29
+ loaded,
30
+ hasDashboard,
31
+ }: {
32
+ dashboardId: string | null;
33
+ isError: boolean;
34
+ loaded: boolean;
35
+ hasDashboard: boolean;
36
+ }): boolean {
37
+ return Boolean(dashboardId && isError && (!loaded || !hasDashboard));
38
+ }
39
+
26
40
  export function createDashboardAdoptionHold({
27
41
  dashboardId,
28
42
  currentUpdatedAt,
@@ -71,6 +71,7 @@ import {
71
71
  useSetPageTitle,
72
72
  useSetHeaderActions,
73
73
  } from "@/components/layout/HeaderActions";
74
+ import { ResourceLoadError } from "@/components/ResourceLoadError";
74
75
  import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
75
76
  import {
76
77
  AlertDialog,
@@ -148,6 +149,7 @@ import { createDashboardSaveQueue } from "./dashboard-save-queue";
148
149
  import {
149
150
  createDashboardAdoptionHold,
150
151
  dashboardPrefetchInitialData,
152
+ shouldShowDashboardLoadError,
151
153
  shouldAdoptDashboardQueryResult,
152
154
  type DashboardAdoptionHold,
153
155
  } from "./dashboard-sync";
@@ -2070,6 +2072,23 @@ function SqlDashboardPageContent({
2070
2072
  );
2071
2073
  }
2072
2074
 
2075
+ if (
2076
+ shouldShowDashboardLoadError({
2077
+ dashboardId,
2078
+ isError: dashboardQuery.isError,
2079
+ loaded,
2080
+ hasDashboard: !!dashboard,
2081
+ })
2082
+ ) {
2083
+ return (
2084
+ <ResourceLoadError
2085
+ message={t("sidebar.dashboardsLoadFailed")}
2086
+ retryLabel={t("sidebar.retry")}
2087
+ onRetry={() => void dashboardQuery.refetch()}
2088
+ />
2089
+ );
2090
+ }
2091
+
2073
2092
  if (!loaded) {
2074
2093
  if (reportScreenshot) {
2075
2094
  return (
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-17
4
+ ---
5
+
6
+ Fixed dashboard editing focus, history recovery, panel setup focus, and repeated failed warehouse queries.
@@ -22,6 +22,7 @@ import {
22
22
  ensureOrganizerInAttendees,
23
23
  normalizeAttendees,
24
24
  normalizeCreateEventInput,
25
+ normalizeRecurrence,
25
26
  resolveOwnedAccountEmail,
26
27
  reminderMethodInput,
27
28
  reminderMinutesInput,
@@ -99,9 +100,11 @@ export default defineAction({
99
100
  "Google Calendar event color id, 1 through 11.",
100
101
  ),
101
102
  recurrence: z
102
- .array(z.string())
103
+ .union([z.string(), z.array(z.string())])
103
104
  .optional()
104
- .describe("Google recurrence rules such as RRULE:FREQ=WEEKLY;BYDAY=MO."),
105
+ .describe(
106
+ "Google recurrence rules, such as RRULE:FREQ=DAILY. Pass an empty string or [] for a non-recurring event.",
107
+ ),
105
108
  reminderMinutes: reminderMinutesInput.describe(
106
109
  "Convenience field for a single reminder in minutes before the event.",
107
110
  ),
@@ -176,6 +179,7 @@ export default defineAction({
176
179
  reminderMethod: args.reminderMethod,
177
180
  useDefaultReminders: args.remindersUseDefault,
178
181
  });
182
+ const recurrence = normalizeRecurrence(args.recurrence);
179
183
  const statusEventFields = buildStatusEventFields({
180
184
  eventType: args.eventType,
181
185
  title: normalized.title,
@@ -204,7 +208,7 @@ export default defineAction({
204
208
  attendees,
205
209
  attachments: args.attachments,
206
210
  colorId: args.colorId,
207
- recurrence: args.recurrence,
211
+ ...(recurrence && recurrence.length > 0 ? { recurrence } : {}),
208
212
  ...reminderFields,
209
213
  ...statusEventFields,
210
214
  createdAt: new Date().toISOString(),
@@ -20,6 +20,7 @@ import {
20
20
  googleColorIdInput,
21
21
  normalizeAttendees,
22
22
  normalizeCreateEventInput,
23
+ normalizeRecurrence,
23
24
  reminderMethodInput,
24
25
  reminderMinutesInput,
25
26
  remindersInput,
@@ -136,6 +137,12 @@ export default defineAction({
136
137
  reminders: remindersInput.describe(
137
138
  "Custom reminder overrides, max 5, such as [{method:'popup', minutes:10}].",
138
139
  ),
140
+ recurrence: z
141
+ .union([z.string(), z.array(z.string())])
142
+ .optional()
143
+ .describe(
144
+ "Google recurrence rules, such as RRULE:FREQ=DAILY. Pass an empty string or [] to clear recurrence.",
145
+ ),
139
146
  reminderMinutes: reminderMinutesInput.describe(
140
147
  "Convenience field for a single reminder in minutes before the event.",
141
148
  ),
@@ -239,6 +246,7 @@ export default defineAction({
239
246
  setIfPresent(draft, "transparency", args.transparency);
240
247
  setIfPresent(draft, "visibility", args.visibility);
241
248
  setIfPresent(draft, "colorId", args.colorId);
249
+ setIfPresent(draft, "recurrence", normalizeRecurrence(args.recurrence));
242
250
  setIfPresent(draft, "attachments", args.attachments);
243
251
  setIfPresent(draft, "workingLocationType", args.workingLocationType);
244
252
  setIfPresent(draft, "workingLocationLabel", args.workingLocationLabel);
@@ -71,14 +71,17 @@ import { getGoogleEventColorHex } from "@/lib/event-colors";
71
71
  import { buildEventFormInitializationKey } from "@/lib/event-form-initialization";
72
72
  import {
73
73
  attachmentsToDrafts,
74
+ buildRecurrenceRules,
74
75
  buildReminderPayload,
75
76
  createAttachmentDraft,
76
77
  createReminderDraft,
77
78
  dateTimeInTimezoneToIso,
78
79
  getEventEndValidationMessage,
80
+ getRecurrencePreset,
79
81
  remindersToDraftState,
80
82
  resolveEventTimezone,
81
83
  type AttachmentDraft,
84
+ type RecurrencePreset,
82
85
  type ReminderDraft,
83
86
  type ReminderMode,
84
87
  validateAttachmentDrafts,
@@ -265,6 +268,8 @@ export function CreateEventPopover({
265
268
  );
266
269
  const [availability, setAvailability] = useState<Availability>("opaque");
267
270
  const [visibility, setVisibility] = useState<Visibility>("default");
271
+ const [recurrencePreset, setRecurrencePreset] =
272
+ useState<RecurrencePreset>("none");
268
273
  const [timezone, setTimezone] = useState(defaultTimezone);
269
274
  const [colorId, setColorId] = useState<string | undefined>();
270
275
  const [reminderMode, setReminderMode] = useState<ReminderMode>("default");
@@ -371,6 +376,7 @@ export function CreateEventPopover({
371
376
  );
372
377
  setAvailability(draft.transparency ?? "opaque");
373
378
  setVisibility(draft.visibility ?? "default");
379
+ setRecurrencePreset(getRecurrencePreset(draft.recurrence));
374
380
  setTimezone(draftTimezone);
375
381
  setColorId(draft.colorId);
376
382
  setReminderMode(reminderState.mode);
@@ -409,6 +415,7 @@ export function CreateEventPopover({
409
415
  setDeclineMessage("Declined because I am out of office");
410
416
  setAvailability("opaque");
411
417
  setVisibility("default");
418
+ setRecurrencePreset("none");
412
419
  setTimezone(defaultTimezone);
413
420
  setColorId(undefined);
414
421
  setReminderMode("default");
@@ -471,6 +478,11 @@ export function CreateEventPopover({
471
478
  : dateTimeInTimezoneToIso(endDate, endTime, eventTimezone);
472
479
  const attachmentResult = validateAttachmentDrafts(attachments);
473
480
  const reminderPatch = buildReminderPayload(reminderMode, reminders);
481
+ const recurrence = buildRecurrenceRules(
482
+ recurrencePreset,
483
+ effectiveAllDay ? date : startValue,
484
+ eventTimezone,
485
+ );
474
486
  const nextDraft: CalendarEventDraft = {
475
487
  id: draftId,
476
488
  createdAt: draft?.createdAt,
@@ -499,6 +511,7 @@ export function CreateEventPopover({
499
511
  : "opaque",
500
512
  visibility: eventType === "workingLocation" ? "public" : visibility,
501
513
  ...reminderPatch,
514
+ recurrence: recurrence ?? undefined,
502
515
  colorId,
503
516
  attachments:
504
517
  attachmentResult.error ||
@@ -555,6 +568,7 @@ export function CreateEventPopover({
555
568
  declineMessage,
556
569
  availability,
557
570
  visibility,
571
+ recurrencePreset,
558
572
  eventTimezone,
559
573
  colorId,
560
574
  reminderMode,
@@ -753,6 +767,11 @@ export function CreateEventPopover({
753
767
  ...trailingAttendees,
754
768
  ]);
755
769
  const reminderPatch = buildReminderPayload(reminderMode, reminders);
770
+ const recurrence = buildRecurrenceRules(
771
+ recurrencePreset,
772
+ effectiveAllDay ? date : startValue,
773
+ eventTimezone,
774
+ );
756
775
  const statusPatch =
757
776
  eventType === "default"
758
777
  ? {}
@@ -788,6 +807,7 @@ export function CreateEventPopover({
788
807
  : "opaque",
789
808
  visibility: eventType === "workingLocation" ? "public" : visibility,
790
809
  ...reminderPatch,
810
+ recurrence: recurrence ?? undefined,
791
811
  ...statusPatch,
792
812
  color: colorId ? getGoogleEventColorHex(colorId) : undefined,
793
813
  colorId,
@@ -1411,6 +1431,50 @@ export function CreateEventPopover({
1411
1431
  </div>
1412
1432
  )}
1413
1433
 
1434
+ <div className="space-y-1.5">
1435
+ <Label htmlFor="event-recurrence" className="text-xs">
1436
+ {t("eventForm.repeats")}
1437
+ </Label>
1438
+ <Select
1439
+ value={recurrencePreset}
1440
+ onValueChange={(value) =>
1441
+ setRecurrencePreset(value as RecurrencePreset)
1442
+ }
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>
1476
+ </div>
1477
+
1414
1478
  {(!allDay || isOutOfOffice) && (
1415
1479
  <div className="space-y-1.5">
1416
1480
  <Label htmlFor="event-timezone" className="text-xs">
@@ -1290,7 +1290,10 @@ export function EventDetailPopover({
1290
1290
  toast.error(t("eventForm.customRepeatGoogleCalendar"));
1291
1291
  return;
1292
1292
  }
1293
- saveField({ recurrence, scope: "all" });
1293
+ saveField({
1294
+ recurrence,
1295
+ scope: isRecurringEvent ? "all" : "single",
1296
+ });
1294
1297
  },
1295
1298
  [
1296
1299
  editTimezone,
@@ -1298,6 +1301,7 @@ export function EventDetailPopover({
1298
1301
  event.startTimeZone,
1299
1302
  masterEvent.data?.start,
1300
1303
  masterEvent.data?.startTimeZone,
1304
+ isRecurringEvent,
1301
1305
  saveField,
1302
1306
  t,
1303
1307
  ],
@@ -165,6 +165,7 @@ function buildOptimisticCalendarEvent(
165
165
  : undefined,
166
166
  reminders: newData.reminders,
167
167
  remindersUseDefault: newData.remindersUseDefault,
168
+ recurrence: newData.recurrence,
168
169
  attendees: newData.attendees,
169
170
  accountEmail: newData.accountEmail,
170
171
  source: "local",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-17
4
+ ---
5
+
6
+ Booking links now respect each required host's saved availability