@agent-native/core 0.92.5 → 0.92.7

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 (148) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/template-assets.mdx +139 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +96 -6
  6. package/corpus/core/src/cli/sync-builder-starter-manifest.ts +1 -1
  7. package/corpus/core/src/client/AssistantChat.tsx +127 -15
  8. package/corpus/core/src/client/chat/tool-call-display.tsx +9 -3
  9. package/corpus/core/src/client/org/TeamPage.tsx +274 -177
  10. package/corpus/core/src/client/settings/DemoModeSection.tsx +6 -6
  11. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  12. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +49 -1
  13. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
  14. package/corpus/core/src/demo/config.ts +3 -3
  15. package/corpus/core/src/demo/redact.ts +720 -0
  16. package/corpus/core/src/extensions/url-safety.ts +13 -1
  17. package/corpus/core/src/localization/default-messages.ts +7 -0
  18. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +7 -0
  19. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +7 -0
  20. package/corpus/core/src/templates/default/app/i18n/en-US.ts +7 -0
  21. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +7 -0
  22. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +7 -0
  23. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +7 -0
  24. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +7 -0
  25. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +7 -0
  26. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +7 -0
  27. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +7 -0
  28. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +7 -0
  29. package/corpus/templates/analytics/AGENTS.md +6 -3
  30. package/corpus/templates/analytics/changelog/2026-07-09-uptime-monitors-use-scheduled-workers-in-production-serverless.md +6 -0
  31. package/corpus/templates/analytics/docs/uptime-monitoring.md +5 -0
  32. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +14 -2
  33. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +35 -0
  34. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +19 -0
  35. package/corpus/templates/assets/AGENTS.md +13 -0
  36. package/corpus/templates/assets/actions/_generation-preset-settings.ts +73 -0
  37. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +63 -0
  38. package/corpus/templates/assets/actions/create-generation-preset.ts +22 -8
  39. package/corpus/templates/assets/actions/duplicate-library.ts +49 -1
  40. package/corpus/templates/assets/actions/generate-image-batch.ts +14 -1
  41. package/corpus/templates/assets/actions/generate-image.ts +195 -26
  42. package/corpus/templates/assets/actions/import-asset-from-url.ts +323 -0
  43. package/corpus/templates/assets/actions/list-generation-presets.ts +1 -1
  44. package/corpus/templates/assets/actions/rerun-generation-run.ts +37 -0
  45. package/corpus/templates/assets/actions/update-generation-preset.ts +19 -1
  46. package/corpus/templates/assets/app/i18n/ar-SA.ts +1 -0
  47. package/corpus/templates/assets/app/i18n/de-DE.ts +1 -0
  48. package/corpus/templates/assets/app/i18n/es-ES.ts +1 -0
  49. package/corpus/templates/assets/app/i18n/fr-FR.ts +1 -0
  50. package/corpus/templates/assets/app/i18n/hi-IN.ts +1 -0
  51. package/corpus/templates/assets/app/i18n/ja-JP.ts +1 -0
  52. package/corpus/templates/assets/app/i18n/ko-KR.ts +1 -0
  53. package/corpus/templates/assets/app/i18n/pt-BR.ts +1 -0
  54. package/corpus/templates/assets/app/i18n/zh-CN.ts +1 -0
  55. package/corpus/templates/assets/app/i18n/zh-TW.ts +41 -0
  56. package/corpus/templates/assets/app/i18n-data.ts +93 -0
  57. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +66 -11
  58. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +959 -24
  59. package/corpus/templates/assets/changelog/2026-07-07-preset-skeleton-controls-fit-correctly-on-narrower-edit-page.md +6 -0
  60. package/corpus/templates/assets/changelog/2026-07-09-import-reference-images-from-url.md +6 -0
  61. package/corpus/templates/assets/changelog/2026-07-09-preset-reference-board.md +6 -0
  62. package/corpus/templates/assets/server/handlers/assets.ts +7 -27
  63. package/corpus/templates/assets/server/lib/generation.ts +83 -2
  64. package/corpus/templates/assets/server/lib/preset-chat-context.ts +17 -4
  65. package/corpus/templates/assets/server/lib/preset-references.ts +182 -0
  66. package/corpus/templates/assets/server/lib/upload-validation.ts +38 -0
  67. package/corpus/templates/assets/shared/api.ts +20 -0
  68. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
  69. package/corpus/templates/calendar/actions/create-event.ts +8 -2
  70. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +22 -7
  71. package/corpus/templates/calendar/changelog/2026-07-09-calendar-events-with-guests-now-automatically-get-a-google-m.md +6 -0
  72. package/corpus/templates/calendar/changelog/2026-07-09-new-events-now-mark-the-creator-rsvp-as-yes.md +6 -0
  73. package/corpus/templates/calendar/server/handlers/events.ts +9 -2
  74. package/corpus/templates/calendar/server/lib/event-video-conferencing.ts +58 -0
  75. package/corpus/templates/calendar/server/lib/google-calendar.ts +1 -0
  76. package/corpus/templates/clips/changelog/2026-07-09-meeting-reminder-popovers-can-now-be-dismissed-with-a-top-le.md +6 -0
  77. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +3 -1
  78. package/corpus/templates/clips/desktop/src/styles.css +13 -9
  79. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +5 -2
  80. package/dist/agent/production-agent.d.ts +34 -1
  81. package/dist/agent/production-agent.d.ts.map +1 -1
  82. package/dist/agent/production-agent.js +58 -5
  83. package/dist/agent/production-agent.js.map +1 -1
  84. package/dist/cli/sync-builder-starter-manifest.js +1 -1
  85. package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
  86. package/dist/client/AssistantChat.d.ts +3 -1
  87. package/dist/client/AssistantChat.d.ts.map +1 -1
  88. package/dist/client/AssistantChat.js +96 -15
  89. package/dist/client/AssistantChat.js.map +1 -1
  90. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  91. package/dist/client/chat/tool-call-display.js +5 -1
  92. package/dist/client/chat/tool-call-display.js.map +1 -1
  93. package/dist/client/org/TeamPage.d.ts.map +1 -1
  94. package/dist/client/org/TeamPage.js +32 -13
  95. package/dist/client/org/TeamPage.js.map +1 -1
  96. package/dist/client/settings/DemoModeSection.d.ts +3 -3
  97. package/dist/client/settings/DemoModeSection.js +4 -4
  98. package/dist/client/settings/DemoModeSection.js.map +1 -1
  99. package/dist/client/settings/SettingsPanel.js +1 -1
  100. package/dist/client/settings/SettingsPanel.js.map +1 -1
  101. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  102. package/dist/client/settings/SettingsTabsPage.js +38 -2
  103. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  104. package/dist/collab/routes.d.ts +1 -1
  105. package/dist/collab/struct-routes.d.ts +1 -1
  106. package/dist/demo/actions/toggle-demo-mode.js +2 -2
  107. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  108. package/dist/demo/config.js +3 -3
  109. package/dist/demo/config.js.map +1 -1
  110. package/dist/demo/redact.d.ts +1 -1
  111. package/dist/demo/redact.d.ts.map +1 -1
  112. package/dist/demo/redact.js +0 -0
  113. package/dist/demo/redact.js.map +1 -1
  114. package/dist/extensions/url-safety.d.ts +5 -0
  115. package/dist/extensions/url-safety.d.ts.map +1 -1
  116. package/dist/extensions/url-safety.js +10 -0
  117. package/dist/extensions/url-safety.js.map +1 -1
  118. package/dist/localization/default-messages.d.ts +7 -0
  119. package/dist/localization/default-messages.d.ts.map +1 -1
  120. package/dist/localization/default-messages.js +7 -0
  121. package/dist/localization/default-messages.js.map +1 -1
  122. package/dist/notifications/routes.d.ts +2 -2
  123. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  124. package/dist/resources/handlers.d.ts +2 -2
  125. package/dist/templates/default/app/i18n/ar-SA.ts +7 -0
  126. package/dist/templates/default/app/i18n/de-DE.ts +7 -0
  127. package/dist/templates/default/app/i18n/en-US.ts +7 -0
  128. package/dist/templates/default/app/i18n/es-ES.ts +7 -0
  129. package/dist/templates/default/app/i18n/fr-FR.ts +7 -0
  130. package/dist/templates/default/app/i18n/hi-IN.ts +7 -0
  131. package/dist/templates/default/app/i18n/ja-JP.ts +7 -0
  132. package/dist/templates/default/app/i18n/ko-KR.ts +7 -0
  133. package/dist/templates/default/app/i18n/pt-BR.ts +7 -0
  134. package/dist/templates/default/app/i18n/zh-CN.ts +7 -0
  135. package/dist/templates/default/app/i18n/zh-TW.ts +7 -0
  136. package/docs/content/template-assets.mdx +139 -0
  137. package/package.json +1 -1
  138. package/src/templates/default/app/i18n/ar-SA.ts +7 -0
  139. package/src/templates/default/app/i18n/de-DE.ts +7 -0
  140. package/src/templates/default/app/i18n/en-US.ts +7 -0
  141. package/src/templates/default/app/i18n/es-ES.ts +7 -0
  142. package/src/templates/default/app/i18n/fr-FR.ts +7 -0
  143. package/src/templates/default/app/i18n/hi-IN.ts +7 -0
  144. package/src/templates/default/app/i18n/ja-JP.ts +7 -0
  145. package/src/templates/default/app/i18n/ko-KR.ts +7 -0
  146. package/src/templates/default/app/i18n/pt-BR.ts +7 -0
  147. package/src/templates/default/app/i18n/zh-CN.ts +7 -0
  148. package/src/templates/default/app/i18n/zh-TW.ts +7 -0
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-07
4
+ ---
5
+
6
+ Preset skeleton controls fit correctly on narrower edit pages.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-09
4
+ ---
5
+
6
+ Import reference images into a brand kit directly from a URL - the agent can now ingest blog or web imagery as style, logo, or subject references.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-09
4
+ ---
5
+
6
+ Presets now have a reference board: pin annotated photos (like your usual host) and mark per-event slots to fill when generating.
@@ -18,16 +18,18 @@ import { IMAGE_CATEGORIES, MAX_ASSET_UPLOAD_FILES } from "../../shared/api.js";
18
18
  import type { ImageCategory, ImageRole } from "../../shared/api.js";
19
19
  import { getDb, schema } from "../db/index.js";
20
20
  import { createAssetFromBuffer, mediaTypeFromMime } from "../lib/assets.js";
21
- import {
22
- hasRasterImageSignature,
23
- hasVideoSignature,
24
- } from "../lib/image-processing.js";
25
21
  import { nowIso, parseJson, stringifyJson } from "../lib/json.js";
26
22
  import { getObject } from "../lib/storage.js";
27
23
  import {
28
24
  filterDuplicateAssetUploads,
29
25
  hashAssetBuffer,
30
26
  } from "../lib/upload-dedupe.js";
27
+ import {
28
+ hasAllowedSignature,
29
+ IMAGE_MIME_TYPES,
30
+ maxUploadBytesForMediaType,
31
+ VIDEO_MIME_TYPES,
32
+ } from "../lib/upload-validation.js";
31
33
 
32
34
  const MIME_BY_EXT: Record<string, string> = {
33
35
  png: "image/png",
@@ -41,20 +43,6 @@ const MIME_BY_EXT: Record<string, string> = {
41
43
  webm: "video/webm",
42
44
  };
43
45
 
44
- const IMAGE_MIME_TYPES = new Set([
45
- "image/png",
46
- "image/jpeg",
47
- "image/webp",
48
- "image/avif",
49
- ]);
50
-
51
- const VIDEO_MIME_TYPES = new Set([
52
- "video/mp4",
53
- "video/x-m4v",
54
- "video/quicktime",
55
- "video/webm",
56
- ]);
57
-
58
46
  const UPLOAD_CONCURRENCY = 3;
59
47
 
60
48
  /**
@@ -117,13 +105,6 @@ function defaultUploadTitle(
117
105
  return mediaType === "video" ? "Reference video" : "Reference image";
118
106
  }
119
107
 
120
- function hasAllowedSignature(mimeType: string, data: Uint8Array): boolean {
121
- if (IMAGE_MIME_TYPES.has(mimeType))
122
- return hasRasterImageSignature(mimeType, data);
123
- if (VIDEO_MIME_TYPES.has(mimeType)) return hasVideoSignature(mimeType, data);
124
- return false;
125
- }
126
-
127
108
  async function assertCollectionBelongsToLibrary(
128
109
  collectionId: string,
129
110
  libraryId: string,
@@ -213,8 +194,7 @@ export const uploadAssets = defineEventHandler(async (event) =>
213
194
  for (const part of files) {
214
195
  const mimeType = cleanMime(part.type, part.filename);
215
196
  const mediaType = mediaTypeFromMime(mimeType);
216
- const maxBytes =
217
- mediaType === "video" ? 250 * 1024 * 1024 : 25 * 1024 * 1024;
197
+ const maxBytes = maxUploadBytesForMediaType(mediaType);
218
198
  if (part.data.byteLength > maxBytes) {
219
199
  setResponseStatus(event, 413);
220
200
  return {
@@ -16,6 +16,7 @@ import type {
16
16
  ImageModel,
17
17
  ImageQualityTier,
18
18
  ImageSize,
19
+ PresetReferenceRole,
19
20
  StyleStrength,
20
21
  StyleBrief,
21
22
  } from "../../shared/api.js";
@@ -29,7 +30,13 @@ export interface ReferenceForGeneration {
29
30
  category?: string;
30
31
  mimeType: string;
31
32
  data: string;
32
- selectionReason?: "subject" | "source" | "anchor" | "scored" | "explicit";
33
+ selectionReason?:
34
+ | "subject"
35
+ | "source"
36
+ | "anchor"
37
+ | "scored"
38
+ | "explicit"
39
+ | `preset-ref:${string}`;
33
40
  }
34
41
 
35
42
  // Keep automatic reference context compact for Gemini. Explicit
@@ -56,6 +63,7 @@ export interface GenerateProviderInput {
56
63
  collectionId?: string | null;
57
64
  source?: "chat" | "ui" | "a2a";
58
65
  callerAppId?: string;
66
+ hasBoardReferences?: boolean;
59
67
  }
60
68
 
61
69
  export interface GenerateProviderOutput {
@@ -645,6 +653,7 @@ export async function generateWithManagedImageProvider(
645
653
  if (
646
654
  shouldFallback &&
647
655
  input.mode !== "edit" &&
656
+ !(input.hasBoardReferences && input.model.startsWith("gpt-")) &&
648
657
  (await isManualImageGenerationConfigured())
649
658
  ) {
650
659
  logGeneration("builder.fallback_to_manual", {
@@ -939,9 +948,30 @@ async function generateWithManualImageProvider(
939
948
  "Mask inpainting runs need Builder-managed image generation because the manual fallback cannot pass the image-edit mask.",
940
949
  });
941
950
  }
951
+ // Board-reference runs on gpt-* models must not reroute into the Gemini
952
+ // fallback: Gemini rejects GPT model ids, which would surface a cryptic
953
+ // provider error instead of this setup guidance.
954
+ if (input.hasBoardReferences && input.model.startsWith("gpt-")) {
955
+ throw new FeatureNotConfiguredError({
956
+ requiredCredential: "BUILDER_PRIVATE_KEY",
957
+ builderConnectUrl: "/_agent-native/builder/connect",
958
+ byokDocsUrl: "https://aistudio.google.com/apikey",
959
+ message:
960
+ "This preset attaches reference board images, which the manual OpenAI fallback cannot pass. Connect Builder.io managed generation, or switch the preset to a Gemini model with a GEMINI_API_KEY.",
961
+ });
962
+ }
942
963
  if (await isGeminiImageGenerationConfigured()) {
943
964
  return generateWithGemini(input);
944
965
  }
966
+ if (input.hasBoardReferences) {
967
+ throw new FeatureNotConfiguredError({
968
+ requiredCredential: "BUILDER_PRIVATE_KEY or GEMINI_API_KEY",
969
+ builderConnectUrl: "/_agent-native/builder/connect",
970
+ byokDocsUrl: "https://aistudio.google.com/apikey",
971
+ message:
972
+ "This preset attaches reference board images, which the manual OpenAI fallback cannot pass. Connect Builder.io managed generation, or switch the preset to a Gemini model with a GEMINI_API_KEY.",
973
+ });
974
+ }
945
975
  if (input.intent === "restyle" || input.intent === "edit") {
946
976
  throw new FeatureNotConfiguredError({
947
977
  requiredCredential: "GEMINI_API_KEY",
@@ -957,6 +987,15 @@ async function generateWithManualImageProvider(
957
987
  export async function generateWithOpenAI(
958
988
  input: GenerateProviderInput,
959
989
  ): Promise<GenerateProviderOutput> {
990
+ if (input.hasBoardReferences) {
991
+ throw new FeatureNotConfiguredError({
992
+ requiredCredential: "BUILDER_PRIVATE_KEY or GEMINI_API_KEY",
993
+ builderConnectUrl: "/_agent-native/builder/connect",
994
+ byokDocsUrl: "https://aistudio.google.com/apikey",
995
+ message:
996
+ "This preset attaches reference board images, which the manual OpenAI fallback cannot pass. Connect Builder.io managed generation, or switch the preset to a Gemini model with a GEMINI_API_KEY.",
997
+ });
998
+ }
960
999
  const startedAt = Date.now();
961
1000
  const model = openAIImageModelForInput(input.model);
962
1001
  logGeneration("openai.request", {
@@ -1211,6 +1250,12 @@ export function compilePrompt(input: {
1211
1250
  category?: ImageCategory;
1212
1251
  intent?: GenerationIntent;
1213
1252
  styleStrength?: StyleStrength;
1253
+ referenceBoard?: Array<{
1254
+ label: string;
1255
+ role: PresetReferenceRole;
1256
+ count: number;
1257
+ description?: string;
1258
+ }>;
1214
1259
  }): string {
1215
1260
  const style = input.styleBrief;
1216
1261
  const intent = input.intent ?? "generate";
@@ -1257,6 +1302,7 @@ export function compilePrompt(input: {
1257
1302
  : input.referenceCount > 0
1258
1303
  ? `Use the ${input.referenceCount} attached reference image${input.referenceCount === 1 ? "" : "s"} as visual evidence. Treat them by role: style references define visual language, logo/product references define accurate brand/product appearance, background references define fixed composition plates/layout only, mask references define editable regions only, subject/source references provide content or composition only, and prior candidates define continuity. Subject/source/background/mask references must not override the library style brief or custom instructions.`
1259
1304
  : "No reference images are attached for this run. Use the style brief and custom instructions as the source of truth.";
1305
+ const referenceBoardBlock = formatReferenceBoardBlock(input.referenceBoard);
1260
1306
 
1261
1307
  if (intent === "edit") {
1262
1308
  const editTypographyInstruction =
@@ -1270,6 +1316,7 @@ export function compilePrompt(input: {
1270
1316
  return `Edit the attached image for the "${input.libraryTitle}" asset library.
1271
1317
 
1272
1318
  ${referenceInstruction}
1319
+ ${referenceBoardBlock}
1273
1320
  ${editTypographyInstruction}
1274
1321
 
1275
1322
  Make only this change:
@@ -1282,6 +1329,7 @@ ${editConstraint}`;
1282
1329
  return `Create a brand-consistent image for the "${input.libraryTitle}" asset library.
1283
1330
 
1284
1331
  ${referenceInstruction}
1332
+ ${referenceBoardBlock}
1285
1333
 
1286
1334
  Style brief:
1287
1335
  ${style.description || "Infer the style from the references."}${palette}
@@ -1300,6 +1348,39 @@ User request:
1300
1348
  ${input.prompt}`;
1301
1349
  }
1302
1350
 
1351
+ function formatReferenceBoardBlock(
1352
+ entries?: Array<{
1353
+ label: string;
1354
+ role: PresetReferenceRole;
1355
+ count: number;
1356
+ description?: string;
1357
+ }>,
1358
+ ): string {
1359
+ const visibleEntries = (entries ?? []).filter((entry) => entry.count > 0);
1360
+ if (!visibleEntries.length) return "";
1361
+ const lines = visibleEntries.map((entry) => {
1362
+ const description =
1363
+ entry.description?.trim() || presetReferenceRoleDefault(entry.role);
1364
+ return `- "${entry.label}" (${entry.count} image(s), role: ${entry.role}): ${description}`;
1365
+ });
1366
+ return `\nPreset reference board attached to this run:\n${lines.join("\n")}`;
1367
+ }
1368
+
1369
+ function presetReferenceRoleDefault(role: PresetReferenceRole): string {
1370
+ switch (role) {
1371
+ case "subject":
1372
+ return "This is the actual person/object for this piece. Render them faithfully and recognizably; do not replace them with a generic subject.";
1373
+ case "style":
1374
+ return "Match this visual style; do not copy its content.";
1375
+ case "product":
1376
+ return "Reproduce this product accurately; do not invent variants.";
1377
+ case "background":
1378
+ return "Use as fixed backdrop/setting context only; do not treat it as a subject.";
1379
+ case "composition":
1380
+ return "Imitate this image's layout, arrangement, and framing; do not copy its content or style.";
1381
+ }
1382
+ }
1383
+
1303
1384
  function hasEmbeddedText(value?: string | null): boolean {
1304
1385
  return typeof value === "string" && !!value.trim();
1305
1386
  }
@@ -1568,7 +1649,7 @@ export function compareReferenceCandidates(
1568
1649
  );
1569
1650
  }
1570
1651
 
1571
- async function loadReferenceData(
1652
+ export async function loadReferenceData(
1572
1653
  selected: Array<{
1573
1654
  id: string;
1574
1655
  role: string;
@@ -4,6 +4,7 @@ import { inArray } from "drizzle-orm";
4
4
  import type { StyleBrief } from "../../shared/api.js";
5
5
  import { getDb, schema } from "../db/index.js";
6
6
  import { parseJson } from "./json.js";
7
+ import { normalizePresetReferences } from "./preset-references.js";
7
8
 
8
9
  const PRESET_REF_TYPE = "preset";
9
10
 
@@ -67,10 +68,12 @@ export async function preparePresetChatContext(args: {
67
68
  }
68
69
 
69
70
  function describePreset(preset: PresetRow, library?: LibraryRow): string {
70
- const settings = parseJson<{ tier?: string; includeLogo?: boolean }>(
71
- preset.settings,
72
- {},
73
- );
71
+ const settings = parseJson<{
72
+ tier?: string;
73
+ includeLogo?: boolean;
74
+ presetReferences?: unknown;
75
+ }>(preset.settings, {});
76
+ const presetReferences = normalizePresetReferences(settings.presetReferences);
74
77
  const style = library
75
78
  ? parseJson<StyleBrief>(library.styleBrief, {})
76
79
  : ({} as StyleBrief);
@@ -109,6 +112,16 @@ function describePreset(preset: PresetRow, library?: LibraryRow): string {
109
112
  if (style.doNot?.length) {
110
113
  lines.push(`- Avoid: ${style.doNot.join("; ")}.`);
111
114
  }
115
+ for (const entry of presetReferences) {
116
+ lines.push(
117
+ `- Reference "${entry.label}" (id: ${entry.id}): role ${entry.role}, ${entry.variable ? "variable" : "fixed"}${entry.required ? ", required" : ""}, ${entry.assetIds.length ? `${entry.assetIds.length} pinned image(s)` : "no images yet"}.${entry.description ? ` ${entry.description}` : ""}`,
118
+ );
119
+ }
120
+ if (presetReferences.some((entry) => entry.variable)) {
121
+ lines.push(
122
+ "- Before generating, collect images for required variable references (from the user's attachments or the library) and pass presetReferenceFills to generate-image / generate-image-batch. Fixed references attach automatically.",
123
+ );
124
+ }
112
125
  const customInstructions = library?.customInstructions?.trim();
113
126
  if (customInstructions) {
114
127
  lines.push(`- Brand custom instructions: ${customInstructions}`);
@@ -0,0 +1,182 @@
1
+ import {
2
+ PRESET_REFERENCE_ROLES,
3
+ type ImageRole,
4
+ type PresetReference,
5
+ type PresetReferenceRole,
6
+ } from "../../shared/api.js";
7
+
8
+ export const PRESET_REFERENCE_MAX_ENTRIES = 6;
9
+ export const PRESET_REFERENCE_MAX_IMAGES_PER_ENTRY = 4;
10
+ export const PRESET_REFERENCE_MAX_TOTAL_IMAGES = 8;
11
+ export const PRESET_REFERENCE_MAX_SUBJECT_IMAGES = 4;
12
+ export const PRESET_REFERENCE_TOTAL_IMAGES_ERROR =
13
+ "The reference board may attach at most 8 images total.";
14
+ export const PRESET_REFERENCE_SUBJECT_IMAGES_ERROR =
15
+ "Subject reference entries may attach at most 4 images total.";
16
+
17
+ export const PRESET_REFERENCE_ROLE_MAP: Record<PresetReferenceRole, ImageRole> =
18
+ {
19
+ subject: "subject_reference",
20
+ style: "style_reference",
21
+ product: "product_reference",
22
+ background: "background_reference",
23
+ composition: "background_reference",
24
+ };
25
+
26
+ const ROLE_SET = new Set<string>(PRESET_REFERENCE_ROLES);
27
+ const ENTRY_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
28
+
29
+ export function normalizePresetReferences(value: unknown): PresetReference[] {
30
+ if (!Array.isArray(value) || value.length === 0) return [];
31
+
32
+ const seenIds = new Set<string>();
33
+ const entries: PresetReference[] = [];
34
+ for (const item of value) {
35
+ if (!item || typeof item !== "object") continue;
36
+ const raw = item as Record<string, unknown>;
37
+ const id = typeof raw.id === "string" ? raw.id.trim() : "";
38
+ const label = typeof raw.label === "string" ? raw.label.trim() : "";
39
+ const role = raw.role;
40
+ if (
41
+ !id ||
42
+ !ENTRY_ID_RE.test(id) ||
43
+ id.length > 40 ||
44
+ seenIds.has(id) ||
45
+ !label ||
46
+ label.length > 60 ||
47
+ typeof role !== "string" ||
48
+ !ROLE_SET.has(role)
49
+ ) {
50
+ continue;
51
+ }
52
+ seenIds.add(id);
53
+ const description =
54
+ typeof raw.description === "string"
55
+ ? raw.description.trim().slice(0, 400)
56
+ : undefined;
57
+ const assetIds = Array.isArray(raw.assetIds)
58
+ ? uniqueStrings(raw.assetIds).slice(
59
+ 0,
60
+ PRESET_REFERENCE_MAX_IMAGES_PER_ENTRY,
61
+ )
62
+ : [];
63
+ entries.push({
64
+ id,
65
+ label,
66
+ role: role as PresetReferenceRole,
67
+ ...(description ? { description } : {}),
68
+ assetIds,
69
+ variable: coerceBoolean(raw.variable),
70
+ required: coerceBoolean(raw.required),
71
+ });
72
+ if (entries.length >= PRESET_REFERENCE_MAX_ENTRIES) break;
73
+ }
74
+
75
+ return trimToPinnedImageCaps(entries);
76
+ }
77
+
78
+ export type ResolvedPresetReference = {
79
+ entry: PresetReference;
80
+ assetIds: string[];
81
+ filled: boolean;
82
+ };
83
+
84
+ export function resolvePresetReferenceFills(input: {
85
+ entries: PresetReference[];
86
+ fills?: Array<{ referenceId: string; assetIds: string[] }>;
87
+ presetTitle: string;
88
+ }): ResolvedPresetReference[] {
89
+ const byId = new Map(input.entries.map((entry) => [entry.id, entry]));
90
+ const fillsById = new Map<string, string[]>();
91
+ for (const fill of input.fills ?? []) {
92
+ const entry = byId.get(fill.referenceId);
93
+ if (!entry) {
94
+ const available = input.entries.map((item) => item.id).join(", ");
95
+ throw new Error(
96
+ `Unknown reference entry "${fill.referenceId}" for preset "${input.presetTitle}". Available entries: ${available || "none"}.`,
97
+ );
98
+ }
99
+ if (!entry.variable) {
100
+ throw new Error(
101
+ `Reference entry "${entry.id}" is fixed by the preset designer. Edit the preset to change it, or mark it as variable.`,
102
+ );
103
+ }
104
+ const assetIds = uniqueStrings(fill.assetIds);
105
+ if (assetIds.length > PRESET_REFERENCE_MAX_IMAGES_PER_ENTRY) {
106
+ throw new Error(
107
+ `Reference entry "${entry.id}" accepts at most 4 images; got ${assetIds.length}.`,
108
+ );
109
+ }
110
+ fillsById.set(entry.id, assetIds);
111
+ }
112
+
113
+ const resolved = input.entries.map((entry) => {
114
+ const hasFill = fillsById.has(entry.id);
115
+ const assetIds = hasFill
116
+ ? (fillsById.get(entry.id) ?? [])
117
+ : uniqueStrings(entry.assetIds).slice(
118
+ 0,
119
+ PRESET_REFERENCE_MAX_IMAGES_PER_ENTRY,
120
+ );
121
+ if (entry.required && assetIds.length === 0) {
122
+ throw new Error(
123
+ `Preset "${input.presetTitle}" requires image(s) for reference entry "${entry.label}" (${entry.id}). Pass them via presetReferenceFills (up to 4).`,
124
+ );
125
+ }
126
+ return { entry, assetIds, filled: hasFill };
127
+ });
128
+
129
+ assertPresetReferenceImageCaps(resolved);
130
+ return resolved.filter((item) => item.assetIds.length > 0);
131
+ }
132
+
133
+ export function assertPresetReferenceImageCaps(
134
+ entries: Array<{ entry: Pick<PresetReference, "role">; assetIds: string[] }>,
135
+ ) {
136
+ const total = entries.reduce((sum, item) => sum + item.assetIds.length, 0);
137
+ if (total > PRESET_REFERENCE_MAX_TOTAL_IMAGES) {
138
+ throw new Error(PRESET_REFERENCE_TOTAL_IMAGES_ERROR);
139
+ }
140
+ const subjectTotal = entries
141
+ .filter((item) => item.entry.role === "subject")
142
+ .reduce((sum, item) => sum + item.assetIds.length, 0);
143
+ if (subjectTotal > PRESET_REFERENCE_MAX_SUBJECT_IMAGES) {
144
+ throw new Error(PRESET_REFERENCE_SUBJECT_IMAGES_ERROR);
145
+ }
146
+ }
147
+
148
+ function trimToPinnedImageCaps(entries: PresetReference[]): PresetReference[] {
149
+ const trimmed = [...entries];
150
+ while (trimmed.length && exceedsPinnedImageCaps(trimmed)) {
151
+ trimmed.pop();
152
+ }
153
+ return trimmed;
154
+ }
155
+
156
+ function exceedsPinnedImageCaps(entries: PresetReference[]) {
157
+ const total = entries.reduce((sum, entry) => sum + entry.assetIds.length, 0);
158
+ const subjectTotal = entries
159
+ .filter((entry) => entry.role === "subject")
160
+ .reduce((sum, entry) => sum + entry.assetIds.length, 0);
161
+ return (
162
+ total > PRESET_REFERENCE_MAX_TOTAL_IMAGES ||
163
+ subjectTotal > PRESET_REFERENCE_MAX_SUBJECT_IMAGES
164
+ );
165
+ }
166
+
167
+ function uniqueStrings(value: unknown[]): string[] {
168
+ const seen = new Set<string>();
169
+ const output: string[] = [];
170
+ for (const item of value) {
171
+ if (typeof item !== "string") continue;
172
+ const trimmed = item.trim();
173
+ if (!trimmed || seen.has(trimmed)) continue;
174
+ seen.add(trimmed);
175
+ output.push(trimmed);
176
+ }
177
+ return output;
178
+ }
179
+
180
+ function coerceBoolean(value: unknown): boolean {
181
+ return value === true || value === 1 || value === "true" || value === "1";
182
+ }
@@ -0,0 +1,38 @@
1
+ import type { AssetMediaType } from "../../shared/api.js";
2
+ import {
3
+ hasRasterImageSignature,
4
+ hasVideoSignature,
5
+ } from "./image-processing.js";
6
+
7
+ export const IMAGE_MIME_TYPES = new Set([
8
+ "image/png",
9
+ "image/jpeg",
10
+ "image/webp",
11
+ "image/avif",
12
+ ]);
13
+
14
+ export const VIDEO_MIME_TYPES = new Set([
15
+ "video/mp4",
16
+ "video/x-m4v",
17
+ "video/quicktime",
18
+ "video/webm",
19
+ ]);
20
+
21
+ export const MAX_IMAGE_UPLOAD_BYTES = 25 * 1024 * 1024;
22
+ export const MAX_VIDEO_UPLOAD_BYTES = 250 * 1024 * 1024;
23
+
24
+ export function maxUploadBytesForMediaType(mediaType: AssetMediaType): number {
25
+ return mediaType === "video"
26
+ ? MAX_VIDEO_UPLOAD_BYTES
27
+ : MAX_IMAGE_UPLOAD_BYTES;
28
+ }
29
+
30
+ export function hasAllowedSignature(
31
+ mimeType: string,
32
+ data: Uint8Array,
33
+ ): boolean {
34
+ if (IMAGE_MIME_TYPES.has(mimeType))
35
+ return hasRasterImageSignature(mimeType, data);
36
+ if (VIDEO_MIME_TYPES.has(mimeType)) return hasVideoSignature(mimeType, data);
37
+ return false;
38
+ }
@@ -166,6 +166,26 @@ export interface PresetSkeletonSpec {
166
166
  foreground?: PresetSkeletonForegroundLayer[];
167
167
  }
168
168
 
169
+ export const PRESET_REFERENCE_ROLES = [
170
+ "subject",
171
+ "style",
172
+ "product",
173
+ "background",
174
+ "composition",
175
+ ] as const;
176
+
177
+ export type PresetReferenceRole = (typeof PRESET_REFERENCE_ROLES)[number];
178
+
179
+ export interface PresetReference {
180
+ id: string;
181
+ label: string;
182
+ role: PresetReferenceRole;
183
+ description?: string;
184
+ assetIds: string[];
185
+ variable: boolean;
186
+ required: boolean;
187
+ }
188
+
169
189
  export interface ImageLibrarySummary {
170
190
  id: string;
171
191
  title: string;
@@ -87,6 +87,12 @@ pnpm action create-event \
87
87
  Required: `--title`, `--start`, `--end` (all ISO datetime format).
88
88
  Optional: `--description`, `--location`, `--attendees`, `--addGoogleMeet`, `--addZoom`, `--sendUpdates`.
89
89
 
90
+ When attendees are invited and no video link/provider is supplied, Calendar
91
+ automatically adds a Google Meet link by default. Pass `--addGoogleMeet=false`
92
+ only when the user explicitly wants no video conferencing. If the user provides
93
+ a Zoom/Meet/Teams link in the location or description, or asks for
94
+ `--addZoom=true`, do not add Google Meet too.
95
+
90
96
  Native Google Calendar status events are supported:
91
97
 
92
98
  ```bash
@@ -7,7 +7,10 @@ import {
7
7
  } from "@agent-native/core/server";
8
8
  import { z } from "zod";
9
9
 
10
- import { prepareZoomMeetingPatch } from "../server/lib/event-video-conferencing.js";
10
+ import {
11
+ prepareZoomMeetingPatch,
12
+ shouldAutoAddGoogleMeet,
13
+ } from "../server/lib/event-video-conferencing.js";
11
14
  import * as googleCalendar from "../server/lib/google-calendar.js";
12
15
  import type { CalendarEvent } from "../shared/api.js";
13
16
  import {
@@ -191,7 +194,10 @@ export default defineAction({
191
194
  }
192
195
 
193
196
  const result = await googleCalendar.createEvent(calEvent, {
194
- addGoogleMeet: args.addGoogleMeet,
197
+ addGoogleMeet: shouldAutoAddGoogleMeet(calEvent, {
198
+ addGoogleMeet: args.addGoogleMeet,
199
+ addZoom: args.addZoom,
200
+ }),
195
201
  sendUpdates: args.sendUpdates ?? (attendees?.length ? "all" : undefined),
196
202
  });
197
203
  if (result.id) {
@@ -122,6 +122,16 @@ function uniqueAttendees(attendees: AttendeeRecipient[]) {
122
122
  return Array.from(byEmail.values());
123
123
  }
124
124
 
125
+ function buildVideoProviderPatch(
126
+ provider: VideoProvider,
127
+ explicitChoice: boolean,
128
+ ): { addGoogleMeet?: boolean; addZoom?: boolean } {
129
+ if (provider === "google_meet")
130
+ return { addGoogleMeet: true, addZoom: false };
131
+ if (provider === "zoom") return { addGoogleMeet: false, addZoom: true };
132
+ return explicitChoice ? { addGoogleMeet: false, addZoom: false } : {};
133
+ }
134
+
125
135
  function dateTimePartsInTimezone(value: string, timezone: string) {
126
136
  const parsed = new Date(value);
127
137
  if (Number.isNaN(parsed.getTime())) return null;
@@ -240,6 +250,7 @@ export function CreateEventPopover({
240
250
  const [workingLocationType, setWorkingLocationType] =
241
251
  useState<WorkingLocationType>("customLocation");
242
252
  const [videoProvider, setVideoProvider] = useState<VideoProvider>("none");
253
+ const [videoProviderTouched, setVideoProviderTouched] = useState(false);
243
254
  const [attendees, setAttendees] = useState<AttendeeRecipient[]>([]);
244
255
  const [findTimeOpen, setFindTimeOpen] = useState(false);
245
256
  const timedOnlyStatus =
@@ -304,6 +315,9 @@ export function CreateEventPopover({
304
315
  setVideoProvider(
305
316
  draft.addGoogleMeet ? "google_meet" : draft.addZoom ? "zoom" : "none",
306
317
  );
318
+ setVideoProviderTouched(
319
+ draft.addGoogleMeet !== undefined || draft.addZoom !== undefined,
320
+ );
307
321
  setAttendees(
308
322
  uniqueAttendees(
309
323
  (draft.attendees ?? []).map((attendee) => ({
@@ -335,6 +349,7 @@ export function CreateEventPopover({
335
349
  setAttachments([createAttachmentDraft()]);
336
350
  setWorkingLocationType("customLocation");
337
351
  setVideoProvider("none");
352
+ setVideoProviderTouched(false);
338
353
  setAttendees([]);
339
354
  }, [
340
355
  open,
@@ -403,8 +418,7 @@ export function CreateEventPopover({
403
418
  ...(attendee.optional === true ? { optional: true } : {}),
404
419
  }))
405
420
  : undefined,
406
- addGoogleMeet: videoProvider === "google_meet",
407
- addZoom: videoProvider === "zoom",
421
+ ...buildVideoProviderPatch(videoProvider, videoProviderTouched),
408
422
  accountEmail: draft?.accountEmail,
409
423
  workingLocationType,
410
424
  workingLocationLabel:
@@ -449,6 +463,7 @@ export function CreateEventPopover({
449
463
  attachments,
450
464
  attendees,
451
465
  videoProvider,
466
+ videoProviderTouched,
452
467
  workingLocationType,
453
468
  timedOnlyStatus,
454
469
  onDraftChange,
@@ -647,8 +662,6 @@ export function CreateEventPopover({
647
662
  visibility: eventType === "workingLocation" ? "public" : visibility,
648
663
  ...reminderPatch,
649
664
  ...statusPatch,
650
- addGoogleMeet: videoProvider === "google_meet",
651
- addZoom: videoProvider === "zoom",
652
665
  color: colorId ? getGoogleEventColorHex(colorId) : undefined,
653
666
  colorId,
654
667
  attachments:
@@ -663,6 +676,7 @@ export function CreateEventPopover({
663
676
  ...(attendee.optional === true ? { optional: true } : {}),
664
677
  }))
665
678
  : undefined,
679
+ ...buildVideoProviderPatch(videoProvider, videoProviderTouched),
666
680
  };
667
681
 
668
682
  onOpenChange(false);
@@ -979,9 +993,10 @@ export function CreateEventPopover({
979
993
  </Label>
980
994
  <Select
981
995
  value={videoProvider}
982
- onValueChange={(value) =>
983
- setVideoProvider(value as VideoProvider)
984
- }
996
+ onValueChange={(value) => {
997
+ setVideoProvider(value as VideoProvider);
998
+ setVideoProviderTouched(true);
999
+ }}
985
1000
  >
986
1001
  <SelectTrigger
987
1002
  id="event-video-provider"
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-09
4
+ ---
5
+
6
+ Calendar events with guests now automatically get a Google Meet link when no video link is provided.