@agent-native/core 0.84.9 → 0.84.12

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 (80) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +43 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/run-store.ts +29 -6
  5. package/corpus/core/src/cli/skills.ts +6 -3
  6. package/corpus/core/src/client/agent-chat-adapter.ts +31 -11
  7. package/corpus/core/src/client/require-session.tsx +20 -1
  8. package/corpus/core/src/client/sharing/ShareButton.tsx +9 -3
  9. package/corpus/core/src/collab/storage.ts +40 -5
  10. package/corpus/core/src/server/auth.ts +6 -0
  11. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +7 -6
  12. package/corpus/templates/assets/AGENTS.md +15 -0
  13. package/corpus/templates/assets/actions/_helpers.ts +3 -1
  14. package/corpus/templates/assets/actions/create-generation-preset.ts +12 -1
  15. package/corpus/templates/assets/actions/generate-asset.ts +5 -1
  16. package/corpus/templates/assets/actions/generate-image-batch.ts +6 -1
  17. package/corpus/templates/assets/actions/generate-image.ts +18 -11
  18. package/corpus/templates/assets/actions/open-asset-picker.ts +4 -2
  19. package/corpus/templates/assets/actions/update-generation-preset.ts +16 -3
  20. package/corpus/templates/assets/app/i18n/zh-TW.ts +3 -0
  21. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  22. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +42 -7
  23. package/corpus/templates/assets/app/routes/library.tsx +4 -2
  24. package/corpus/templates/assets/changelog/2026-07-01-generation-presets-can-now-composite-your-brand-s-canonical-.md +6 -0
  25. package/corpus/templates/assets/changelog/2026-07-01-tagging-a-preset-now-briefs-the-agent-on-that-preset-s-aesth.md +6 -0
  26. package/corpus/templates/assets/server/lib/preset-chat-context.ts +118 -0
  27. package/corpus/templates/assets/server/plugins/agent-chat.ts +5 -0
  28. package/corpus/templates/assets/shared/api.ts +1 -0
  29. package/corpus/templates/design/actions/export-html.ts +4 -2
  30. package/corpus/templates/design/actions/export-pdf.ts +3 -1
  31. package/corpus/templates/design/actions/export-svg.ts +4 -2
  32. package/corpus/templates/design/actions/export-zip.ts +10 -3
  33. package/corpus/templates/design/actions/get-design-snapshot.ts +41 -4
  34. package/corpus/templates/design/actions/present-design-variants.ts +21 -6
  35. package/corpus/templates/design/actions/update-file.ts +68 -17
  36. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +3 -2
  37. package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -3
  38. package/corpus/templates/design/app/components/design/MotionDock.tsx +46 -11
  39. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +80 -30
  40. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +60 -10
  41. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +2 -0
  42. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +4 -0
  43. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  44. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +2 -2
  45. package/corpus/templates/design/app/i18n-data.ts +1 -1
  46. package/corpus/templates/design/app/pages/DesignEditor.tsx +493 -109
  47. package/corpus/templates/design/app/pages/VisualEdit.tsx +4 -16
  48. package/corpus/templates/design/changelog/2026-07-01-improved-variant-pick-follow-ups-so-selected-directions-cont.md +6 -0
  49. package/corpus/templates/design/e2e/helpers.ts +2 -2
  50. package/dist/agent/run-store.d.ts.map +1 -1
  51. package/dist/agent/run-store.js +28 -6
  52. package/dist/agent/run-store.js.map +1 -1
  53. package/dist/cli/skills.d.ts.map +1 -1
  54. package/dist/cli/skills.js +6 -3
  55. package/dist/cli/skills.js.map +1 -1
  56. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  57. package/dist/client/agent-chat-adapter.js +26 -11
  58. package/dist/client/agent-chat-adapter.js.map +1 -1
  59. package/dist/client/require-session.d.ts +14 -0
  60. package/dist/client/require-session.d.ts.map +1 -1
  61. package/dist/client/require-session.js +19 -1
  62. package/dist/client/require-session.js.map +1 -1
  63. package/dist/client/sharing/ShareButton.d.ts +4 -2
  64. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  65. package/dist/client/sharing/ShareButton.js +3 -1
  66. package/dist/client/sharing/ShareButton.js.map +1 -1
  67. package/dist/collab/routes.d.ts +1 -1
  68. package/dist/collab/storage.d.ts.map +1 -1
  69. package/dist/collab/storage.js +31 -5
  70. package/dist/collab/storage.js.map +1 -1
  71. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  72. package/dist/notifications/routes.d.ts +2 -2
  73. package/dist/observability/routes.d.ts +6 -6
  74. package/dist/resources/handlers.d.ts +2 -2
  75. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  76. package/dist/server/auth.d.ts.map +1 -1
  77. package/dist/server/auth.js +7 -0
  78. package/dist/server/auth.js.map +1 -1
  79. package/dist/server/transcribe-voice.d.ts +1 -1
  80. package/package.json +1 -1
@@ -4,7 +4,7 @@ import { eq } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
- import { nowIso, stringifyJson } from "../server/lib/json.js";
7
+ import { nowIso, parseJson, stringifyJson } from "../server/lib/json.js";
8
8
  import {
9
9
  ASPECT_RATIOS,
10
10
  GENERATION_PRESET_REFERENCE_POLICIES,
@@ -29,6 +29,12 @@ export default defineAction({
29
29
  model: z.enum(IMAGE_MODELS).optional(),
30
30
  textPolicy: z.string().optional(),
31
31
  referencePolicy: z.enum(GENERATION_PRESET_REFERENCE_POLICIES).optional(),
32
+ includeLogo: z.coerce
33
+ .boolean()
34
+ .optional()
35
+ .describe(
36
+ "When true, images generated with this preset composite the library's canonical logo (no-op if the library has no canonical logo).",
37
+ ),
32
38
  settings: z.record(z.string(), z.unknown()).optional(),
33
39
  sortOrder: z.coerce.number().optional(),
34
40
  }),
@@ -67,8 +73,15 @@ export default defineAction({
67
73
  ] as const) {
68
74
  if (args[key] !== undefined) updates[key] = args[key];
69
75
  }
70
- if (args.settings !== undefined) {
71
- updates.settings = stringifyJson(args.settings);
76
+ if (args.settings !== undefined || args.includeLogo !== undefined) {
77
+ const nextSettings = {
78
+ ...parseJson<Record<string, unknown>>(preset.settings, {}),
79
+ ...(args.settings ?? {}),
80
+ };
81
+ if (args.includeLogo !== undefined) {
82
+ nextSettings.includeLogo = args.includeLogo;
83
+ }
84
+ updates.settings = stringifyJson(nextSettings);
72
85
  }
73
86
  await db
74
87
  .update(schema.assetGenerationPresets)
@@ -383,7 +383,10 @@ const messages = {
383
383
  videoPromptPlaceholder: "8 秒產品展示,慢速推近鏡頭",
384
384
  imagePromptPlaceholder: "關於冷啟動延遲文章的部落格頭圖",
385
385
  variants: "{{count}} 個變體",
386
+ logo: "徽標",
386
387
  compositeCanonicalLogo: "合成規範徽標",
388
+ compositeCanonicalLogoHint:
389
+ "為使用此預設產生的每張圖片合成此套件的規範徽標。需要已設定規範徽標。",
387
390
  openChat: "開啟聊天",
388
391
  defaultTextPolicy: "盡量不要嵌入文字。如需文字,請保持簡短且易讀。",
389
392
  generationPresetCreated: "生成預設已建立。",
@@ -127,7 +127,10 @@ const brandKitDetailEnUS = {
127
127
  "Eight-second product reveal with slow camera push-in",
128
128
  imagePromptPlaceholder: "Blog hero for an article about cold-start latency",
129
129
  variants: "{{count}} variants",
130
+ logo: "Logo",
130
131
  compositeCanonicalLogo: "Composite canonical logo",
132
+ compositeCanonicalLogoHint:
133
+ "Stamps this kit's canonical logo onto every image from this preset. Requires a canonical logo.",
131
134
  openChat: "Open chat",
132
135
  defaultTextPolicy:
133
136
  "Prefer no embedded text. Keep any requested text short and readable.",
@@ -968,7 +971,10 @@ const brandKitDetailArSA = {
968
971
  clearSearch: "مسح البحث",
969
972
  clearing: "المقاصة...",
970
973
  compiledPrompt: "موجه المترجمة",
974
+ logo: "شعار",
971
975
  compositeCanonicalLogo: "الشعار الأساسي المركب",
976
+ compositeCanonicalLogoHint:
977
+ "يضيف الشعار الأساسي لهذه المجموعة إلى كل صورة تُنشأ بهذا الإعداد المسبق. يتطلب وجود شعار أساسي.",
972
978
  couldNotAddSelectedToReferences: "لا يمكن إضافة الأصول المحددة إلى المراجع.",
973
979
  couldNotAddToReferences: "لا يمكن إضافة الأصل إلى المراجع.",
974
980
  couldNotArchiveBrandKit: "تعذر أرشفة مجموعة العلامة التجارية.",
@@ -1137,7 +1143,10 @@ const brandKitDetailDeDE = {
1137
1143
  clearSearch: "Suche löschen",
1138
1144
  clearing: "Löschen...",
1139
1145
  compiledPrompt: "Kompilierte Eingabeaufforderung",
1146
+ logo: "Logo",
1140
1147
  compositeCanonicalLogo: "Zusammengesetztes kanonisches Logo",
1148
+ compositeCanonicalLogoHint:
1149
+ "Fügt das kanonische Logo dieses Kits jedem mit dieser Vorlage erstellten Bild hinzu. Erfordert ein kanonisches Logo.",
1141
1150
  couldNotAddSelectedToReferences:
1142
1151
  "Ausgewählte Assets konnten nicht zu Referenzen hinzugefügt werden.",
1143
1152
  couldNotAddToReferences:
@@ -1312,7 +1321,10 @@ const brandKitDetailEsES = {
1312
1321
  clearSearch: "Borrar búsqueda",
1313
1322
  clearing: "Limpiando...",
1314
1323
  compiledPrompt: "Aviso compilado",
1324
+ logo: "Logotipo",
1315
1325
  compositeCanonicalLogo: "Logotipo canónico compuesto",
1326
+ compositeCanonicalLogoHint:
1327
+ "Añade el logotipo canónico de este kit a cada imagen creada con este ajuste. Requiere un logotipo canónico.",
1316
1328
  couldNotAddSelectedToReferences:
1317
1329
  "No se pudieron agregar los recursos seleccionados a las Referencias.",
1318
1330
  couldNotAddToReferences: "No se pudo agregar activo a Referencias.",
@@ -1489,7 +1501,10 @@ const brandKitDetailFrFR = {
1489
1501
  clearSearch: "Effacer la recherche",
1490
1502
  clearing: "Dégagement...",
1491
1503
  compiledPrompt: "Invite compilée",
1504
+ logo: "Logo",
1492
1505
  compositeCanonicalLogo: "Logo canonique composite",
1506
+ compositeCanonicalLogoHint:
1507
+ "Ajoute le logo canonique de ce kit à chaque image créée avec ce préréglage. Nécessite un logo canonique.",
1493
1508
  couldNotAddSelectedToReferences:
1494
1509
  "Impossible d'ajouter les éléments sélectionnés aux références.",
1495
1510
  couldNotAddToReferences: "Impossible d'ajouter un élément aux références.",
@@ -1664,7 +1679,10 @@ const brandKitDetailHiIN = {
1664
1679
  clearSearch: "स्पष्ट खोज",
1665
1680
  clearing: "समाशोधन...",
1666
1681
  compiledPrompt: "शीघ्र संकलित",
1682
+ logo: "लोगो",
1667
1683
  compositeCanonicalLogo: "समग्र विहित लोगो",
1684
+ compositeCanonicalLogoHint:
1685
+ "इस प्रीसेट से बनी हर छवि पर इस किट का विहित लोगो जोड़ता है। इसके लिए विहित लोगो आवश्यक है।",
1668
1686
  couldNotAddSelectedToReferences:
1669
1687
  "चयनित परिसंपत्तियों को संदर्भों में नहीं जोड़ा जा सका.",
1670
1688
  couldNotAddToReferences: "संदर्भों में संपत्ति नहीं जोड़ी जा सकी.",
@@ -1832,7 +1850,10 @@ const brandKitDetailJaJP = {
1832
1850
  clearSearch: "検索をクリア",
1833
1851
  clearing: "クリア中...",
1834
1852
  compiledPrompt: "コンパイルされたプロンプト",
1853
+ logo: "ロゴ",
1835
1854
  compositeCanonicalLogo: "複合正規ロゴ",
1855
+ compositeCanonicalLogoHint:
1856
+ "このプリセットで生成する各画像に、このキットの正規ロゴを合成します。正規ロゴの設定が必要です。",
1836
1857
  couldNotAddSelectedToReferences:
1837
1858
  "選択したアセットを参照に追加できませんでした。",
1838
1859
  couldNotAddToReferences: "アセットを参照に追加できませんでした。",
@@ -2005,7 +2026,10 @@ const brandKitDetailKoKR = {
2005
2026
  clearSearch: "검색 지우기",
2006
2027
  clearing: "지우는 중...",
2007
2028
  compiledPrompt: "컴파일된 프롬프트",
2029
+ logo: "로고",
2008
2030
  compositeCanonicalLogo: "복합 표준 로고",
2031
+ compositeCanonicalLogoHint:
2032
+ "이 프리셋으로 생성되는 모든 이미지에 이 키트의 표준 로고를 합성합니다. 표준 로고가 필요합니다.",
2009
2033
  couldNotAddSelectedToReferences: "선택한 자산을 참조에 추가할 수 없습니다.",
2010
2034
  couldNotAddToReferences: "참조에 자산을 추가할 수 없습니다.",
2011
2035
  couldNotArchiveBrandKit: "브랜드 키트를 보관할 수 없습니다.",
@@ -2172,7 +2196,10 @@ const brandKitDetailPtBR = {
2172
2196
  clearSearch: "Limpar pesquisa",
2173
2197
  clearing: "Limpando...",
2174
2198
  compiledPrompt: "Prompt compilado",
2199
+ logo: "Logotipo",
2175
2200
  compositeCanonicalLogo: "Logotipo canônico composto",
2201
+ compositeCanonicalLogoHint:
2202
+ "Adiciona o logotipo canônico deste kit a cada imagem criada com esta predefinição. Requer um logotipo canônico.",
2176
2203
  couldNotAddSelectedToReferences:
2177
2204
  "Não foi possível adicionar os ativos selecionados às referências.",
2178
2205
  couldNotAddToReferences: "Não foi possível adicionar recurso às referências.",
@@ -3305,7 +3332,10 @@ export const messagesByLocale = {
3305
3332
  videoPromptPlaceholder: "8 秒产品展示,慢速推近镜头",
3306
3333
  imagePromptPlaceholder: "关于冷启动延迟文章的博客头图",
3307
3334
  variants: "{{count}} 个变体",
3335
+ logo: "徽标",
3308
3336
  compositeCanonicalLogo: "合成规范徽标",
3337
+ compositeCanonicalLogoHint:
3338
+ "为使用此预设生成的每张图片合成此套件的规范徽标。需要已设置规范徽标。",
3309
3339
  openChat: "打开聊天",
3310
3340
  defaultTextPolicy: "尽量不要嵌入文字。如需文字,请保持简短且易读。",
3311
3341
  generationPresetCreated: "生成预设已创建。",
@@ -1151,12 +1151,6 @@ export function BrandKitDetailRoute({
1151
1151
  <IconFolderPlus className="mr-2 h-4 w-4 shrink-0" />
1152
1152
  {t("library.newFolder")}
1153
1153
  </DropdownMenuItem>
1154
- <ShareButton
1155
- resourceType="asset-library"
1156
- resourceId={library.id}
1157
- resourceTitle={library.title}
1158
- triggerClassName="w-full justify-start border-0 bg-transparent px-2 py-1.5 text-sm font-normal shadow-none hover:bg-accent hover:text-accent-foreground"
1159
- />
1160
1154
  <DropdownMenuSeparator />
1161
1155
  <DropdownMenuItem
1162
1156
  disabled={duplicateLibrary.isPending}
@@ -1183,15 +1177,31 @@ export function BrandKitDetailRoute({
1183
1177
  </DropdownMenuContent>
1184
1178
  </DropdownMenu>
1185
1179
  );
1180
+ const shareAction = (
1181
+ <ShareButton
1182
+ trigger="label-icon"
1183
+ resourceType="asset-library"
1184
+ resourceId={library.id}
1185
+ resourceTitle={library.title}
1186
+ triggerClassName="h-10 gap-2 px-4 border-input bg-background hover:bg-accent hover:text-accent-foreground"
1187
+ />
1188
+ );
1186
1189
  const headerActions = (
1187
1190
  <div className="flex flex-wrap items-center gap-2 sm:flex-nowrap lg:shrink-0">
1188
1191
  {uploadAction}
1192
+ {shareAction}
1189
1193
  {moreActions}
1190
1194
  </div>
1191
1195
  );
1192
1196
  const headerPrimaryActionsPortal =
1193
1197
  headerMode === "actions" && headerPrimaryActionsTarget
1194
- ? createPortal(uploadAction, headerPrimaryActionsTarget)
1198
+ ? createPortal(
1199
+ <>
1200
+ {uploadAction}
1201
+ {shareAction}
1202
+ </>,
1203
+ headerPrimaryActionsTarget,
1204
+ )
1195
1205
  : null;
1196
1206
  const headerMoreActionsPortal =
1197
1207
  headerMode === "actions" && headerMoreActionsTarget
@@ -2036,6 +2046,7 @@ function GenerationPresetsPanel({
2036
2046
  const [aspectRatio, setAspectRatio] = useState<AspectRatio>("1:1");
2037
2047
  const [promptTemplate, setPromptTemplate] = useState("");
2038
2048
  const [textPolicy, setTextPolicy] = useState(t("library.defaultTextPolicy"));
2049
+ const [includeLogo, setIncludeLogo] = useState(false);
2039
2050
 
2040
2051
  function reset() {
2041
2052
  setTitle("");
@@ -2043,6 +2054,7 @@ function GenerationPresetsPanel({
2043
2054
  setAspectRatio("1:1");
2044
2055
  setPromptTemplate("");
2045
2056
  setTextPolicy(t("library.defaultTextPolicy"));
2057
+ setIncludeLogo(false);
2046
2058
  }
2047
2059
 
2048
2060
  function submit() {
@@ -2058,6 +2070,7 @@ function GenerationPresetsPanel({
2058
2070
  promptTemplate: promptTemplate.trim() || undefined,
2059
2071
  textPolicy,
2060
2072
  referencePolicy: "auto",
2073
+ includeLogo,
2061
2074
  },
2062
2075
  {
2063
2076
  onSuccess: () => {
@@ -2101,6 +2114,9 @@ function GenerationPresetsPanel({
2101
2114
  {preset.title}
2102
2115
  </span>
2103
2116
  <Badge variant="outline">{preset.aspectRatio}</Badge>
2117
+ {preset.includeLogo ? (
2118
+ <Badge variant="secondary">{t("brandKitDetail.logo")}</Badge>
2119
+ ) : null}
2104
2120
  </div>
2105
2121
  <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
2106
2122
  {preset.textPolicy || preset.description || preset.category}
@@ -2251,6 +2267,25 @@ function GenerationPresetsPanel({
2251
2267
  onChange={(event) => setTextPolicy(event.target.value)}
2252
2268
  />
2253
2269
  </div>
2270
+ <label
2271
+ htmlFor="preset-include-logo"
2272
+ className="flex items-start gap-3 rounded-md border border-border p-3"
2273
+ >
2274
+ <Checkbox
2275
+ id="preset-include-logo"
2276
+ checked={includeLogo}
2277
+ onCheckedChange={(checked) => setIncludeLogo(checked === true)}
2278
+ className="mt-0.5"
2279
+ />
2280
+ <span className="grid gap-1">
2281
+ <span className="text-sm font-medium leading-none">
2282
+ {t("brandKitDetail.compositeCanonicalLogo")}
2283
+ </span>
2284
+ <span className="text-xs text-muted-foreground">
2285
+ {t("brandKitDetail.compositeCanonicalLogoHint")}
2286
+ </span>
2287
+ </span>
2288
+ </label>
2254
2289
  </div>
2255
2290
  <DialogFooter>
2256
2291
  <Button variant="outline" onClick={() => setOpen(false)}>
@@ -2439,7 +2439,8 @@ export function AssetPickerSurface() {
2439
2439
  presetTitle: selectedPreset?.title ?? null,
2440
2440
  tier: hostConfig.tier,
2441
2441
  styleStrength: hostConfig.styleStrength ?? "balanced",
2442
- includeLogo: hostConfig.includeLogo ?? false,
2442
+ // Omit when unset so the selected preset's logo setting drives it.
2443
+ includeLogo: hostConfig.includeLogo,
2443
2444
  }),
2444
2445
  submit: true,
2445
2446
  openSidebar: true,
@@ -2463,7 +2464,8 @@ export function AssetPickerSurface() {
2463
2464
  })),
2464
2465
  tier: hostConfig.tier,
2465
2466
  styleStrength: hostConfig.styleStrength ?? "balanced",
2466
- includeLogo: hostConfig.includeLogo ?? false,
2467
+ // Omit when unset so the selected preset's logo setting drives it.
2468
+ includeLogo: hostConfig.includeLogo,
2467
2469
  source: "ui",
2468
2470
  callerAppId: hostConfig.callerAppId,
2469
2471
  } as any);
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Generation presets can now composite your brand's canonical logo onto every image made with them
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Tagging a preset now briefs the agent on that preset's aesthetics and philosophy before it generates, for more on-brand results
@@ -0,0 +1,118 @@
1
+ import type { AgentChatReference } from "@agent-native/core/server";
2
+ import { inArray } from "drizzle-orm";
3
+
4
+ import type { StyleBrief } from "../../shared/api.js";
5
+ import { getDb, schema } from "../db/index.js";
6
+ import { parseJson } from "./json.js";
7
+
8
+ const PRESET_REF_TYPE = "preset";
9
+
10
+ type PresetRow = typeof schema.assetGenerationPresets.$inferSelect;
11
+ type LibraryRow = typeof schema.assetLibraries.$inferSelect;
12
+
13
+ /**
14
+ * When a user tags one or more generation presets with an `@preset` mention,
15
+ * embed each preset's aesthetics and creative philosophy into the model-facing
16
+ * message so the agent internalizes the brief before it generates. The user's
17
+ * visible message is untouched — only the message the model reads is augmented.
18
+ */
19
+ export async function preparePresetChatContext(args: {
20
+ message: string;
21
+ references: AgentChatReference[];
22
+ }): Promise<{ message?: string } | void> {
23
+ const presetIds = Array.from(
24
+ new Set(
25
+ (args.references ?? [])
26
+ .filter((ref) => ref.refType === PRESET_REF_TYPE && ref.refId)
27
+ .map((ref) => ref.refId as string),
28
+ ),
29
+ );
30
+ if (!presetIds.length) return;
31
+
32
+ const db = getDb();
33
+ const presets = (await db
34
+ .select()
35
+ .from(schema.assetGenerationPresets)
36
+ .where(
37
+ inArray(schema.assetGenerationPresets.id, presetIds),
38
+ )) as PresetRow[];
39
+ if (!presets.length) return;
40
+
41
+ const libraryIds = Array.from(
42
+ new Set(presets.map((preset) => preset.libraryId)),
43
+ );
44
+ const libraries = (await db
45
+ .select()
46
+ .from(schema.assetLibraries)
47
+ .where(inArray(schema.assetLibraries.id, libraryIds))) as LibraryRow[];
48
+ const libraryById = new Map(
49
+ libraries.map((library) => [library.id, library]),
50
+ );
51
+
52
+ const blocks = presets.map((preset) =>
53
+ describePreset(preset, libraryById.get(preset.libraryId)),
54
+ );
55
+
56
+ const context = [
57
+ "<tagged-generation-presets>",
58
+ "The user tagged the generation preset(s) below. Before generating anything, study each preset's aesthetics and creative philosophy and let it drive your composition, mood, lighting, styling, and subject choices. Treat the preset as the creative brief, not just a set of output dimensions.",
59
+ "",
60
+ blocks.join("\n\n"),
61
+ "",
62
+ "When you call generate-image or generate-image-batch, pass the matching presetId so its saved format, model, tier, logo setting, and prompt template apply automatically — do not restate aspect ratio, size, model, or tier as ad-hoc args. Keep your own prompt focused on the specific subject the user asked for, expressed through the preset's philosophy above.",
63
+ "</tagged-generation-presets>",
64
+ ].join("\n");
65
+
66
+ return { message: `${args.message}\n\n${context}` };
67
+ }
68
+
69
+ function describePreset(preset: PresetRow, library?: LibraryRow): string {
70
+ const settings = parseJson<{ tier?: string; includeLogo?: boolean }>(
71
+ preset.settings,
72
+ {},
73
+ );
74
+ const style = library
75
+ ? parseJson<StyleBrief>(library.styleBrief, {})
76
+ : ({} as StyleBrief);
77
+
78
+ const lines = [
79
+ `Preset "${preset.title}" (id: ${preset.id})`,
80
+ library ? `- Brand kit: ${library.title}` : "",
81
+ preset.description ? `- Intent: ${preset.description}` : "",
82
+ preset.category ? `- Deliverable type: ${preset.category}` : "",
83
+ preset.promptTemplate
84
+ ? `- Prompt philosophy / template: ${preset.promptTemplate}`
85
+ : "",
86
+ preset.textPolicy ? `- Text policy: ${preset.textPolicy}` : "",
87
+ `- Output: ${preset.aspectRatio}, ${preset.imageSize}, model ${preset.model}${
88
+ settings.tier ? `, ${settings.tier} tier` : ""
89
+ }`,
90
+ settings.includeLogo === true
91
+ ? "- Brand logo: the library's canonical logo is composited onto the result; leave a clean upper-right area and do not draw a logo yourself."
92
+ : "",
93
+ ];
94
+
95
+ const aesthetics = [
96
+ style.description ? `overall: ${style.description}` : "",
97
+ style.mood ? `mood: ${style.mood}` : "",
98
+ style.palette?.length ? `palette: ${style.palette.join(", ")}` : "",
99
+ style.medium ? `medium: ${style.medium}` : "",
100
+ style.composition ? `composition: ${style.composition}` : "",
101
+ style.lighting ? `lighting: ${style.lighting}` : "",
102
+ style.texture ? `texture: ${style.texture}` : "",
103
+ style.subjectMatter ? `subject matter: ${style.subjectMatter}` : "",
104
+ style.typographyPolicy ? `typography: ${style.typographyPolicy}` : "",
105
+ ].filter(Boolean);
106
+ if (aesthetics.length) {
107
+ lines.push(`- Brand aesthetics: ${aesthetics.join("; ")}.`);
108
+ }
109
+ if (style.doNot?.length) {
110
+ lines.push(`- Avoid: ${style.doNot.join("; ")}.`);
111
+ }
112
+ const customInstructions = library?.customInstructions?.trim();
113
+ if (customInstructions) {
114
+ lines.push(`- Brand custom instructions: ${customInstructions}`);
115
+ }
116
+
117
+ return lines.filter(Boolean).join("\n");
118
+ }
@@ -8,6 +8,7 @@ import { and, asc, desc, inArray, isNull } from "drizzle-orm";
8
8
 
9
9
  import actionsRegistry from "../../.generated/actions-registry.js";
10
10
  import { getDb, schema } from "../db/index.js";
11
+ import { preparePresetChatContext } from "../lib/preset-chat-context.js";
11
12
  import "../register-secrets.js";
12
13
 
13
14
  const ASSETS_BACKGROUND_RUN_SOFT_TIMEOUT_MS = 13 * 60_000;
@@ -49,6 +50,10 @@ export default createAgentChatPlugin({
49
50
  initialToolNames: INITIAL_TOOL_NAMES,
50
51
  actions: loadActionsFromStaticRegistry(actionsRegistry),
51
52
  resolveOrgId: async (event) => (await getOrgContext(event)).orgId,
53
+ // When a user tags an @preset, embed its aesthetics/philosophy into the
54
+ // model-facing message so the agent internalizes the brief before generating.
55
+ prepareRequest: ({ message, references }) =>
56
+ preparePresetChatContext({ message, references }),
52
57
  mentionProviders: {
53
58
  mediaTypes: {
54
59
  label: "Media type",
@@ -237,6 +237,7 @@ export interface GenerationPresetSummary {
237
237
  model: ImageModel | VideoModel;
238
238
  textPolicy: string;
239
239
  referencePolicy: GenerationPresetReferencePolicy;
240
+ includeLogo: boolean;
240
241
  settings: Record<string, unknown>;
241
242
  sortOrder: number;
242
243
  createdAt?: string;
@@ -9,6 +9,7 @@ import {
9
9
  exportFilename,
10
10
  trySaveExportFile,
11
11
  } from "../server/lib/design-export.js";
12
+ import { isBoardFile } from "../shared/board-file.js";
12
13
  import "../server/db/index.js"; // ensure registerShareableResource runs
13
14
 
14
15
  export default defineAction({
@@ -31,12 +32,13 @@ export default defineAction({
31
32
  .select()
32
33
  .from(schema.designFiles)
33
34
  .where(eq(schema.designFiles.designId, id));
35
+ const exportFiles = files.filter((file) => !isBoardFile(file.filename));
34
36
 
35
- const html = buildStandaloneHtml({ title: row.title, files });
37
+ const html = buildStandaloneHtml({ title: row.title, files: exportFiles });
36
38
 
37
39
  const filename = exportFilename(row.title, "html");
38
40
  const saveResult = await trySaveExportFile(filename, html);
39
41
 
40
- return { html, filename, ...saveResult, fileCount: files.length };
42
+ return { html, filename, ...saveResult, fileCount: exportFiles.length };
41
43
  },
42
44
  });
@@ -4,6 +4,7 @@ import { eq } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import { isBoardFile } from "../shared/board-file.js";
7
8
  import "../server/db/index.js"; // ensure registerShareableResource runs
8
9
 
9
10
  export default defineAction({
@@ -27,6 +28,7 @@ export default defineAction({
27
28
  .select()
28
29
  .from(schema.designFiles)
29
30
  .where(eq(schema.designFiles.designId, id));
31
+ const exportFiles = files.filter((file) => !isBoardFile(file.filename));
30
32
 
31
33
  return {
32
34
  id: row.id,
@@ -34,7 +36,7 @@ export default defineAction({
34
36
  description: row.description,
35
37
  projectType: row.projectType,
36
38
  data: row.data ?? null,
37
- files: files.map((f) => ({
39
+ files: exportFiles.map((f) => ({
38
40
  id: f.id,
39
41
  filename: f.filename,
40
42
  fileType: f.fileType,
@@ -10,6 +10,7 @@ import {
10
10
  exportFilename,
11
11
  trySaveExportFile,
12
12
  } from "../server/lib/design-export.js";
13
+ import { isBoardFile } from "../shared/board-file.js";
13
14
  import "../server/db/index.js"; // ensure registerShareableResource runs
14
15
 
15
16
  export default defineAction({
@@ -45,8 +46,9 @@ export default defineAction({
45
46
  .select()
46
47
  .from(schema.designFiles)
47
48
  .where(eq(schema.designFiles.designId, id));
49
+ const exportFiles = files.filter((file) => !isBoardFile(file.filename));
48
50
 
49
- const html = buildStandaloneHtml({ title: row.title, files });
51
+ const html = buildStandaloneHtml({ title: row.title, files: exportFiles });
50
52
  const svg = buildSvgForeignObject({
51
53
  html,
52
54
  width,
@@ -56,6 +58,6 @@ export default defineAction({
56
58
  const filename = exportFilename(row.title, "svg");
57
59
  const saveResult = await trySaveExportFile(filename, svg);
58
60
 
59
- return { svg, filename, ...saveResult, fileCount: files.length };
61
+ return { svg, filename, ...saveResult, fileCount: exportFiles.length };
60
62
  },
61
63
  });
@@ -8,6 +8,7 @@ import {
8
8
  exportFilename,
9
9
  trySaveExportFile,
10
10
  } from "../server/lib/design-export.js";
11
+ import { isBoardFile } from "../shared/board-file.js";
11
12
  import "../server/db/index.js"; // ensure registerShareableResource runs
12
13
 
13
14
  const METADATA_ARCHIVE_DIR = "agent-native-metadata";
@@ -40,6 +41,7 @@ export default defineAction({
40
41
  .select()
41
42
  .from(schema.designFiles)
42
43
  .where(eq(schema.designFiles.designId, id));
44
+ const exportFiles = files.filter((file) => !isBoardFile(file.filename));
43
45
 
44
46
  // Dynamic import JSZip
45
47
  const JSZip = (await import("jszip")).default;
@@ -57,14 +59,14 @@ export default defineAction({
57
59
  "",
58
60
  "## Files",
59
61
  "",
60
- ...files.map((f) => `- ${f.filename} (${f.fileType})`),
62
+ ...exportFiles.map((f) => `- ${f.filename} (${f.fileType})`),
61
63
  ].join("\n");
62
64
 
63
65
  zip.file(`${METADATA_ARCHIVE_DIR}/README.md`, readme);
64
66
 
65
67
  // Preserve design-relative paths so exported HTML keeps working with
66
68
  // sibling CSS/assets. Strip traversal segments defensively for legacy rows.
67
- for (const [index, file] of files.entries()) {
69
+ for (const [index, file] of exportFiles.entries()) {
68
70
  const filename = safeArchivePath(
69
71
  file.filename,
70
72
  `design-file-${index + 1}.txt`,
@@ -84,6 +86,11 @@ export default defineAction({
84
86
  const filename = exportFilename(row.title, "zip");
85
87
  const saveResult = await trySaveExportFile(filename, zipBuffer);
86
88
 
87
- return { zipBase64, filename, ...saveResult, fileCount: files.length };
89
+ return {
90
+ zipBase64,
91
+ filename,
92
+ ...saveResult,
93
+ fileCount: exportFiles.length,
94
+ };
88
95
  },
89
96
  });
@@ -23,9 +23,22 @@ export default defineAction({
23
23
  "edited live, otherwise the stored content), the design's tweak " +
24
24
  "definitions, the user's applied tweak selections, and the resolved CSS " +
25
25
  "custom-property values so the agent sees the *tuned* design, not the " +
26
- "original generated tokens. Read-only.",
26
+ "original generated tokens. Pass fileId or filename when continuing from " +
27
+ "one selected screen so large multi-file designs stay bounded. Read-only.",
27
28
  schema: z.object({
28
29
  designId: z.string().describe("Design project ID to snapshot"),
30
+ fileId: z
31
+ .string()
32
+ .optional()
33
+ .describe(
34
+ "Optional design file ID to return. Use this after a variant pick to snapshot only the kept screen.",
35
+ ),
36
+ filename: z
37
+ .string()
38
+ .optional()
39
+ .describe(
40
+ "Optional design filename to return when fileId is unavailable.",
41
+ ),
29
42
  }),
30
43
  readOnly: true,
31
44
  http: { method: "GET" },
@@ -40,7 +53,7 @@ export default defineAction({
40
53
  height: 680,
41
54
  }),
42
55
  },
43
- run: async ({ designId }) => {
56
+ run: async ({ designId, fileId, filename }) => {
44
57
  const access = await resolveAccess("design", designId);
45
58
  if (!access) {
46
59
  const err = new Error("Design not found") as Error & {
@@ -52,6 +65,28 @@ export default defineAction({
52
65
  const design = access.resource as typeof schema.designs.$inferSelect;
53
66
 
54
67
  const snapshot = await buildDesignSnapshot(designId, design.data);
68
+ const requestedFileId = fileId?.trim();
69
+ const requestedFilename = filename?.trim();
70
+ const files = requestedFileId
71
+ ? snapshot.files.filter((file) => file.id === requestedFileId)
72
+ : requestedFilename
73
+ ? snapshot.files.filter((file) => file.filename === requestedFilename)
74
+ : snapshot.files;
75
+
76
+ if ((requestedFileId || requestedFilename) && files.length === 0) {
77
+ const err = new Error("Design file not found") as Error & {
78
+ statusCode: number;
79
+ };
80
+ err.statusCode = 404;
81
+ throw err;
82
+ }
83
+ if (!requestedFileId && requestedFilename && files.length > 1) {
84
+ const err = new Error(
85
+ "Multiple design files match filename; pass fileId instead",
86
+ ) as Error & { statusCode: number };
87
+ err.statusCode = 409;
88
+ throw err;
89
+ }
55
90
 
56
91
  return {
57
92
  designId,
@@ -60,13 +95,15 @@ export default defineAction({
60
95
  projectType: design.projectType,
61
96
  designSystemId: design.designSystemId ?? null,
62
97
  updatedAt: design.updatedAt,
63
- files: snapshot.files.map((f) => ({
98
+ files: files.map((f) => ({
99
+ id: f.id,
64
100
  filename: f.filename,
65
101
  fileType: f.fileType,
66
102
  content: f.content,
67
103
  source: f.source,
68
104
  })),
69
- fileCount: snapshot.files.length,
105
+ fileCount: files.length,
106
+ totalFileCount: snapshot.files.length,
70
107
  tweaks: snapshot.tweaks,
71
108
  appliedTweaks: snapshot.appliedTweaks,
72
109
  resolvedCssVars: snapshot.resolvedCssVars,