@agent-native/core 0.0.0-beta-20260819161541 → 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 (78) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +78 -19
  3. package/corpus/templates/analytics/changelog/2026-08-18-verify-cross-app-feature-flag-changes.md +6 -0
  4. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +196 -48
  5. package/corpus/templates/analytics/server/plugins/feature-flags.ts +5 -0
  6. package/corpus/templates/analytics/shared/feature-flags.ts +10 -0
  7. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
  8. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
  9. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
  10. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
  11. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
  12. package/corpus/templates/calendar/app/i18n-data.ts +120 -0
  13. package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
  14. package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
  15. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  16. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  17. package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  18. package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  19. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +1 -1
  20. package/corpus/templates/clips/app/routes/share.$shareId.tsx +3 -0
  21. package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +5 -0
  22. package/corpus/templates/clips/desktop/src/app.tsx +78 -16
  23. package/corpus/templates/clips/server/lib/slack-unfurls.ts +7 -13
  24. package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  25. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
  26. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  27. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
  28. package/corpus/templates/clips/shared/share-meta.ts +67 -3
  29. package/corpus/templates/content/actions/list-content-databases.ts +95 -1
  30. package/corpus/templates/content/changelog/2026-08-11-content-mutations-delegated-from-verified-slack-dms-now-use-.md +6 -0
  31. package/corpus/templates/content/shared/api.ts +11 -0
  32. package/corpus/templates/factory/AGENTS.md +1 -1
  33. package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
  34. package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
  35. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
  36. package/corpus/templates/factory/app/i18n/en-US.ts +4 -0
  37. package/corpus/templates/factory/app/routes/factory.tsx +209 -46
  38. package/corpus/templates/factory/server/connectors/slack.ts +44 -2
  39. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -7
  40. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
  41. package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
  42. package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
  43. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
  44. package/corpus/templates/mail/server/handlers/google-auth.ts +105 -2
  45. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  46. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  47. package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  48. package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  49. package/dist/a2a/artifact-response.d.ts +50 -0
  50. package/dist/a2a/artifact-response.js +266 -39
  51. package/dist/a2a/index.d.ts +2 -0
  52. package/dist/a2a/index.js +1 -0
  53. package/dist/cli/code-agent-schedules.d.ts +11 -0
  54. package/dist/cli/code-agent-schedules.js +44 -5
  55. package/dist/collab/struct-routes.d.ts +1 -1
  56. package/dist/integrations/adapters/slack.js +58 -7
  57. package/dist/jobs/background-automation-runner.js +111 -9
  58. package/dist/progress/routes.d.ts +1 -1
  59. package/dist/secrets/routes.d.ts +9 -9
  60. package/dist/server/agent-chat/action-filters-a2a.d.ts +2 -1
  61. package/dist/server/agent-chat/action-filters-a2a.js +10 -2
  62. package/dist/server/agent-chat-plugin.js +33 -11
  63. package/dist/server/auth.d.ts +17 -1
  64. package/dist/server/auth.js +369 -153
  65. package/dist/server/google-auth-plugin.js +3 -1
  66. package/dist/server/google-oauth.d.ts +10 -0
  67. package/dist/server/google-oauth.js +25 -1
  68. package/dist/server/index.d.ts +1 -1
  69. package/dist/server/index.js +1 -1
  70. package/dist/server/onboarding-html.js +79 -30
  71. package/dist/server/realtime-token.d.ts +1 -1
  72. package/docs/content/drop-in-agent.mdx +4 -4
  73. package/docs/content/faq.mdx +15 -15
  74. package/docs/content/getting-started.mdx +173 -41
  75. package/docs/content/key-concepts.mdx +10 -10
  76. package/docs/content/using-your-agent.mdx +2 -2
  77. package/docs/content/what-is-agent-native.mdx +79 -228
  78. package/package.json +2 -2
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: 8442
34
+ - template files: 8457
@@ -9,7 +9,7 @@ import {
9
9
  } from "@agent-native/core/client/hooks";
10
10
  import { useT } from "@agent-native/core/client/i18n";
11
11
  import { IconRefresh } from "@tabler/icons-react";
12
- import { useQueryClient } from "@tanstack/react-query";
12
+ import { type QueryClient, useQueryClient } from "@tanstack/react-query";
13
13
 
14
14
  import { Badge } from "@/components/ui/badge";
15
15
  import { Button } from "@/components/ui/button";
@@ -40,6 +40,51 @@ export interface FlagDirectory {
40
40
  apps?: FlagApp[];
41
41
  }
42
42
 
43
+ interface VerifiedFlagMutation {
44
+ contractVersion: 3;
45
+ status: "verified";
46
+ key: string;
47
+ rules: FeatureFlagMetadata["rules"];
48
+ enabledForCurrentUser: boolean;
49
+ }
50
+
51
+ export function reconcileVerifiedFleetMutation(
52
+ value: unknown,
53
+ targetAppId: string,
54
+ verified: VerifiedFlagMutation,
55
+ ): FlagDirectory {
56
+ const data = asDirectory(value);
57
+ return {
58
+ ...data,
59
+ apps: data.apps?.map((app) =>
60
+ appId(app) !== targetAppId
61
+ ? app
62
+ : {
63
+ ...app,
64
+ flags: app.flags?.map((flag) =>
65
+ flag.key === verified.key
66
+ ? {
67
+ ...flag,
68
+ rules: verified.rules,
69
+ enabledForCurrentUser: verified.enabledForCurrentUser,
70
+ }
71
+ : flag,
72
+ ),
73
+ },
74
+ ),
75
+ };
76
+ }
77
+
78
+ export function recoverFleetMutationFailure(
79
+ client: QueryClient,
80
+ context?: { previous: unknown; key: readonly unknown[] },
81
+ ) {
82
+ if (context) client.setQueryData(context.key, context.previous);
83
+ void client.invalidateQueries({
84
+ queryKey: ["action", "list-workspace-feature-flags"],
85
+ });
86
+ }
87
+
43
88
  function asDirectory(value: unknown): FlagDirectory {
44
89
  return value && typeof value === "object" ? (value as FlagDirectory) : {};
45
90
  }
@@ -76,9 +121,10 @@ export function FeatureFlagsFleetPanel() {
76
121
  { retry: false },
77
122
  );
78
123
  const mutation = useActionMutation<
79
- unknown,
124
+ VerifiedFlagMutation | unknown,
80
125
  { appId: string } & SetFeatureFlagInput
81
126
  >("set-workspace-feature-flag", {
127
+ skipActionQueryInvalidation: true,
82
128
  onMutate: async (input) => {
83
129
  await client.cancelQueries({
84
130
  queryKey: ["action", "list-workspace-feature-flags"],
@@ -119,13 +165,25 @@ export function FeatureFlagsFleetPanel() {
119
165
  });
120
166
  return { previous, key };
121
167
  },
122
- onError: (_error, _input, context: any) => {
123
- if (context) client.setQueryData(context.key, context.previous);
168
+ onError: (_error, _input, context: any) =>
169
+ recoverFleetMutationFailure(client, context),
170
+ onSuccess: (result, input, context: any) => {
171
+ if (
172
+ !result ||
173
+ typeof result !== "object" ||
174
+ (result as Partial<VerifiedFlagMutation>).contractVersion !== 3 ||
175
+ (result as Partial<VerifiedFlagMutation>).status !== "verified"
176
+ ) {
177
+ void client.invalidateQueries({
178
+ queryKey: ["action", "list-workspace-feature-flags"],
179
+ });
180
+ return;
181
+ }
182
+ const verified = result as VerifiedFlagMutation;
183
+ client.setQueryData(context.key, (old: unknown) =>
184
+ reconcileVerifiedFleetMutation(old, input.appId, verified),
185
+ );
124
186
  },
125
- onSettled: () =>
126
- void client.invalidateQueries({
127
- queryKey: ["action", "list-workspace-feature-flags"],
128
- }),
129
187
  });
130
188
  const directory = asDirectory(flags.data);
131
189
  const apps = directory.apps ?? [];
@@ -134,7 +192,11 @@ export function FeatureFlagsFleetPanel() {
134
192
  return (
135
193
  <StatusState
136
194
  title={t("agents.flagsUnavailable")}
137
- detail={flags.error?.message || t("agents.flagsUnreachable")}
195
+ detail={
196
+ mutation.error?.message ||
197
+ flags.error?.message ||
198
+ t("agents.flagsUnreachable")
199
+ }
138
200
  onRetry={() => void flags.refetch()}
139
201
  />
140
202
  );
@@ -179,22 +241,19 @@ export function FeatureFlagsFleetPanel() {
179
241
  </div>
180
242
  {isReady(app) ? (
181
243
  <div className="p-4">
244
+ {mutation.variables?.appId === appId(app) && mutation.error ? (
245
+ <p className="mb-3 text-sm text-destructive">
246
+ {mutation.error.message}
247
+ </p>
248
+ ) : null}
182
249
  <FeatureFlagsEditor
183
250
  flags={(app.flags ?? []).filter(
184
251
  (flag): flag is FeatureFlagMetadata =>
185
252
  !!flag.rules && typeof flag.defaultValue === "boolean",
186
253
  )}
187
254
  isPending={mutation.isPending}
188
- error={
189
- mutation.variables?.appId === appId(app)
190
- ? mutation.error
191
- : null
192
- }
193
- errorFlagKey={
194
- mutation.variables?.appId === appId(app)
195
- ? mutation.variables.key
196
- : null
197
- }
255
+ error={null}
256
+ errorFlagKey={null}
198
257
  showHeader={false}
199
258
  onMutate={(input) =>
200
259
  mutation.mutate(qualifyFleetMutation(appId(app), input))
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-18
4
+ ---
5
+
6
+ Feature flag changes now preserve actionable failures and verify the target app's persisted state.
@@ -1,9 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
 
3
3
  import { signA2AToken } from "@agent-native/core/a2a";
4
+ import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
4
5
  import { fetchOrgApps, type OrgApp } from "@agent-native/core/mcp";
5
6
  import { getOrgDomain } from "@agent-native/core/org";
6
7
 
8
+ import { VERIFIED_FLEET_FLAG_MUTATIONS } from "../../shared/feature-flags.js";
7
9
  import type { AnalyticsAdminContext } from "./db-admin-connections.js";
8
10
 
9
11
  const TARGET_TIMEOUT_MS = 3_000;
@@ -31,7 +33,16 @@ export interface WorkspaceFeatureFlagsResult {
31
33
  apps: FleetFlagApp[];
32
34
  }
33
35
 
34
- export interface WorkspaceFeatureFlagMutationResult {
36
+ export interface VerifiedWorkspaceFeatureFlagMutationResult {
37
+ contractVersion: 3;
38
+ status: "verified";
39
+ key: string;
40
+ rules: Record<string, unknown>;
41
+ scope: { orgId: string | null; orgDomain: string | null };
42
+ enabledForCurrentUser: boolean;
43
+ }
44
+
45
+ interface TargetFeatureFlagMutationResult {
35
46
  contractVersion: 2;
36
47
  status: "ready";
37
48
  key: string;
@@ -39,6 +50,10 @@ export interface WorkspaceFeatureFlagMutationResult {
39
50
  scope: { orgId: string | null; orgDomain: string | null };
40
51
  }
41
52
 
53
+ export type WorkspaceFeatureFlagMutationResult =
54
+ | TargetFeatureFlagMutationResult
55
+ | VerifiedWorkspaceFeatureFlagMutationResult;
56
+
42
57
  export interface WorkspaceFeatureFlagMutationInput {
43
58
  appId: string;
44
59
  key: string;
@@ -72,8 +87,8 @@ export function validateWorkspaceFeatureFlagMutation(
72
87
  rules?: Record<string, unknown>;
73
88
  enabledForEmail?: string;
74
89
  },
75
- ): WorkspaceFeatureFlagMutationResult {
76
- const payload = body as Partial<WorkspaceFeatureFlagMutationResult> | null;
90
+ ): TargetFeatureFlagMutationResult {
91
+ const payload = body as Partial<TargetFeatureFlagMutationResult> | null;
77
92
  const valid =
78
93
  payload?.contractVersion === 2 &&
79
94
  payload.status === "ready" &&
@@ -144,7 +159,7 @@ export function validateWorkspaceFeatureFlagMutation(
144
159
  "The target app did not enable the feature flag for the delegated operator.",
145
160
  );
146
161
  }
147
- return payload as WorkspaceFeatureFlagMutationResult;
162
+ return payload as TargetFeatureFlagMutationResult;
148
163
  }
149
164
 
150
165
  function targetOrigin(app: OrgApp): string {
@@ -173,6 +188,39 @@ async function delegatedToken(
173
188
  }
174
189
  }
175
190
 
191
+ export type WorkspaceFeatureFlagFailurePhase =
192
+ | "directory"
193
+ | "token-generation"
194
+ | "timeout"
195
+ | "network"
196
+ | "authorization"
197
+ | "unsupported-target"
198
+ | "target-action"
199
+ | "persistence"
200
+ | "verification";
201
+
202
+ const FAILURE_MESSAGES: Record<WorkspaceFeatureFlagFailurePhase, string> = {
203
+ directory: "The organization app directory could not resolve this target.",
204
+ "token-generation":
205
+ "Analytics could not create delegated feature flag authority.",
206
+ timeout: "The target app timed out before the feature flag change completed.",
207
+ network: "Analytics could not reach the target app.",
208
+ authorization: "The target app denied this delegated flag operation.",
209
+ "unsupported-target":
210
+ "The target app does not support feature flag management.",
211
+ "target-action": "The target app could not complete the feature flag action.",
212
+ persistence:
213
+ "The target app did not persist the requested feature flag rules.",
214
+ verification: "Analytics could not verify the persisted feature flag change.",
215
+ };
216
+
217
+ export class WorkspaceFeatureFlagFailure extends Error {
218
+ constructor(readonly phase: WorkspaceFeatureFlagFailurePhase) {
219
+ super(`[${phase}] ${FAILURE_MESSAGES[phase]}`);
220
+ this.name = "WorkspaceFeatureFlagFailure";
221
+ }
222
+ }
223
+
176
224
  type TargetFailureReason = "token-generation" | "timeout" | "network";
177
225
 
178
226
  class TargetCallFailure extends Error {
@@ -194,6 +242,30 @@ export function classifyWorkspaceFeatureFlagTargetFailure(
194
242
  return "network";
195
243
  }
196
244
 
245
+ function targetFailure(error: unknown): WorkspaceFeatureFlagFailure {
246
+ const reason = classifyWorkspaceFeatureFlagTargetFailure(error);
247
+ return new WorkspaceFeatureFlagFailure(reason);
248
+ }
249
+
250
+ async function resolveTargetApp(
251
+ admin: AnalyticsAdminContext,
252
+ appId: string,
253
+ ): Promise<OrgApp> {
254
+ let apps: OrgApp[];
255
+ try {
256
+ apps = await fetchOrgApps({
257
+ selfId: "analytics",
258
+ includeDirectoryApp: true,
259
+ serviceOrgId: admin.orgId,
260
+ });
261
+ } catch {
262
+ throw new WorkspaceFeatureFlagFailure("directory");
263
+ }
264
+ const app = apps.find((candidate) => candidate.id === appId);
265
+ if (!app) throw new WorkspaceFeatureFlagFailure("directory");
266
+ return app;
267
+ }
268
+
197
269
  async function callTarget(
198
270
  app: OrgApp,
199
271
  admin: AnalyticsAdminContext,
@@ -326,63 +398,139 @@ export async function setWorkspaceFeatureFlag(
326
398
  admin: AnalyticsAdminContext,
327
399
  input: WorkspaceFeatureFlagMutationInput,
328
400
  ): Promise<WorkspaceFeatureFlagMutationResult> {
329
- const apps = await fetchOrgApps({
330
- selfId: "analytics",
331
- includeDirectoryApp: true,
332
- serviceOrgId: admin.orgId,
333
- });
334
- const app = apps.find((candidate) => candidate.id === input.appId);
335
- if (!app)
336
- throw new Error(
337
- "The requested app is not available in this organization directory.",
338
- );
401
+ const app = await resolveTargetApp(admin, input.appId);
339
402
  const targetInput = workspaceFeatureFlagTargetInput(input);
340
403
  let orgDomain: string | undefined;
341
404
  try {
342
405
  orgDomain = (await getOrgDomain(admin.orgId))?.trim().toLowerCase();
343
406
  } catch {
344
- throw new TargetCallFailure("token-generation");
407
+ throw new WorkspaceFeatureFlagFailure("token-generation");
408
+ }
409
+ if (!orgDomain) throw new WorkspaceFeatureFlagFailure("token-generation");
410
+ let result: Awaited<ReturnType<typeof callTarget>>;
411
+ try {
412
+ result = await callTarget(
413
+ app,
414
+ admin,
415
+ "set-feature-flag",
416
+ targetInput,
417
+ orgDomain,
418
+ );
419
+ } catch (error) {
420
+ throw targetFailure(error);
345
421
  }
346
- if (!orgDomain) throw new TargetCallFailure("token-generation");
347
- const result = await callTarget(
348
- app,
349
- admin,
350
- "set-feature-flag",
351
- targetInput,
352
- orgDomain,
353
- );
354
422
  if (result.status === 401 || result.status === 403)
355
- throw new Error("The target app denied this delegated flag operation.");
423
+ throw new WorkspaceFeatureFlagFailure("authorization");
356
424
  if (result.status === 404 || result.status === 405)
357
- throw new Error("The target app does not support feature flag management.");
425
+ throw new WorkspaceFeatureFlagFailure("unsupported-target");
358
426
  if (result.status < 200 || result.status >= 300)
359
- throw new Error(
360
- "The target app could not persist the feature flag change.",
361
- );
362
- return validateWorkspaceFeatureFlagMutation(result.body, {
363
- key: input.key,
364
- orgDomain,
365
- allowExplicitNoOrgTarget: true,
366
- ...(input.operation === "replace-rules" && input.rules
367
- ? {
368
- rules: {
369
- mode: input.rules.mode,
370
- emails: input.rules.emails ?? [],
371
- orgIds: input.rules.orgIds ?? [],
372
- percentage: input.rules.percentage ?? 0,
373
- },
374
- }
375
- : input.operation === "off"
427
+ throw new WorkspaceFeatureFlagFailure("target-action");
428
+ let mutation: TargetFeatureFlagMutationResult;
429
+ try {
430
+ mutation = validateWorkspaceFeatureFlagMutation(result.body, {
431
+ key: input.key,
432
+ orgDomain,
433
+ allowExplicitNoOrgTarget: true,
434
+ ...(input.operation === "replace-rules" && input.rules
376
435
  ? {
377
436
  rules: {
378
- mode: "off",
379
- emails: [],
380
- orgIds: [],
381
- percentage: 0,
437
+ mode: input.rules.mode,
438
+ emails: input.rules.emails ?? [],
439
+ orgIds: input.rules.orgIds ?? [],
440
+ percentage: input.rules.percentage ?? 0,
382
441
  },
383
442
  }
384
- : { enabledForEmail: admin.userEmail }),
385
- });
443
+ : input.operation === "off"
444
+ ? {
445
+ rules: {
446
+ mode: "off",
447
+ emails: [],
448
+ orgIds: [],
449
+ percentage: 0,
450
+ },
451
+ }
452
+ : { enabledForEmail: admin.userEmail }),
453
+ });
454
+ } catch {
455
+ throw new WorkspaceFeatureFlagFailure("persistence");
456
+ }
457
+
458
+ if (
459
+ !(await isFeatureFlagEnabled(VERIFIED_FLEET_FLAG_MUTATIONS, {
460
+ userEmail: admin.userEmail,
461
+ userKey: admin.userEmail,
462
+ orgId: admin.orgId,
463
+ }))
464
+ ) {
465
+ return mutation;
466
+ }
467
+
468
+ let readBack: Awaited<ReturnType<typeof callTarget>>;
469
+ try {
470
+ readBack = await callTarget(
471
+ app,
472
+ admin,
473
+ "list-feature-flags",
474
+ {},
475
+ orgDomain,
476
+ );
477
+ } catch (error) {
478
+ throw targetFailure(error);
479
+ }
480
+ if (readBack.status === 401 || readBack.status === 403)
481
+ throw new WorkspaceFeatureFlagFailure("authorization");
482
+ if (readBack.status === 404 || readBack.status === 405)
483
+ throw new WorkspaceFeatureFlagFailure("unsupported-target");
484
+ if (readBack.status < 200 || readBack.status >= 300)
485
+ throw new WorkspaceFeatureFlagFailure("verification");
486
+ const verifiedApp = classifyWorkspaceFeatureFlagList(app, readBack);
487
+ if (verifiedApp.state === "forbidden")
488
+ throw new WorkspaceFeatureFlagFailure("authorization");
489
+ if (verifiedApp.state === "unsupported")
490
+ throw new WorkspaceFeatureFlagFailure("unsupported-target");
491
+ const verifiedFlag = verifiedApp.flags.find((flag) => flag.key === input.key);
492
+ const expectedEnabled =
493
+ input.operation === "enable-for-current-user"
494
+ ? true
495
+ : input.operation === "off"
496
+ ? false
497
+ : null;
498
+ if (
499
+ verifiedApp.state !== "ready" ||
500
+ !verifiedFlag ||
501
+ typeof verifiedFlag.enabledForCurrentUser !== "boolean" ||
502
+ (expectedEnabled !== null &&
503
+ verifiedFlag.enabledForCurrentUser !== expectedEnabled)
504
+ ) {
505
+ throw new WorkspaceFeatureFlagFailure("verification");
506
+ }
507
+ try {
508
+ validateWorkspaceFeatureFlagMutation(
509
+ {
510
+ contractVersion: 2,
511
+ status: "ready",
512
+ key: input.key,
513
+ rules: verifiedFlag.rules,
514
+ scope: mutation.scope,
515
+ },
516
+ {
517
+ key: input.key,
518
+ orgDomain,
519
+ allowExplicitNoOrgTarget: true,
520
+ rules: mutation.rules,
521
+ },
522
+ );
523
+ } catch {
524
+ throw new WorkspaceFeatureFlagFailure("verification");
525
+ }
526
+ return {
527
+ contractVersion: 3,
528
+ status: "verified",
529
+ key: mutation.key,
530
+ rules: verifiedFlag.rules as Record<string, unknown>,
531
+ scope: mutation.scope,
532
+ enabledForCurrentUser: verifiedFlag.enabledForCurrentUser,
533
+ };
386
534
  }
387
535
 
388
536
  export async function getWorkspaceFlagTarget(
@@ -0,0 +1,5 @@
1
+ import { createFeatureFlagsPlugin } from "@agent-native/core/server";
2
+
3
+ import { ANALYTICS_FEATURE_FLAGS } from "../../shared/feature-flags.js";
4
+
5
+ export default createFeatureFlagsPlugin({ flags: ANALYTICS_FEATURE_FLAGS });
@@ -0,0 +1,10 @@
1
+ import { defineFeatureFlag } from "@agent-native/core/feature-flags/registry";
2
+
3
+ export const VERIFIED_FLEET_FLAG_MUTATIONS = defineFeatureFlag({
4
+ key: "analytics.verified-fleet-flag-mutations",
5
+ displayName: "Verified fleet flag mutations",
6
+ description:
7
+ "Verify cross-app feature flag changes with an independent target read-back.",
8
+ });
9
+
10
+ export const ANALYTICS_FEATURE_FLAGS = [VERIFIED_FLEET_FLAG_MUTATIONS];
@@ -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}