@agent-native/core 0.124.6 → 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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/server/email-template.ts +37 -1
- package/corpus/core/src/server/email.ts +40 -4
- package/corpus/core/src/sharing/actions/share-resource.ts +65 -2
- package/corpus/core/src/sharing/registry.ts +49 -0
- package/corpus/templates/assets/actions/edit-image.ts +31 -18
- package/corpus/templates/assets/actions/refine-image.ts +43 -29
- package/corpus/templates/assets/actions/restyle-image.ts +30 -19
- package/corpus/templates/assets/actions/variant-slots.ts +26 -0
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +85 -17
- package/corpus/templates/clips/changelog/2026-07-27-organization-brand-logos-now-upload-to-configured-file-stora.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-27-share-notification-emails-now-include-a-clickable-recording-.md +6 -0
- package/corpus/templates/clips/server/db/index.ts +45 -0
- package/corpus/templates/clips/server/lib/share-email-hero.ts +105 -0
- package/corpus/templates/clips/server/routes/api/media/index.post.ts +19 -31
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +100 -12
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +17 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +169 -22
- package/dist/collab/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/server/email-template.d.ts +12 -0
- package/dist/server/email-template.d.ts.map +1 -1
- package/dist/server/email-template.js +24 -1
- package/dist/server/email-template.js.map +1 -1
- package/dist/server/email.d.ts +7 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +31 -6
- package/dist/server/email.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/sharing/actions/share-resource.d.ts.map +1 -1
- package/dist/sharing/actions/share-resource.js +57 -2
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/sharing/registry.d.ts +46 -0
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/package.json +1 -1
- package/src/server/email-template.ts +37 -1
- package/src/server/email.ts +40 -4
- package/src/sharing/actions/share-resource.ts +65 -2
- package/src/sharing/registry.ts +49 -0
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.125.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- c7ed52a: Add an optional `brandLogoUrl` to `renderEmail` and a `getShareEmailLogoUrl` resolver on `registerShareableResource`, so share-notification emails show the sharing org's logo (absolute `https://`) when available and fall back to the embedded Agent Native logo otherwise.
|
|
16
|
+
|
|
3
17
|
## 0.124.6
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "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,10 +36,22 @@ 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. */
|
|
42
48
|
brandName?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Optional absolute `https://` logo URL shown in the brand header. When a
|
|
51
|
+
* valid URL is provided it replaces the default embedded Agent Native logo;
|
|
52
|
+
* anything else (missing, relative, non-https) falls back to that logo.
|
|
53
|
+
*/
|
|
54
|
+
brandLogoUrl?: string;
|
|
43
55
|
/**
|
|
44
56
|
* Optional brand hex color for the CTA button and inline links. Defaults to
|
|
45
57
|
* a monochrome near-white button with dark text.
|
|
@@ -74,10 +86,27 @@ function sanitizeHexColor(input: string | undefined): string | undefined {
|
|
|
74
86
|
return /^#[0-9a-fA-F]{6}$/.test(input) ? input : undefined;
|
|
75
87
|
}
|
|
76
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Only accept an absolute `https://` URL for the brand logo. Email clients drop
|
|
91
|
+
* relative and mixed-content images, and an unvalidated string in `src` is an
|
|
92
|
+
* injection surface — so anything else falls back to the embedded logo.
|
|
93
|
+
*/
|
|
94
|
+
function sanitizeLogoUrl(input: string | undefined): string | undefined {
|
|
95
|
+
if (!input) return undefined;
|
|
96
|
+
try {
|
|
97
|
+
return new URL(input).protocol === "https:" ? input : undefined;
|
|
98
|
+
} catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
export function renderEmail(args: RenderEmailArgs): RenderedEmail {
|
|
78
104
|
const preheader = args.preheader || "";
|
|
79
105
|
const brand = sanitizeHexColor(args.brandColor);
|
|
80
106
|
const brandName = args.brandName?.trim() || getAppName() || "Agent Native";
|
|
107
|
+
const logoSrc =
|
|
108
|
+
sanitizeLogoUrl(args.brandLogoUrl) ??
|
|
109
|
+
`cid:${AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID}`;
|
|
81
110
|
|
|
82
111
|
// Monochrome default: near-white button with dark text. Brand override:
|
|
83
112
|
// colored button with white text.
|
|
@@ -85,6 +114,12 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
|
|
|
85
114
|
const ctaFg = brand ? "#ffffff" : "#0a0a0c";
|
|
86
115
|
const linkColor = brand ?? "#a1a1aa";
|
|
87
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
|
+
|
|
88
123
|
const paragraphsHtml = args.paragraphs
|
|
89
124
|
.map(
|
|
90
125
|
(p) =>
|
|
@@ -115,7 +150,7 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
|
|
|
115
150
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="margin:0 0 28px 0; padding:0 0 24px 0; border-bottom:1px solid #27272a;">
|
|
116
151
|
<tr>
|
|
117
152
|
<td align="center">
|
|
118
|
-
<img src="
|
|
153
|
+
<img src="${escapeAttr(logoSrc)}" alt="${escapeAttr(brandName)}" width="28" height="28" style="display:inline-block; vertical-align:middle; width:28px; height:28px; margin:0 8px 0 0; border:0;" />
|
|
119
154
|
<span style="font-size:18px; line-height:28px; font-weight:600; color:#fafafa; vertical-align:middle;">${escapeHtml(brandName)}</span>
|
|
120
155
|
</td>
|
|
121
156
|
</tr>
|
|
@@ -151,6 +186,7 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
|
|
|
151
186
|
${escapeHtml(args.heading)}
|
|
152
187
|
</h1>
|
|
153
188
|
${paragraphsHtml}
|
|
189
|
+
${heroHtml}
|
|
154
190
|
${ctaHtml}
|
|
155
191
|
${footerHtml}
|
|
156
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
|
-
|
|
115
|
-
|
|
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]
|
|
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 {
|
|
@@ -297,19 +298,81 @@ export default defineAction({
|
|
|
297
298
|
);
|
|
298
299
|
const appName =
|
|
299
300
|
process.env.APP_NAME || process.env.VITE_APP_NAME || "Agent Native";
|
|
301
|
+
let brandLogoUrl: string | undefined;
|
|
302
|
+
if (reg.getLogoUrl) {
|
|
303
|
+
try {
|
|
304
|
+
brandLogoUrl = (await reg.getLogoUrl(resource)) ?? undefined;
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.error(
|
|
307
|
+
"[share-resource] brand logo resolver failed; using default logo:",
|
|
308
|
+
err,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
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
|
+
}
|
|
300
354
|
const subject = `${actor} shared "${resourceTitle}" with you on ${appName}`;
|
|
301
355
|
const { html, text } = renderEmail({
|
|
302
|
-
brandName
|
|
356
|
+
brandName,
|
|
357
|
+
brandLogoUrl,
|
|
303
358
|
preheader: subject,
|
|
304
359
|
heading: "You've been given access",
|
|
305
360
|
paragraphs: [
|
|
306
361
|
`${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,
|
|
307
362
|
`Use the button below to open it. If prompted, sign in with ${emailStrong(principalId)}.`,
|
|
308
363
|
],
|
|
364
|
+
heroHtml,
|
|
309
365
|
cta: { label: `Open ${reg.displayName}`, url: notificationUrl },
|
|
310
366
|
footer: `You received this because ${actor} granted you ${args.role} access.`,
|
|
311
367
|
});
|
|
312
|
-
await sendEmail({
|
|
368
|
+
await sendEmail({
|
|
369
|
+
to: principalId,
|
|
370
|
+
subject,
|
|
371
|
+
html,
|
|
372
|
+
text,
|
|
373
|
+
fromName,
|
|
374
|
+
replyTo,
|
|
375
|
+
});
|
|
313
376
|
} catch (err) {
|
|
314
377
|
console.error(
|
|
315
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;
|
|
@@ -36,6 +38,53 @@ export interface ShareableResourceRegistration {
|
|
|
36
38
|
* when the caller does not pass a more specific resourceUrl.
|
|
37
39
|
*/
|
|
38
40
|
getResourcePath?: (resource: any) => string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Optional resolver for the share-notification email's brand logo. Return an
|
|
43
|
+
* absolute `https://` URL to override the default embedded Agent Native logo
|
|
44
|
+
* (e.g. the sharing org's own logo), or undefined to fall back. Receives the
|
|
45
|
+
* resource row being shared so the template can scope the logo to its org.
|
|
46
|
+
*/
|
|
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?: (
|
|
85
|
+
resource: any,
|
|
86
|
+
ctx: { href: string; alt?: string },
|
|
87
|
+
) => string | undefined | Promise<string | undefined>;
|
|
39
88
|
/**
|
|
40
89
|
* Drizzle DB accessor from the template's server/db/index.ts. Required —
|
|
41
90
|
* the framework-level share actions and access helpers call this to reach
|
|
@@ -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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|