@agent-native/core 0.125.0 → 0.126.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/server/email-template.ts +13 -0
  5. package/corpus/core/src/server/email.ts +40 -4
  6. package/corpus/core/src/sharing/actions/share-resource.ts +55 -5
  7. package/corpus/core/src/sharing/registry.ts +41 -1
  8. package/corpus/templates/assets/actions/edit-image.ts +31 -18
  9. package/corpus/templates/assets/actions/refine-image.ts +43 -29
  10. package/corpus/templates/assets/actions/restyle-image.ts +30 -19
  11. package/corpus/templates/assets/actions/variant-slots.ts +26 -0
  12. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +85 -17
  13. package/corpus/templates/clips/changelog/2026-07-27-share-notification-emails-now-include-a-clickable-recording-.md +6 -0
  14. package/corpus/templates/clips/server/db/index.ts +28 -6
  15. package/corpus/templates/clips/server/lib/share-email-hero.ts +105 -0
  16. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +100 -12
  17. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +17 -0
  18. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +169 -22
  19. package/dist/collab/routes.d.ts +1 -1
  20. package/dist/collab/struct-routes.d.ts +1 -1
  21. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  22. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  23. package/dist/secrets/routes.d.ts +9 -9
  24. package/dist/server/email-template.d.ts +6 -0
  25. package/dist/server/email-template.d.ts.map +1 -1
  26. package/dist/server/email-template.js +6 -0
  27. package/dist/server/email-template.js.map +1 -1
  28. package/dist/server/email.d.ts +7 -0
  29. package/dist/server/email.d.ts.map +1 -1
  30. package/dist/server/email.js +31 -6
  31. package/dist/server/email.js.map +1 -1
  32. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  33. package/dist/sharing/actions/share-resource.js +49 -5
  34. package/dist/sharing/actions/share-resource.js.map +1 -1
  35. package/dist/sharing/registry.d.ts +40 -1
  36. package/dist/sharing/registry.d.ts.map +1 -1
  37. package/dist/sharing/registry.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/server/email-template.ts +13 -0
  40. package/src/server/email.ts +40 -4
  41. package/src/sharing/actions/share-resource.ts +55 -5
  42. package/src/sharing/registry.ts +41 -1
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1737
32
32
  - toolkit files: 160
33
- - template files: 6804
33
+ - template files: 6806
@@ -1,5 +1,13 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.126.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b99b899: **Breaking:** rename the `registerShareableResource` resolver `getShareEmailLogoUrl` (added in 0.125.0) to `getLogoUrl`. Update any registration that used the old name.
8
+
9
+ Add `getBrandName`, `getSender`, and `getHeroHtml` resolvers to `registerShareableResource`, plus an optional `heroHtml` on `renderEmail` and `fromName` on `sendEmail`. Share-notification emails can now render a template-owned preview block above the CTA (injected verbatim, so a template supplies its own markup — e.g. a video thumbnail with a play badge), override the brand name shown beside the logo, and appear to come from the sharing user ("Alice via Clips") with replies routed to them while keeping the configured domain-verified sending address so SPF/DKIM still pass.
10
+
3
11
  ## 0.125.0
4
12
 
5
13
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.125.0",
3
+ "version": "0.126.0",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -36,6 +36,12 @@ export interface RenderEmailArgs {
36
36
  paragraphs: string[];
37
37
  /** Primary call-to-action rendered as a real button. */
38
38
  cta?: EmailCta;
39
+ /**
40
+ * Optional trusted HTML injected above the CTA — e.g. a template-owned video
41
+ * thumbnail with a play badge. Injected verbatim, so only pass markup built
42
+ * by app/template code (never raw user input), and escape any dynamic values.
43
+ */
44
+ heroHtml?: string;
39
45
  /** Small muted text under the CTA (e.g. expiry note). */
40
46
  footer?: string;
41
47
  /** Optional app name shown beside the framework logo. */
@@ -108,6 +114,12 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
108
114
  const ctaFg = brand ? "#ffffff" : "#0a0a0c";
109
115
  const linkColor = brand ?? "#a1a1aa";
110
116
 
117
+ // Trusted markup supplied by the caller (template code, not user input),
118
+ // injected as-is so a template can own app-specific previews (e.g. a video
119
+ // thumbnail with a play badge). Callers are responsible for escaping any
120
+ // dynamic values they interpolate.
121
+ const heroHtml = args.heroHtml ?? "";
122
+
111
123
  const paragraphsHtml = args.paragraphs
112
124
  .map(
113
125
  (p) =>
@@ -174,6 +186,7 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
174
186
  ${escapeHtml(args.heading)}
175
187
  </h1>
176
188
  ${paragraphsHtml}
189
+ ${heroHtml}
177
190
  ${ctaHtml}
178
191
  ${footerHtml}
179
192
  </td>
@@ -30,6 +30,13 @@ export interface SendEmailArgs {
30
30
  html: string;
31
31
  text?: string;
32
32
  from?: string;
33
+ /**
34
+ * Display-name-only override. Keeps the configured (domain-verified) sending
35
+ * address and just changes the name shown to the recipient, e.g.
36
+ * "Alice via Clips". Ignored when `from` is set. Prefer this over `from` for
37
+ * per-user senders: putting a user's own address in `From` breaks SPF/DKIM.
38
+ */
39
+ fromName?: string;
33
40
  cc?: string | string[];
34
41
  replyTo?: string;
35
42
  inReplyTo?: string;
@@ -110,9 +117,14 @@ export async function getEmailProvider(): Promise<EmailProvider> {
110
117
  function getFromAddress(
111
118
  config: EmailTransportConfig,
112
119
  override?: string,
120
+ fromName?: string,
113
121
  ): string {
114
- const explicit = override || config.from;
115
- if (explicit) return explicit;
122
+ if (override) return override;
123
+ const base = config.from ?? defaultFromAddress(config);
124
+ return fromName ? withDisplayName(base, fromName) : base;
125
+ }
126
+
127
+ function defaultFromAddress(config: EmailTransportConfig): string {
116
128
  // Resend lets unverified accounts send from its sandbox domain; SendGrid
117
129
  // does not, so falling back there would cause silent 403s at runtime.
118
130
  if (config.provider === "sendgrid") {
@@ -123,6 +135,21 @@ function getFromAddress(
123
135
  return "Agent Native <onboarding@resend.dev>";
124
136
  }
125
137
 
138
+ /**
139
+ * Swap the display name while keeping the verified address. The name is
140
+ * sanitized and quoted because it lands in a header: CR/LF would allow header
141
+ * injection, and quotes/angle brackets would break address parsing.
142
+ */
143
+ function withDisplayName(from: string, name: string): string {
144
+ const safe = name
145
+ .replace(/[\r\n"<>\\]/g, " ")
146
+ .replace(/\s+/g, " ")
147
+ .trim();
148
+ if (!safe) return from;
149
+ const address = from.match(/<([^>]+)>/)?.[1]?.trim() ?? from.trim();
150
+ return `"${safe}" <${address}>`;
151
+ }
152
+
126
153
  async function sendEmailWithSignal(
127
154
  args: SendEmailArgs,
128
155
  signal?: AbortSignal,
@@ -130,7 +157,7 @@ async function sendEmailWithSignal(
130
157
  const config = await resolveEmailTransport();
131
158
  signal?.throwIfAborted();
132
159
  const provider = config.provider;
133
- const from = getFromAddress(config, args.from);
160
+ const from = getFromAddress(config, args.from, args.fromName);
134
161
  const attachments = resolveAttachments(args);
135
162
 
136
163
  if (provider === "resend") {
@@ -270,10 +297,19 @@ export async function sendEmail(args: SendEmailArgs): Promise<void> {
270
297
 
271
298
  function parseSendGridFrom(from: string): { email: string; name?: string } {
272
299
  const m = from.match(/^\s*(.*?)\s*<(.+)>\s*$/);
273
- if (m && m[2]) return { name: m[1] || undefined, email: m[2] };
300
+ if (m && m[2]) return { name: unquoteDisplayName(m[1]), email: m[2] };
274
301
  return { email: from.trim() };
275
302
  }
276
303
 
304
+ function unquoteDisplayName(name: string): string | undefined {
305
+ const trimmed = name.trim();
306
+ const unquoted =
307
+ trimmed.startsWith('"') && trimmed.endsWith('"')
308
+ ? trimmed.slice(1, -1).replace(/\\(.)/g, "$1")
309
+ : trimmed;
310
+ return unquoted || undefined;
311
+ }
312
+
277
313
  function stripHtml(html: string): string {
278
314
  return html
279
315
  .replace(/<br\s*\/?>/gi, "\n")
@@ -8,6 +8,7 @@ import { renderEmail, emailStrong } from "../../server/email-template.js";
8
8
  import { sendEmail, isEmailConfigured } from "../../server/email.js";
9
9
  import { invalidateCollabAccessCache } from "../../server/poll.js";
10
10
  import { getRequestUserEmail } from "../../server/request-context.js";
11
+ import { getUserProfile } from "../../user-profile/store.js";
11
12
  import { assertAccess, ForbiddenError } from "../access.js";
12
13
  import { requireShareableResource } from "../registry.js";
13
14
  import {
@@ -298,10 +299,9 @@ export default defineAction({
298
299
  const appName =
299
300
  process.env.APP_NAME || process.env.VITE_APP_NAME || "Agent Native";
300
301
  let brandLogoUrl: string | undefined;
301
- if (reg.getShareEmailLogoUrl) {
302
+ if (reg.getLogoUrl) {
302
303
  try {
303
- brandLogoUrl =
304
- (await reg.getShareEmailLogoUrl(resource)) ?? undefined;
304
+ brandLogoUrl = (await reg.getLogoUrl(resource)) ?? undefined;
305
305
  } catch (err) {
306
306
  console.error(
307
307
  "[share-resource] brand logo resolver failed; using default logo:",
@@ -309,9 +309,51 @@ export default defineAction({
309
309
  );
310
310
  }
311
311
  }
312
+ let brandName = appName;
313
+ if (reg.getBrandName) {
314
+ try {
315
+ brandName = (await reg.getBrandName(resource))?.trim() || appName;
316
+ } catch (err) {
317
+ console.error(
318
+ "[share-resource] brand name resolver failed; using app name:",
319
+ err,
320
+ );
321
+ }
322
+ }
323
+ let fromName: string | undefined;
324
+ let replyTo: string | undefined;
325
+ if (reg.getSender) {
326
+ try {
327
+ const sender = await reg.getSender(resource, {
328
+ sender: await getUserProfile(actor),
329
+ });
330
+ fromName = sender?.fromName?.trim() || undefined;
331
+ replyTo = sender?.replyTo?.trim() || undefined;
332
+ } catch (err) {
333
+ console.error(
334
+ "[share-resource] sender resolver failed; using default sender:",
335
+ err,
336
+ );
337
+ }
338
+ }
339
+ let heroHtml: string | undefined;
340
+ if (reg.getHeroHtml) {
341
+ try {
342
+ heroHtml =
343
+ (await reg.getHeroHtml(resource, {
344
+ href: notificationUrl,
345
+ alt: resourceTitle,
346
+ })) ?? undefined;
347
+ } catch (err) {
348
+ console.error(
349
+ "[share-resource] hero html resolver failed; omitting preview:",
350
+ err,
351
+ );
352
+ }
353
+ }
312
354
  const subject = `${actor} shared "${resourceTitle}" with you on ${appName}`;
313
355
  const { html, text } = renderEmail({
314
- brandName: appName,
356
+ brandName,
315
357
  brandLogoUrl,
316
358
  preheader: subject,
317
359
  heading: "You've been given access",
@@ -319,10 +361,18 @@ export default defineAction({
319
361
  `${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,
320
362
  `Use the button below to open it. If prompted, sign in with ${emailStrong(principalId)}.`,
321
363
  ],
364
+ heroHtml,
322
365
  cta: { label: `Open ${reg.displayName}`, url: notificationUrl },
323
366
  footer: `You received this because ${actor} granted you ${args.role} access.`,
324
367
  });
325
- await sendEmail({ to: principalId, subject, html, text });
368
+ await sendEmail({
369
+ to: principalId,
370
+ subject,
371
+ html,
372
+ text,
373
+ fromName,
374
+ replyTo,
375
+ });
326
376
  } catch (err) {
327
377
  console.error(
328
378
  "[share-resource] failed to send share notification:",
@@ -17,6 +17,8 @@
17
17
  * });
18
18
  */
19
19
 
20
+ import type { UserProfile } from "../user-profile/shared.js";
21
+
20
22
  export interface ShareableResourceRegistration {
21
23
  /** Stable identifier used across actions, UI, and analytics. e.g. "document". */
22
24
  type: string;
@@ -42,8 +44,46 @@ export interface ShareableResourceRegistration {
42
44
  * (e.g. the sharing org's own logo), or undefined to fall back. Receives the
43
45
  * resource row being shared so the template can scope the logo to its org.
44
46
  */
45
- getShareEmailLogoUrl?: (
47
+ getLogoUrl?: (
48
+ resource: any,
49
+ ) => string | undefined | Promise<string | undefined>;
50
+ /**
51
+ * Optional resolver for the brand name shown beside the logo in the
52
+ * share-notification email header. Return a display name (e.g. the sharing
53
+ * org's name) to override the app name, or undefined to keep the default.
54
+ * Receives the resource row being shared.
55
+ */
56
+ getBrandName?: (
57
+ resource: any,
58
+ ) => string | undefined | Promise<string | undefined>;
59
+ /**
60
+ * Optional resolver for who the share-notification email appears to come
61
+ * from. `fromName` changes only the display name and keeps the configured,
62
+ * domain-verified sending address (e.g. "Alice via Clips"); `replyTo` routes
63
+ * replies to the sharer. `ctx.sender` is the account doing the sharing, so the
64
+ * template decides how to present them (name, email, avatar).
65
+ *
66
+ * Do not put a user's address in the sending address itself — SPF/DKIM sign
67
+ * the app's domain, so recipients would bounce or spam-filter it.
68
+ */
69
+ getSender?: (
70
+ resource: any,
71
+ ctx: { sender: UserProfile },
72
+ ) =>
73
+ | { fromName?: string; replyTo?: string }
74
+ | undefined
75
+ | Promise<{ fromName?: string; replyTo?: string } | undefined>;
76
+ /**
77
+ * Optional resolver for a preview block shown above the CTA in the
78
+ * share-notification email — e.g. a recording thumbnail with a play badge.
79
+ * Return trusted HTML (built by template code, dynamic values escaped) that
80
+ * is injected verbatim, or undefined to omit it. `ctx.href` is the resolved
81
+ * link to the resource; `ctx.alt` is its title. The template owns the entire
82
+ * preview markup so the generic share action stays app-agnostic.
83
+ */
84
+ getHeroHtml?: (
46
85
  resource: any,
86
+ ctx: { href: string; alt?: string },
47
87
  ) => string | undefined | Promise<string | undefined>;
48
88
  /**
49
89
  * Drizzle DB accessor from the template's server/db/index.ts. Required —
@@ -1,9 +1,11 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import { z } from "zod";
3
4
 
4
5
  import { IMAGE_MODELS, IMAGE_QUALITY_TIERS } from "../shared/api.js";
5
6
  import { getAssetOrThrow } from "./_helpers.js";
6
7
  import generateImage from "./generate-image.js";
8
+ import { resolveLiveBatchContinuation } from "./variant-slots.js";
7
9
 
8
10
  export default defineAction({
9
11
  description:
@@ -24,26 +26,37 @@ export default defineAction({
24
26
  contextModeOverride: z.literal("off").optional(),
25
27
  }),
26
28
  parallelSafe: true,
27
- run: async (args) => {
29
+ run: async (args, context?: ActionRunContext) => {
28
30
  const asset = await getAssetOrThrow(args.assetId);
29
- return generateImage.run({
31
+ const continuation = await resolveLiveBatchContinuation({
32
+ threadId: context?.threadId,
30
33
  libraryId: asset.libraryId,
31
- collectionId: asset.collectionId ?? undefined,
32
- prompt: args.instruction,
33
- aspectRatio: (asset.aspectRatio ?? "16:9") as any,
34
- imageSize: (asset.imageSize ?? "2K") as any,
35
- model: args.model,
36
- tier: args.tier,
37
- intent: "edit",
38
- styleStrength: "balanced",
39
- referenceAssetIds: [],
40
- includeLogo: false,
41
- groundingMode: "off",
42
- subjectAssetId: asset.id,
43
- slotId: args.slotId,
44
- source: args.source,
45
- callerAppId: args.callerAppId,
46
- contextModeOverride: args.contextModeOverride,
47
34
  });
35
+ return generateImage.run(
36
+ {
37
+ libraryId: asset.libraryId,
38
+ collectionId:
39
+ asset.collectionId ?? continuation?.collectionId ?? undefined,
40
+ presetId: continuation?.presetId ?? undefined,
41
+ sessionId: continuation?.sessionId ?? undefined,
42
+ prompt: args.instruction,
43
+ aspectRatio: (asset.aspectRatio ?? "16:9") as any,
44
+ imageSize: (asset.imageSize ?? "2K") as any,
45
+ model: args.model,
46
+ tier: args.tier,
47
+ intent: "edit",
48
+ styleStrength: "balanced",
49
+ referenceAssetIds: [],
50
+ includeLogo: false,
51
+ groundingMode: "off",
52
+ subjectAssetId: asset.id,
53
+ slotId: args.slotId,
54
+ variantBatchId: continuation?.variantBatchId,
55
+ source: args.source,
56
+ callerAppId: args.callerAppId,
57
+ contextModeOverride: args.contextModeOverride,
58
+ },
59
+ context,
60
+ );
48
61
  },
49
62
  });
@@ -1,4 +1,5 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import { z } from "zod";
3
4
 
4
5
  import { parseJson } from "../server/lib/json.js";
@@ -8,6 +9,7 @@ import {
8
9
  requireGenerationSessionInLibrary,
9
10
  } from "./_helpers.js";
10
11
  import generateImage from "./generate-image.js";
12
+ import { resolveLiveBatchContinuation } from "./variant-slots.js";
11
13
 
12
14
  export default defineAction({
13
15
  description:
@@ -31,19 +33,22 @@ export default defineAction({
31
33
  contextModeOverride: z.literal("off").optional(),
32
34
  }),
33
35
  parallelSafe: true,
34
- run: async ({
35
- assetId,
36
- feedback,
37
- presetId,
38
- sessionId,
39
- model,
40
- aspectRatio,
41
- imageSize,
42
- slotId,
43
- source,
44
- callerAppId,
45
- contextModeOverride,
46
- }) => {
36
+ run: async (
37
+ {
38
+ assetId,
39
+ feedback,
40
+ presetId,
41
+ sessionId,
42
+ model,
43
+ aspectRatio,
44
+ imageSize,
45
+ slotId,
46
+ source,
47
+ callerAppId,
48
+ contextModeOverride,
49
+ },
50
+ context?: ActionRunContext,
51
+ ) => {
47
52
  const asset = await getAssetOrThrow(assetId);
48
53
  if (sessionId) {
49
54
  await requireGenerationSessionInLibrary(sessionId, asset.libraryId);
@@ -57,23 +62,32 @@ export default defineAction({
57
62
  "",
58
63
  "Preserve the strongest successful parts of the prior candidate unless the feedback contradicts them.",
59
64
  ].join("\n");
60
- return generateImage.run({
65
+ const continuation = await resolveLiveBatchContinuation({
66
+ threadId: context?.threadId,
61
67
  libraryId: asset.libraryId,
62
- collectionId: asset.collectionId ?? undefined,
63
- presetId,
64
- sessionId,
65
- prompt,
66
- aspectRatio: (aspectRatio ?? asset.aspectRatio ?? "16:9") as any,
67
- imageSize: (imageSize ?? asset.imageSize ?? "2K") as any,
68
- model: (model ?? asset.model ?? "gemini-3.1-flash-image") as any,
69
- categories: metadata.category ? [metadata.category] : undefined,
70
- includeLogo: false,
71
- groundingMode: "auto",
72
- sourceAssetId: asset.id,
73
- slotId,
74
- source,
75
- callerAppId,
76
- contextModeOverride,
77
68
  });
69
+ return generateImage.run(
70
+ {
71
+ libraryId: asset.libraryId,
72
+ collectionId:
73
+ asset.collectionId ?? continuation?.collectionId ?? undefined,
74
+ presetId: presetId ?? continuation?.presetId ?? undefined,
75
+ sessionId: sessionId ?? continuation?.sessionId ?? undefined,
76
+ prompt,
77
+ aspectRatio: (aspectRatio ?? asset.aspectRatio ?? "16:9") as any,
78
+ imageSize: (imageSize ?? asset.imageSize ?? "2K") as any,
79
+ model: (model ?? asset.model ?? "gemini-3.1-flash-image") as any,
80
+ categories: metadata.category ? [metadata.category] : undefined,
81
+ includeLogo: false,
82
+ groundingMode: "auto",
83
+ sourceAssetId: asset.id,
84
+ slotId,
85
+ variantBatchId: continuation?.variantBatchId,
86
+ source,
87
+ callerAppId,
88
+ contextModeOverride,
89
+ },
90
+ context,
91
+ );
78
92
  },
79
93
  });
@@ -1,4 +1,5 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import { z } from "zod";
3
4
 
4
5
  import {
@@ -10,6 +11,7 @@ import {
10
11
  } from "../shared/api.js";
11
12
  import { getAssetOrThrow } from "./_helpers.js";
12
13
  import generateImage from "./generate-image.js";
14
+ import { resolveLiveBatchContinuation } from "./variant-slots.js";
13
15
 
14
16
  export default defineAction({
15
17
  description:
@@ -38,30 +40,39 @@ export default defineAction({
38
40
  contextModeOverride: z.literal("off").optional(),
39
41
  }),
40
42
  parallelSafe: true,
41
- run: async (args) => {
43
+ run: async (args, context?: ActionRunContext) => {
42
44
  const subject = await getAssetOrThrow(args.subjectAssetId);
43
45
  const prompt =
44
46
  args.prompt?.trim() ||
45
47
  "Apply this library's brand style to the subject image while preserving the subject, pose, composition, and framing.";
46
- return generateImage.run({
48
+ const continuation = await resolveLiveBatchContinuation({
49
+ threadId: context?.threadId,
47
50
  libraryId: subject.libraryId,
48
- collectionId: subject.collectionId ?? undefined,
49
- presetId: args.presetId,
50
- sessionId: args.sessionId,
51
- prompt,
52
- aspectRatio: (args.aspectRatio ?? subject.aspectRatio ?? "16:9") as any,
53
- imageSize: (args.imageSize ?? subject.imageSize ?? "2K") as any,
54
- model: args.model,
55
- tier: args.tier,
56
- intent: "restyle",
57
- styleStrength: args.styleStrength,
58
- includeLogo: false,
59
- groundingMode: "auto",
60
- subjectAssetId: subject.id,
61
- slotId: args.slotId,
62
- source: args.source,
63
- callerAppId: args.callerAppId,
64
- contextModeOverride: args.contextModeOverride,
65
51
  });
52
+ return generateImage.run(
53
+ {
54
+ libraryId: subject.libraryId,
55
+ collectionId:
56
+ subject.collectionId ?? continuation?.collectionId ?? undefined,
57
+ presetId: args.presetId ?? continuation?.presetId ?? undefined,
58
+ sessionId: args.sessionId ?? continuation?.sessionId ?? undefined,
59
+ prompt,
60
+ aspectRatio: (args.aspectRatio ?? subject.aspectRatio ?? "16:9") as any,
61
+ imageSize: (args.imageSize ?? subject.imageSize ?? "2K") as any,
62
+ model: args.model,
63
+ tier: args.tier,
64
+ intent: "restyle",
65
+ styleStrength: args.styleStrength,
66
+ includeLogo: false,
67
+ groundingMode: "auto",
68
+ subjectAssetId: subject.id,
69
+ slotId: args.slotId,
70
+ variantBatchId: continuation?.variantBatchId,
71
+ source: args.source,
72
+ callerAppId: args.callerAppId,
73
+ contextModeOverride: args.contextModeOverride,
74
+ },
75
+ context,
76
+ );
66
77
  },
67
78
  });
@@ -177,6 +177,32 @@ export async function readVariantState(
177
177
  return withVariantStateLock(() => readVariantStateUnlocked(scopeId));
178
178
  }
179
179
 
180
+ // Iterations (refine/edit/restyle) are a continuation of what is already on
181
+ // screen for this thread, not a new generation topic: they should append into
182
+ // the live tray instead of tripping the "new batch/run" reset in
183
+ // isSameVariantScope. Passing the current tray's batch/run id plus its
184
+ // collection/preset/session back through keeps every isSameVariantScope check
185
+ // satisfied.
186
+ export async function resolveLiveBatchContinuation(input: {
187
+ threadId?: string | null;
188
+ libraryId: string;
189
+ }): Promise<{
190
+ variantBatchId: string;
191
+ collectionId: string | null;
192
+ presetId: string | null;
193
+ sessionId: string | null;
194
+ } | null> {
195
+ if (!input.threadId) return null;
196
+ const state = await readVariantState(input.threadId);
197
+ if (!state || state.libraryId !== input.libraryId) return null;
198
+ return {
199
+ variantBatchId: state.batchId ?? state.runId,
200
+ collectionId: state.collectionId ?? null,
201
+ presetId: state.presetId ?? null,
202
+ sessionId: state.sessionId ?? null,
203
+ };
204
+ }
205
+
180
206
  export async function writeVariantState(
181
207
  state: AssetVariantState,
182
208
  scopeId?: string | null,