@agent-native/core 0.80.10 → 0.80.11

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 (66) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  6. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  7. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  8. package/corpus/templates/assets/AGENTS.md +4 -0
  9. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  10. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  11. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  12. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  13. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  14. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  15. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  16. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  17. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  18. package/corpus/templates/assets/app/routes/library.tsx +145 -38
  19. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  20. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  21. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  22. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  23. package/corpus/templates/assets/shared/api.ts +4 -0
  24. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  25. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  26. package/corpus/templates/design/actions/apply-component-prop-edit.ts +13 -5
  27. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  28. package/corpus/templates/design/actions/apply-motion-edit.ts +9 -46
  29. package/corpus/templates/design/actions/delete-design.ts +20 -0
  30. package/corpus/templates/design/actions/get-component-details.ts +16 -7
  31. package/corpus/templates/design/actions/index-design-tokens.ts +8 -4
  32. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  33. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  34. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  35. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  36. package/corpus/templates/design/actions/run-design-audit.ts +21 -6
  37. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +139 -14
  38. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +20 -3
  39. package/corpus/templates/design/app/components/design/EditPanel.tsx +428 -31
  40. package/corpus/templates/design/app/components/design/LayersPanel.tsx +110 -30
  41. package/corpus/templates/design/app/components/design/MotionDock.tsx +9 -0
  42. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1 -1
  43. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +12 -5
  44. package/corpus/templates/design/app/components/design/StatesPanel.tsx +4 -2
  45. package/corpus/templates/design/app/components/design/TokensPanel.tsx +13 -8
  46. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  47. package/corpus/templates/design/app/i18n-data.ts +11 -0
  48. package/corpus/templates/design/app/pages/DesignEditor.tsx +684 -179
  49. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  50. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  51. package/corpus/templates/design/shared/component-model.ts +35 -0
  52. package/corpus/templates/design/shared/motion-compiler.ts +79 -8
  53. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  54. package/dist/agent/production-agent.d.ts.map +1 -1
  55. package/dist/agent/production-agent.js +94 -10
  56. package/dist/agent/production-agent.js.map +1 -1
  57. package/dist/client/AssistantChat.d.ts +14 -0
  58. package/dist/client/AssistantChat.d.ts.map +1 -1
  59. package/dist/client/AssistantChat.js +115 -22
  60. package/dist/client/AssistantChat.js.map +1 -1
  61. package/dist/client/sse-event-processor.d.ts.map +1 -1
  62. package/dist/client/sse-event-processor.js +10 -0
  63. package/dist/client/sse-event-processor.js.map +1 -1
  64. package/dist/observability/routes.d.ts +3 -3
  65. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  66. package/package.json +1 -1
@@ -9,6 +9,7 @@ import { IconPhoto, IconSparkles, IconVideo } from "@tabler/icons-react";
9
9
  import { useEffect } from "react";
10
10
  import { useNavigate, useParams } from "react-router";
11
11
 
12
+ import { RecentDraftsSection } from "@/components/create/RecentDraftsSection";
12
13
  import { GenerationResults } from "@/components/generation/GenerationResults";
13
14
  import { useImageModelMenu } from "@/hooks/use-image-model-menu";
14
15
  import { ASSETS_CHAT_STORAGE_KEY } from "@/lib/chat";
@@ -122,6 +123,9 @@ export default function CreatePage() {
122
123
  </button>
123
124
  ))}
124
125
  </div>
126
+ <div className="mt-8 w-full text-left">
127
+ <RecentDraftsSection />
128
+ </div>
125
129
  </div>
126
130
  }
127
131
  />
@@ -102,16 +102,22 @@ import {
102
102
  type VariantSlot,
103
103
  } from "./brand-kits.$id";
104
104
 
105
- type AssetTab = "all" | "generated" | "references";
105
+ type AssetTab = "all" | "generated" | "drafts" | "references";
106
106
 
107
107
  function isGeneratedAsset(asset: Asset) {
108
108
  const role = asset.role ?? "";
109
109
  return role === "generated" || role === "active" || role === "candidate";
110
110
  }
111
111
 
112
+ function isDraftAsset(asset: Asset) {
113
+ return asset.role === "generated" && asset.status === "candidate";
114
+ }
115
+
112
116
  function assetMatchesTab(asset: Asset, tab: AssetTab) {
113
117
  if (tab === "all") return true;
114
- if (tab === "generated") return isGeneratedAsset(asset);
118
+ if (tab === "drafts") return isDraftAsset(asset);
119
+ if (tab === "generated")
120
+ return isGeneratedAsset(asset) && !isDraftAsset(asset);
115
121
  return !isGeneratedAsset(asset);
116
122
  }
117
123
 
@@ -1957,12 +1963,29 @@ export function LibraryWorkspace({
1957
1963
 
1958
1964
  export function AssetPickerSurface() {
1959
1965
  const t = useT();
1960
- const [searchParams] = useSearchParams();
1966
+ const [searchParams, setSearchParams] = useSearchParams();
1961
1967
  const searchParamsKey = searchParams.toString();
1968
+ const urlAssetTab = useMemo<AssetTab | null>(() => {
1969
+ const tab = new URLSearchParams(searchParamsKey).get("tab");
1970
+ return tab === "all" ||
1971
+ tab === "generated" ||
1972
+ tab === "drafts" ||
1973
+ tab === "references"
1974
+ ? tab
1975
+ : null;
1976
+ }, [searchParamsKey]);
1977
+ // The active tab is the only host-irrelevant search param. Exclude it from the
1978
+ // host-config key so toggling tabs (which writes `?tab=`) doesn't retrigger the
1979
+ // effect that resets media type / query / library from the URL.
1980
+ const hostParamsKey = useMemo(() => {
1981
+ const params = new URLSearchParams(searchParamsKey);
1982
+ params.delete("tab");
1983
+ return params.toString();
1984
+ }, [searchParamsKey]);
1962
1985
  const mcpChatBridgeActive =
1963
1986
  searchParamsRequestPicker(searchParams) || isEmbedMcpChatBridgeActive();
1964
1987
  const urlHostConfig = useMemo(() => {
1965
- const params = new URLSearchParams(searchParamsKey);
1988
+ const params = new URLSearchParams(hostParamsKey);
1966
1989
  return {
1967
1990
  mediaType: normalizeMediaType(params.get("mediaType")),
1968
1991
  prompt: params.get("prompt") ?? undefined,
@@ -1983,7 +2006,7 @@ export function AssetPickerSurface() {
1983
2006
  ),
1984
2007
  autoGenerate: normalizeBoolean(params.get("autoGenerate")) ?? false,
1985
2008
  } satisfies HostConfig;
1986
- }, [searchParamsKey]);
2009
+ }, [hostParamsKey]);
1987
2010
  const bridgeRef = useRef<EmbeddedAppBridge | null>(null);
1988
2011
  const embedded = useMemo(
1989
2012
  () =>
@@ -2008,7 +2031,7 @@ export function AssetPickerSurface() {
2008
2031
  );
2009
2032
  const [presetId, setPresetId] = useState(() => hostConfig.presetId ?? "none");
2010
2033
  const [count, setCount] = useState(() => hostConfig.count ?? 3);
2011
- const [assetTab, setAssetTab] = useState<AssetTab>("all");
2034
+ const [assetTab, setAssetTab] = useState<AssetTab>(urlAssetTab ?? "all");
2012
2035
  const [selectedLibraryId, setSelectedLibraryId] = useState(
2013
2036
  () => hostConfig.libraryId ?? "",
2014
2037
  );
@@ -2032,6 +2055,30 @@ export function AssetPickerSurface() {
2032
2055
  setVisibleCandidateRunIds(urlHostConfig.candidateRunIds ?? []);
2033
2056
  }, [urlHostConfig]);
2034
2057
 
2058
+ useEffect(() => {
2059
+ // Reset to "all" when the tab param is removed (e.g. back/forward nav)
2060
+ // since the component stays mounted across search-param changes.
2061
+ setAssetTab(urlAssetTab ?? "all");
2062
+ }, [urlAssetTab]);
2063
+
2064
+ const handleAssetTabChange = useCallback(
2065
+ (value: AssetTab) => {
2066
+ setAssetTab(value);
2067
+ // Keep the tab reflected in the URL so it survives refresh/share and
2068
+ // stays consistent with the `?tab=` deep link from the home page.
2069
+ setSearchParams(
2070
+ (prev) => {
2071
+ const next = new URLSearchParams(prev);
2072
+ if (value === "all") next.delete("tab");
2073
+ else next.set("tab", value);
2074
+ return next;
2075
+ },
2076
+ { replace: true },
2077
+ );
2078
+ },
2079
+ [setSearchParams],
2080
+ );
2081
+
2035
2082
  const librariesQuery = useActionQuery("list-libraries", {
2036
2083
  compact: true,
2037
2084
  } as any) as {
@@ -2116,23 +2163,54 @@ export function AssetPickerSurface() {
2116
2163
  !selectedPreset &&
2117
2164
  Boolean(waitingForPresetData);
2118
2165
  const mediaLabel = mediaType === "video" ? "video" : "image";
2166
+ const viewingDrafts = assetTab === "drafts";
2167
+ // The standalone Library "Drafts" tab mirrors the home Recent Drafts section:
2168
+ // every unsaved draft across all accessible libraries, regardless of media
2169
+ // type. The embedded picker keeps its library + media-type scope so an
2170
+ // image-only picker never surfaces video drafts.
2171
+ const draftsGlobalView = viewingDrafts && !embedded;
2119
2172
  const assetsParams = useMemo(
2120
2173
  () => ({
2121
2174
  libraryId: selectedLibraryId,
2122
2175
  mediaType,
2176
+ // Drafts are exactly generated candidates — filter them server-side
2177
+ // instead of fetching the whole library and filtering on the client.
2178
+ role: viewingDrafts ? "generated" : undefined,
2179
+ status: viewingDrafts ? "candidate" : undefined,
2123
2180
  query: query.trim() || undefined,
2124
2181
  includeCandidates:
2125
- mediaType === "image" && visibleCandidateRunIds.length > 0,
2182
+ viewingDrafts ||
2183
+ (mediaType === "image" && visibleCandidateRunIds.length > 0),
2184
+ // The Drafts tab shows every unsaved draft, not just the latest run batch.
2126
2185
  candidateRunIds:
2127
- visibleCandidateRunIds.length > 0 ? visibleCandidateRunIds : undefined,
2186
+ !viewingDrafts && visibleCandidateRunIds.length > 0
2187
+ ? visibleCandidateRunIds
2188
+ : undefined,
2128
2189
  }),
2129
- [mediaType, query, selectedLibraryId, visibleCandidateRunIds],
2190
+ [
2191
+ viewingDrafts,
2192
+ mediaType,
2193
+ query,
2194
+ selectedLibraryId,
2195
+ visibleCandidateRunIds,
2196
+ ],
2130
2197
  );
2131
2198
  const { data: assetData, isLoading: assetsLoading } = useActionQuery(
2132
2199
  "list-assets",
2133
2200
  assetsParams as any,
2134
- { enabled: Boolean(selectedLibraryId) && !usingStarterLibrary } as any,
2201
+ {
2202
+ enabled:
2203
+ Boolean(selectedLibraryId) && !usingStarterLibrary && !draftsGlobalView,
2204
+ } as any,
2135
2205
  ) as { data?: { assets?: Asset[] }; isLoading: boolean };
2206
+ const { data: globalDraftsData, isLoading: globalDraftsLoading } =
2207
+ useActionQuery(
2208
+ "list-draft-assets",
2209
+ { limit: 500 } as any,
2210
+ {
2211
+ enabled: draftsGlobalView,
2212
+ } as any,
2213
+ ) as { data?: { assets?: Asset[] }; isLoading: boolean };
2136
2214
  const starterAssets: Asset[] = useMemo(
2137
2215
  () =>
2138
2216
  STARTER_PRESET.referenceImages.map((reference) => ({
@@ -2159,11 +2237,23 @@ export function AssetPickerSurface() {
2159
2237
  .some((value) => String(value).toLowerCase().includes(needle)),
2160
2238
  );
2161
2239
  }, [query, starterAssets]);
2162
- const allAssets = usingStarterLibrary
2163
- ? mediaType === "image"
2164
- ? visibleStarterAssets
2165
- : []
2166
- : (assetData?.assets ?? []);
2240
+ const globalDrafts = useMemo(() => {
2241
+ const drafts = globalDraftsData?.assets ?? [];
2242
+ const needle = query.trim().toLowerCase();
2243
+ if (!needle) return drafts;
2244
+ return drafts.filter((asset) =>
2245
+ [asset.title, asset.description, asset.prompt]
2246
+ .filter(Boolean)
2247
+ .some((value) => String(value).toLowerCase().includes(needle)),
2248
+ );
2249
+ }, [globalDraftsData, query]);
2250
+ const allAssets = draftsGlobalView
2251
+ ? globalDrafts
2252
+ : usingStarterLibrary
2253
+ ? mediaType === "image"
2254
+ ? visibleStarterAssets
2255
+ : []
2256
+ : (assetData?.assets ?? []);
2167
2257
  const currentLibrary = useMemo(
2168
2258
  () =>
2169
2259
  displayLibraries.find((library) => library.id === selectedLibraryId) ??
@@ -2174,6 +2264,7 @@ export function AssetPickerSurface() {
2174
2264
  () => allAssets.filter((asset) => assetMatchesTab(asset, assetTab)),
2175
2265
  [allAssets, assetTab],
2176
2266
  );
2267
+ const listLoading = draftsGlobalView ? globalDraftsLoading : assetsLoading;
2177
2268
  const [previewAsset, setPreviewAsset] = useState<Asset | null>(null);
2178
2269
  const [standaloneSelection, setStandaloneSelection] = useState<ReturnType<
2179
2270
  typeof assetPayload
@@ -2845,27 +2936,35 @@ export function AssetPickerSurface() {
2845
2936
  {displayLibraries.length > 0 && (
2846
2937
  <div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
2847
2938
  <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
2848
- <Select
2849
- value={selectedLibraryId}
2850
- onValueChange={setSelectedLibraryId}
2851
- >
2852
- <SelectTrigger className="h-9 w-full border-border/70 bg-background sm:w-48">
2853
- <SelectValue placeholder={t("library.library")} />
2854
- </SelectTrigger>
2855
- <SelectContent>
2856
- <SelectGroup>
2857
- {displayLibraries.map((library) => (
2858
- <SelectItem key={library.id} value={library.id}>
2859
- {library.title}
2860
- </SelectItem>
2861
- ))}
2862
- </SelectGroup>
2863
- </SelectContent>
2864
- </Select>
2939
+ {draftsGlobalView ? (
2940
+ <div className="flex h-9 items-center rounded-md border border-border/70 bg-background px-3 text-sm text-muted-foreground sm:w-48">
2941
+ {t("library.allLibraries")}
2942
+ </div>
2943
+ ) : (
2944
+ <Select
2945
+ value={selectedLibraryId}
2946
+ onValueChange={setSelectedLibraryId}
2947
+ >
2948
+ <SelectTrigger className="h-9 w-full border-border/70 bg-background sm:w-48">
2949
+ <SelectValue placeholder={t("library.library")} />
2950
+ </SelectTrigger>
2951
+ <SelectContent>
2952
+ <SelectGroup>
2953
+ {displayLibraries.map((library) => (
2954
+ <SelectItem key={library.id} value={library.id}>
2955
+ {library.title}
2956
+ </SelectItem>
2957
+ ))}
2958
+ </SelectGroup>
2959
+ </SelectContent>
2960
+ </Select>
2961
+ )}
2865
2962
  {selectedLibraryId && (
2866
2963
  <Tabs
2867
2964
  value={assetTab}
2868
- onValueChange={(value) => setAssetTab(value as AssetTab)}
2965
+ onValueChange={(value) =>
2966
+ handleAssetTabChange(value as AssetTab)
2967
+ }
2869
2968
  >
2870
2969
  <TabsList>
2871
2970
  <TabsTrigger value="all">
@@ -2874,6 +2973,9 @@ export function AssetPickerSurface() {
2874
2973
  <TabsTrigger value="generated">
2875
2974
  {t("library.generated")}
2876
2975
  </TabsTrigger>
2976
+ <TabsTrigger value="drafts">
2977
+ {t("library.drafts")}
2978
+ </TabsTrigger>
2877
2979
  <TabsTrigger value="references">
2878
2980
  {t("library.references")}
2879
2981
  </TabsTrigger>
@@ -2897,6 +2999,7 @@ export function AssetPickerSurface() {
2897
2999
  {mediaType === "image" &&
2898
3000
  selectedLibraryId &&
2899
3001
  !usingStarterLibrary &&
3002
+ !viewingDrafts &&
2900
3003
  pickerVariantScopeId && (
2901
3004
  <LibraryCandidateStage
2902
3005
  activeLibraryId={selectedLibraryId}
@@ -2914,7 +3017,7 @@ export function AssetPickerSurface() {
2914
3017
  </div>
2915
3018
  )}
2916
3019
 
2917
- {selectedLibraryId && assetsLoading && (
3020
+ {selectedLibraryId && listLoading && (
2918
3021
  <div className="assets-library-dense-grid grid grid-cols-2 gap-3">
2919
3022
  {Array.from({ length: 8 }).map((_, index) => (
2920
3023
  <Skeleton key={index} className="aspect-square rounded-md" />
@@ -2922,12 +3025,16 @@ export function AssetPickerSurface() {
2922
3025
  </div>
2923
3026
  )}
2924
3027
 
2925
- {selectedLibraryId && !assetsLoading && assets.length === 0 && (
3028
+ {selectedLibraryId && !listLoading && assets.length === 0 && (
2926
3029
  <div className="flex h-full items-center justify-center text-center">
2927
3030
  <div className="max-w-sm text-sm text-muted-foreground">
2928
- {query
2929
- ? t("library.noMatchingLibraryAssets", { mediaLabel })
2930
- : t("library.noAssetsInLibrary", { mediaLabel })}
3031
+ {draftsGlobalView
3032
+ ? query
3033
+ ? t("library.noMatchingDrafts")
3034
+ : t("library.noDrafts")
3035
+ : query
3036
+ ? t("library.noMatchingLibraryAssets", { mediaLabel })
3037
+ : t("library.noAssetsInLibrary", { mediaLabel })}
2931
3038
  </div>
2932
3039
  </div>
2933
3040
  )}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-29
4
+ ---
5
+
6
+ Image generation can now render requested text and respect brand fonts
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-30
4
+ ---
5
+
6
+ Image generation no longer looks stuck while the provider is still creating and saving the asset.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-30
4
+ ---
5
+
6
+ Tagging a @preset now reliably uses its aspect ratio and other saved settings instead of a guessed one
@@ -11,6 +11,7 @@ import type {
11
11
  GenerationIntent,
12
12
  ImageCategory,
13
13
  ImageModel,
14
+ ImageQualityTier,
14
15
  ImageSize,
15
16
  StyleStrength,
16
17
  StyleBrief,
@@ -564,7 +565,8 @@ export async function analyzeStyleWithGemini(input: {
564
565
  {
565
566
  text: [
566
567
  "Analyze these brand/style reference images for a reusable image generation style brief.",
567
- "Return only compact JSON with keys: description, medium, mood, subjectMatter, texture, composition, lighting, typographyPolicy, doNot.",
568
+ "Return only compact JSON with keys: description, medium, mood, subjectMatter, texture, composition, lighting, fontFamilies, fontWeights, letterforms, caseStyle, typographyPolicy, doNot.",
569
+ "For typography, describe specific letterforms and font traits, not just broad sans/serif categories; omit any typography field you cannot determine confidently.",
568
570
  "Use specific trait-locking phrases that can be reused across future image prompts.",
569
571
  "Do not name brands, artists, studios, franchises, or copyrighted works unless they appear as user-provided brand identity.",
570
572
  "Keep doNot as an array of short constraints. Omit uncertain fields instead of guessing.",
@@ -608,16 +610,24 @@ function extractJsonObject(text: string): string {
608
610
  return "{}";
609
611
  }
610
612
 
611
- function sanitizeStyleBrief(value: Record<string, unknown>): StyleBrief {
613
+ export function sanitizeStyleBrief(value: Record<string, unknown>): StyleBrief {
612
614
  const stringField = (key: string) => {
613
615
  const raw = value[key];
614
616
  return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
615
617
  };
616
- const doNot = Array.isArray(value.doNot)
617
- ? value.doNot.filter(
618
- (item): item is string => typeof item === "string" && !!item.trim(),
619
- )
620
- : undefined;
618
+ const stringArrayField = (key: string) => {
619
+ const raw = value[key];
620
+ return Array.isArray(raw)
621
+ ? raw
622
+ .filter(
623
+ (item): item is string => typeof item === "string" && !!item.trim(),
624
+ )
625
+ .map((item) => item.trim())
626
+ : undefined;
627
+ };
628
+ const fontFamilies = stringArrayField("fontFamilies");
629
+ const fontWeights = stringArrayField("fontWeights");
630
+ const doNot = stringArrayField("doNot");
621
631
  return {
622
632
  description: stringField("description"),
623
633
  medium: stringField("medium"),
@@ -626,8 +636,12 @@ function sanitizeStyleBrief(value: Record<string, unknown>): StyleBrief {
626
636
  texture: stringField("texture"),
627
637
  composition: stringField("composition"),
628
638
  lighting: stringField("lighting"),
639
+ fontFamilies: fontFamilies?.length ? fontFamilies : undefined,
640
+ fontWeights: fontWeights?.length ? fontWeights : undefined,
641
+ letterforms: stringField("letterforms"),
642
+ caseStyle: stringField("caseStyle"),
629
643
  typographyPolicy: stringField("typographyPolicy"),
630
- doNot: doNot?.length ? doNot.map((item) => item.trim()) : undefined,
644
+ doNot: doNot?.length ? doNot : undefined,
631
645
  };
632
646
  }
633
647
 
@@ -774,6 +788,66 @@ function toBuilderImageModel(model: ImageModel) {
774
788
  return model;
775
789
  }
776
790
 
791
+ function resolveModelForTier(
792
+ tier: ImageQualityTier | undefined,
793
+ category: ImageCategory | undefined,
794
+ ): ImageModel | undefined {
795
+ if (!tier) return undefined;
796
+ if (tier === "fast") return "gemini-3.1-flash-image";
797
+ if (tier === "best") return "gemini-3-pro-image";
798
+ return ["hero", "landing", "logo", "campaign"].includes(category ?? "")
799
+ ? "gemini-3-pro-image"
800
+ : "gemini-3.1-flash-image";
801
+ }
802
+
803
+ export function resolveImageModelForRequest(input: {
804
+ explicitModel?: ImageModel;
805
+ imageModelDefault?: ImageModel;
806
+ explicitTier?: ImageQualityTier;
807
+ resolvedTier?: ImageQualityTier;
808
+ category?: ImageCategory;
809
+ presetModel?: ImageModel | null;
810
+ embeddedText?: string | null;
811
+ }): ImageModel {
812
+ if (input.explicitModel) return input.explicitModel;
813
+
814
+ // Exact embedded text is materially better on Gemini Pro, so upgrade to it by
815
+ // default for text requests — but never override a model or tier the caller
816
+ // or a tagged preset already chose (presets "own" model/tier per the action
817
+ // docs). Only auto-upgrade when there is no tier (explicit or preset-derived)
818
+ // and no preset model in play; this still upgrades a bare text request over
819
+ // the composer default.
820
+ const textAccurateModel: ImageModel | undefined =
821
+ input.embeddedText?.trim() &&
822
+ !input.explicitTier &&
823
+ !input.resolvedTier &&
824
+ !input.presetModel
825
+ ? "gemini-3-pro-image"
826
+ : undefined;
827
+
828
+ // Precedence: explicit per-request model (handled above) > embedded-text
829
+ // upgrade > an EXPLICIT per-request tier > the preset's saved model > the
830
+ // preset-DERIVED tier mapping > the sticky composer default > the floor.
831
+ //
832
+ // Two subtleties:
833
+ // - An explicit per-request tier outranks the preset's saved model (the
834
+ // caller is deliberately overriding the preset this turn).
835
+ // - The preset's explicit saved model outranks its OWN derived tier: a
836
+ // preset's `model` column and `settings.tier` can drift out of sync
837
+ // (update-generation-preset can change one without the other), and the
838
+ // explicitly saved model is the authoritative choice.
839
+ // - The composer default ranks LAST of the real signals: it is a global
840
+ // stored preference and must not defeat a tagged preset or a tier request.
841
+ return (
842
+ textAccurateModel ??
843
+ resolveModelForTier(input.explicitTier, input.category) ??
844
+ input.presetModel ??
845
+ resolveModelForTier(input.resolvedTier, input.category) ??
846
+ input.imageModelDefault ??
847
+ "gemini-3.1-flash-image"
848
+ );
849
+ }
850
+
777
851
  function toBuilderReferenceRole(role: string) {
778
852
  switch (role) {
779
853
  case "style_reference":
@@ -799,6 +873,8 @@ export function compilePrompt(input: {
799
873
  styleBrief: StyleBrief;
800
874
  customInstructions?: string | null;
801
875
  prompt: string;
876
+ embeddedText?: string | null;
877
+ textPlacement?: string | null;
802
878
  referenceCount: number;
803
879
  includeLogo: boolean;
804
880
  aspectRatio?: AspectRatio;
@@ -815,6 +891,11 @@ export function compilePrompt(input: {
815
891
  const doNot = style.doNot?.length
816
892
  ? `\nAvoid: ${style.doNot.join("; ")}.`
817
893
  : "";
894
+ const typographyBlock = formatBrandTypography(style);
895
+ const requestedEmbeddedText = hasEmbeddedText(input.embeddedText);
896
+ const textInstruction = requestedEmbeddedText
897
+ ? formatEmbeddedTextInstruction(input.embeddedText, input.textPlacement)
898
+ : "Do not render headlines, body text, UI labels, or prompt wording inside the image unless the user explicitly asks for exact visible text.";
818
899
  const logoInstruction = input.includeLogo
819
900
  ? "\nLeave a clean uncluttered area in the upper-right for the real brand logo; do not draw or approximate the logo yourself."
820
901
  : "";
@@ -839,14 +920,24 @@ export function compilePrompt(input: {
839
920
  : "No reference images are attached for this run. Use the style brief and custom instructions as the source of truth.";
840
921
 
841
922
  if (intent === "edit") {
923
+ const editTypographyInstruction =
924
+ requestedEmbeddedText && typographyBlock ? `\n\n${typographyBlock}` : "";
925
+ const editTextInstruction = requestedEmbeddedText
926
+ ? `\n\n${formatEmbeddedTextInstruction(input.embeddedText, input.textPlacement)}`
927
+ : "";
928
+ const editConstraint = requestedEmbeddedText
929
+ ? "Do not reimagine the image, change its aspect ratio, or alter unrelated subjects. Do not add any other new text. Return the full image."
930
+ : "Do not reimagine the image, change its aspect ratio, add new text, or alter unrelated subjects. Return the full image.";
842
931
  return `Edit the attached image for the "${input.libraryTitle}" asset library.
843
932
 
844
933
  ${referenceInstruction}
934
+ ${editTypographyInstruction}
845
935
 
846
936
  Make only this change:
847
937
  ${input.prompt}
938
+ ${editTextInstruction}
848
939
 
849
- Do not reimagine the image, change its aspect ratio, add new text, or alter unrelated subjects. Return the full image.`;
940
+ ${editConstraint}`;
850
941
  }
851
942
 
852
943
  return `Create a brand-consistent image for the "${input.libraryTitle}" asset library.
@@ -861,15 +952,48 @@ ${style.subjectMatter ? `\nSubject matter: ${style.subjectMatter}.` : ""}
861
952
  ${style.texture ? `\nTexture/material treatment: ${style.texture}.` : ""}
862
953
  ${style.composition ? `\nComposition: ${style.composition}.` : ""}
863
954
  ${style.lighting ? `\nLighting: ${style.lighting}.` : ""}
864
- ${style.typographyPolicy ? `\nTypography policy: ${style.typographyPolicy}.` : ""}${frameInstruction}
955
+ ${typographyBlock ? `\n${typographyBlock}` : ""}${frameInstruction}
865
956
  ${doNot}${logoInstruction}${diagramInstruction}${customInstructions}
866
957
 
867
- Do not render headlines, body text, UI labels, or prompt wording inside the image unless the user explicitly asks for exact visible text.
958
+ ${textInstruction}
868
959
 
869
960
  User request:
870
961
  ${input.prompt}`;
871
962
  }
872
963
 
964
+ function hasEmbeddedText(value?: string | null): boolean {
965
+ return typeof value === "string" && !!value.trim();
966
+ }
967
+
968
+ function formatEmbeddedTextInstruction(
969
+ embeddedText?: string | null,
970
+ textPlacement?: string | null,
971
+ ): string {
972
+ const placement = textPlacement?.trim();
973
+ return [
974
+ `Render this text exactly, spelled correctly, inside the image: ${JSON.stringify(embeddedText ?? "")}.`,
975
+ placement ? `Placement: ${placement}.` : "",
976
+ "Match the brand typography described above where specified; keep it legible and well-kerned.",
977
+ ]
978
+ .filter(Boolean)
979
+ .join(" ");
980
+ }
981
+
982
+ function formatBrandTypography(style: StyleBrief): string {
983
+ const parts = [
984
+ style.fontFamilies?.length
985
+ ? `families ${style.fontFamilies.join(", ")}`
986
+ : "",
987
+ style.fontWeights?.length ? `weights ${style.fontWeights.join(", ")}` : "",
988
+ style.letterforms ? `letterforms: ${style.letterforms}` : "",
989
+ style.caseStyle ? `case: ${style.caseStyle}` : "",
990
+ style.typographyPolicy,
991
+ ].filter((part): part is string => typeof part === "string" && !!part.trim());
992
+
993
+ if (!parts.length) return "";
994
+ return `Brand typography: ${parts.join("; ")}. Match these for any rendered text.`;
995
+ }
996
+
873
997
  // A content-only reference is an image the user attached as subject/content for
874
998
  // a single request (not a reusable brand/style reference). It should never count
875
999
  // as a brand-kit style reference.
@@ -112,6 +112,10 @@ export interface StyleBrief {
112
112
  texture?: string;
113
113
  composition?: string;
114
114
  lighting?: string;
115
+ fontFamilies?: string[];
116
+ fontWeights?: string[];
117
+ letterforms?: string;
118
+ caseStyle?: string;
115
119
  typographyPolicy?: string;
116
120
  doNot?: string[];
117
121
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * List pending clips AI requests for the current user.
3
+ *
4
+ * Collapses the per-recording `clips-ai-request-<id>` polling done by the
5
+ * auto-title bridge into a single call. Application state is already
6
+ * session-scoped, and we additionally filter the referenced recordings through
7
+ * `accessFilter` so we never return a request for a recording the user can't
8
+ * access. Only requests for `status = "ready"` recordings are returned — the
9
+ * bridge ignores non-ready recordings, so excluding them avoids sending large
10
+ * transcript blobs for recordings the hook will skip this tick.
11
+ */
12
+
13
+ import { defineAction } from "@agent-native/core";
14
+ import { listAppState } from "@agent-native/core/application-state";
15
+ import { accessFilter } from "@agent-native/core/sharing";
16
+ import { and, eq, inArray } from "drizzle-orm";
17
+ import { z } from "zod";
18
+
19
+ import { getDb, schema } from "../server/db/index.js";
20
+
21
+ const REQUEST_PREFIX = "clips-ai-request-";
22
+
23
+ export default defineAction({
24
+ description:
25
+ "List pending clips AI requests (regenerate-title, summary, chapters, etc.) for recordings the current user can access.",
26
+ schema: z.object({}),
27
+ http: { method: "GET" },
28
+ run: async () => {
29
+ const entries = await listAppState(REQUEST_PREFIX);
30
+
31
+ const requests = entries
32
+ .map((e) => e.value as Record<string, unknown>)
33
+ .filter(
34
+ (v): v is Record<string, unknown> & { recordingId: string } =>
35
+ !!v && typeof v.recordingId === "string" && v.recordingId.length > 0,
36
+ );
37
+
38
+ if (requests.length === 0) return { requests: [] };
39
+
40
+ const recordingIds = [...new Set(requests.map((r) => r.recordingId))];
41
+
42
+ const db = getDb();
43
+ const accessible = await db
44
+ .select({ id: schema.recordings.id })
45
+ .from(schema.recordings)
46
+ .where(
47
+ and(
48
+ accessFilter(schema.recordings, schema.recordingShares),
49
+ inArray(schema.recordings.id, recordingIds),
50
+ eq(schema.recordings.status, "ready"),
51
+ ),
52
+ );
53
+
54
+ const accessibleIds = new Set(accessible.map((r) => r.id));
55
+
56
+ return {
57
+ requests: requests.filter((r) => accessibleIds.has(r.recordingId)),
58
+ };
59
+ },
60
+ });