@agent-native/core 0.78.0 → 0.78.2
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 +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +269 -237
- package/corpus/core/src/agent/types.ts +22 -0
- package/corpus/core/src/client/AgentPanel.tsx +8 -0
- package/corpus/core/src/client/AssistantChat.tsx +40 -0
- package/corpus/core/src/client/agent-chat.ts +173 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +316 -60
- package/corpus/core/src/client/composer/extensions/MentionReference.tsx +3 -0
- package/corpus/core/src/client/composer/types.ts +22 -0
- package/corpus/core/src/client/index.ts +5 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
- package/corpus/core/src/server/request-context.ts +8 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +13 -0
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +10 -2
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +14 -7
- package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +12 -0
- package/corpus/templates/assets/AGENTS.md +18 -3
- package/corpus/templates/assets/actions/_image-model-default.ts +24 -0
- package/corpus/templates/assets/actions/dismiss-variant-slots.ts +15 -22
- package/corpus/templates/assets/actions/generate-asset.ts +30 -23
- package/corpus/templates/assets/actions/generate-image-batch.ts +76 -29
- package/corpus/templates/assets/actions/generate-image.ts +46 -30
- package/corpus/templates/assets/actions/generate-video.ts +18 -3
- package/corpus/templates/assets/actions/list-assets.ts +39 -8
- package/corpus/templates/assets/actions/list-libraries.ts +24 -2
- package/corpus/templates/assets/actions/navigate.ts +1 -1
- package/corpus/templates/assets/actions/open-asset-picker.ts +38 -1
- package/corpus/templates/assets/actions/refresh-generation-run.ts +10 -0
- package/corpus/templates/assets/actions/save-generated-image.ts +21 -19
- package/corpus/templates/assets/actions/variant-slots.ts +131 -28
- package/corpus/templates/assets/actions/view-screen.ts +9 -0
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +425 -0
- package/corpus/templates/assets/app/components/layout/Header.tsx +3 -1
- package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -6
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +47 -21
- package/corpus/templates/assets/app/i18n-data.ts +1815 -0
- package/corpus/templates/assets/app/lib/libraries.ts +6 -0
- package/corpus/templates/assets/app/lib/picker-chat-handoff.ts +71 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
- package/corpus/templates/assets/app/routes/audit.tsx +1 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1557 -2164
- package/corpus/templates/assets/app/routes/brand-kits._index.tsx +7 -191
- package/corpus/templates/assets/app/routes/brand-kits.tsx +1 -5
- package/corpus/templates/assets/app/routes/libraries.tsx +4 -4
- package/corpus/templates/assets/app/routes/library.$id.tsx +9 -7
- package/corpus/templates/assets/app/routes/library.tsx +1454 -175
- package/corpus/templates/assets/server/db/index.ts +1 -1
- package/corpus/templates/assets/server/lib/generation.ts +86 -15
- package/corpus/templates/assets/server/plugins/agent-chat.ts +196 -0
- package/corpus/templates/assets/server/plugins/db.ts +5 -0
- package/corpus/templates/assets/shared/api.ts +6 -3
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +50 -29
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/types.d.ts +21 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +15 -3
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +32 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +113 -0
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +2 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +228 -56
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/composer/extensions/MentionReference.d.ts.map +1 -1
- package/dist/client/composer/extensions/MentionReference.js +3 -0
- package/dist/client/composer/extensions/MentionReference.js.map +1 -1
- package/dist/client/composer/types.d.ts +21 -0
- package/dist/client/composer/types.d.ts.map +1 -1
- package/dist/client/composer/types.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +5 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/package.json +1 -1
|
@@ -32,9 +32,14 @@ import { serializeAsset, serializeGenerationRun } from "./_helpers.js";
|
|
|
32
32
|
|
|
33
33
|
export default defineAction({
|
|
34
34
|
description:
|
|
35
|
-
"Start an async Veo video generation run from
|
|
35
|
+
"Start an async Veo video generation run from a brand kit/library. Use a media-type @mention with refId video to choose this instead of image generation, and use a brand-kit @mention as libraryId. Poll the returned run with refresh-generation-run until it completes and creates a video asset.",
|
|
36
36
|
schema: z.object({
|
|
37
|
-
libraryId: z
|
|
37
|
+
libraryId: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe(
|
|
41
|
+
"Brand kit/library ID. Pass the refId from a brand-kit @mention, or choose a kit from view-screen/list-libraries.",
|
|
42
|
+
),
|
|
38
43
|
folderId: z.string().min(1).nullable().optional(),
|
|
39
44
|
collectionId: z.string().optional(),
|
|
40
45
|
prompt: z.string().min(1),
|
|
@@ -66,7 +71,17 @@ export default defineAction({
|
|
|
66
71
|
callerAppId: z.string().optional(),
|
|
67
72
|
waitForCompletion: z.coerce.boolean().default(false),
|
|
68
73
|
}),
|
|
69
|
-
run: async (
|
|
74
|
+
run: async (input) => {
|
|
75
|
+
const libraryId = input.libraryId;
|
|
76
|
+
if (!libraryId) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"No brand kit selected. Tag a brand kit with @ or pass libraryId.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const args = {
|
|
82
|
+
...input,
|
|
83
|
+
libraryId,
|
|
84
|
+
};
|
|
70
85
|
await assertAccess("asset-library", args.libraryId, "editor");
|
|
71
86
|
const db = getDb();
|
|
72
87
|
const [library] = await db
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
-
import {
|
|
2
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, desc, eq, inArray, isNull } from "drizzle-orm";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
|
|
5
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
@@ -17,9 +18,9 @@ import {
|
|
|
17
18
|
|
|
18
19
|
export default defineAction({
|
|
19
20
|
description:
|
|
20
|
-
"List DAM assets in
|
|
21
|
+
"List DAM assets in one library, or across all accessible libraries when libraryId is omitted. Optionally filter by folder, collection, media type, status, role, category, or text query.",
|
|
21
22
|
schema: z.object({
|
|
22
|
-
libraryId: z.string(),
|
|
23
|
+
libraryId: z.string().optional(),
|
|
23
24
|
collectionId: z.string().optional(),
|
|
24
25
|
folderId: z.string().nullable().optional(),
|
|
25
26
|
mediaType: z.enum(ASSET_MEDIA_TYPES).optional(),
|
|
@@ -51,8 +52,32 @@ export default defineAction({
|
|
|
51
52
|
includeCandidates,
|
|
52
53
|
candidateRunIds,
|
|
53
54
|
}) => {
|
|
54
|
-
|
|
55
|
-
const
|
|
55
|
+
const db = getDb();
|
|
56
|
+
const libraryRows = libraryId
|
|
57
|
+
? [await requireLibrary(libraryId)]
|
|
58
|
+
: await db
|
|
59
|
+
.select({
|
|
60
|
+
id: schema.assetLibraries.id,
|
|
61
|
+
title: schema.assetLibraries.title,
|
|
62
|
+
})
|
|
63
|
+
.from(schema.assetLibraries)
|
|
64
|
+
.where(
|
|
65
|
+
and(
|
|
66
|
+
accessFilter(schema.assetLibraries, schema.assetLibraryShares),
|
|
67
|
+
isNull(schema.assetLibraries.archivedAt),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
const libraryIds = libraryRows.map((library) => library.id);
|
|
71
|
+
if (!libraryIds.length) return { count: 0, assets: [] };
|
|
72
|
+
|
|
73
|
+
const libraryTitleById = new Map(
|
|
74
|
+
libraryRows.map((library) => [library.id, library.title]),
|
|
75
|
+
);
|
|
76
|
+
const filters = [
|
|
77
|
+
libraryId
|
|
78
|
+
? eq(schema.assets.libraryId, libraryId)
|
|
79
|
+
: inArray(schema.assets.libraryId, libraryIds),
|
|
80
|
+
];
|
|
56
81
|
if (collectionId)
|
|
57
82
|
filters.push(eq(schema.assets.collectionId, collectionId));
|
|
58
83
|
if (folderId !== undefined) {
|
|
@@ -67,7 +92,6 @@ export default defineAction({
|
|
|
67
92
|
if (role) filters.push(eq(schema.assets.role, role));
|
|
68
93
|
const normalizedQuery = query?.trim().toLowerCase();
|
|
69
94
|
const candidateRunIdSet = new Set(candidateRunIds ?? []);
|
|
70
|
-
const db = getDb();
|
|
71
95
|
const [rows, lineageRows] = await Promise.all([
|
|
72
96
|
db
|
|
73
97
|
.select()
|
|
@@ -89,7 +113,11 @@ export default defineAction({
|
|
|
89
113
|
createdAt: schema.assets.createdAt,
|
|
90
114
|
})
|
|
91
115
|
.from(schema.assets)
|
|
92
|
-
.where(
|
|
116
|
+
.where(
|
|
117
|
+
libraryId
|
|
118
|
+
? eq(schema.assets.libraryId, libraryId)
|
|
119
|
+
: inArray(schema.assets.libraryId, libraryIds),
|
|
120
|
+
),
|
|
93
121
|
]);
|
|
94
122
|
const lineageById = buildAssetLineage(lineageRows);
|
|
95
123
|
const assets = rows
|
|
@@ -111,7 +139,10 @@ export default defineAction({
|
|
|
111
139
|
);
|
|
112
140
|
})
|
|
113
141
|
.filter((asset) => assetMatchesSearch(asset, normalizedQuery, category))
|
|
114
|
-
.map((asset) =>
|
|
142
|
+
.map((asset) => ({
|
|
143
|
+
...serializeAsset(asset, lineageById.get(asset.id) ?? null),
|
|
144
|
+
libraryTitle: libraryTitleById.get(asset.libraryId) ?? null,
|
|
145
|
+
}));
|
|
115
146
|
return { count: assets.length, assets };
|
|
116
147
|
},
|
|
117
148
|
});
|
|
@@ -67,10 +67,11 @@ export default defineAction({
|
|
|
67
67
|
"List asset libraries accessible to the current user, including counts and preview thumbnails.",
|
|
68
68
|
schema: z.object({
|
|
69
69
|
compact: z.coerce.boolean().optional(),
|
|
70
|
+
includeFolders: z.coerce.boolean().optional(),
|
|
70
71
|
}),
|
|
71
72
|
http: { method: "GET" },
|
|
72
73
|
readOnly: true,
|
|
73
|
-
run: async ({ compact }) => {
|
|
74
|
+
run: async ({ compact, includeFolders }) => {
|
|
74
75
|
const db = getDb();
|
|
75
76
|
const rows = await db
|
|
76
77
|
.select()
|
|
@@ -93,8 +94,23 @@ export default defineAction({
|
|
|
93
94
|
),
|
|
94
95
|
)
|
|
95
96
|
: [];
|
|
97
|
+
const folders =
|
|
98
|
+
includeFolders && rows.length
|
|
99
|
+
? await db
|
|
100
|
+
.select()
|
|
101
|
+
.from(schema.assetFolders)
|
|
102
|
+
.where(
|
|
103
|
+
inArray(
|
|
104
|
+
schema.assetFolders.libraryId,
|
|
105
|
+
rows.map((row) => row.id),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
: [];
|
|
96
109
|
const libraries = rows.map((row) => {
|
|
97
110
|
const libAssets = assets.filter((asset) => asset.libraryId === row.id);
|
|
111
|
+
const libFolders = includeFolders
|
|
112
|
+
? folders.filter((folder) => folder.libraryId === row.id)
|
|
113
|
+
: undefined;
|
|
98
114
|
const visibleAssets = libAssets.filter((asset) =>
|
|
99
115
|
shouldIncludeAssetInLibraryResults(asset),
|
|
100
116
|
);
|
|
@@ -106,7 +122,12 @@ export default defineAction({
|
|
|
106
122
|
const previewAssets = uniqueAssets([cover, ...imageAssets]).slice(0, 4);
|
|
107
123
|
const base = serializeLibrary(row);
|
|
108
124
|
return compact
|
|
109
|
-
? {
|
|
125
|
+
? {
|
|
126
|
+
id: base.id,
|
|
127
|
+
title: base.title,
|
|
128
|
+
description: base.description,
|
|
129
|
+
...(includeFolders ? { folders: libFolders } : {}),
|
|
130
|
+
}
|
|
110
131
|
: {
|
|
111
132
|
...base,
|
|
112
133
|
referenceCount: visibleAssets.filter((asset) =>
|
|
@@ -122,6 +143,7 @@ export default defineAction({
|
|
|
122
143
|
).length,
|
|
123
144
|
coverAsset: cover ? serializeAsset(cover) : null,
|
|
124
145
|
previewAssets: previewAssets.map(serializeAsset),
|
|
146
|
+
...(includeFolders ? { folders: libFolders } : {}),
|
|
125
147
|
};
|
|
126
148
|
});
|
|
127
149
|
return { count: libraries.length, libraries };
|
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
|
|
5
5
|
export default defineAction({
|
|
6
6
|
description:
|
|
7
|
-
"Navigate the Assets UI. Views (internal keys, with the surface they open): create, picker (the image Library
|
|
7
|
+
"Navigate the Assets UI. Views (internal keys, with the surface they open): create, picker (the embedded image Library picker), libraries (the unified Library workspace), library (a single Brand Kit inside Library), asset, generation-session, generation-run, extensions, audit, settings. Use threadId to open a specific create/chat thread; use libraryId, assetId, sessionId, runId, or extensionId where appropriate.",
|
|
8
8
|
schema: z.object({
|
|
9
9
|
view: z
|
|
10
10
|
.enum([
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { defineAction, embedApp } from "@agent-native/core";
|
|
2
|
+
import type { ActionRunContext } from "@agent-native/core/action";
|
|
3
|
+
import { writeAppState } from "@agent-native/core/application-state";
|
|
4
|
+
import { getRequestRunContext } from "@agent-native/core/server/request-context";
|
|
2
5
|
import { z } from "zod";
|
|
3
6
|
|
|
4
7
|
import { IMAGE_QUALITY_TIERS, STYLE_STRENGTHS } from "../shared/api.js";
|
|
@@ -77,6 +80,7 @@ const schema = z.object({
|
|
|
77
80
|
});
|
|
78
81
|
|
|
79
82
|
type OpenAssetPickerArgs = z.infer<typeof schema>;
|
|
83
|
+
const SAFE_BROWSER_TAB_ID_RE = /^[A-Za-z0-9_-]{1,96}$/;
|
|
80
84
|
|
|
81
85
|
type ActionWithToolParameters = {
|
|
82
86
|
tool: { parameters?: { properties?: Record<string, any> } };
|
|
@@ -109,6 +113,7 @@ const FALLBACK_INSTRUCTIONS =
|
|
|
109
113
|
|
|
110
114
|
function pickerPath(args: Partial<OpenAssetPickerArgs>): string {
|
|
111
115
|
const params = new URLSearchParams();
|
|
116
|
+
params.set("__an_picker", "1");
|
|
112
117
|
params.set("mediaType", args.mediaType ?? "image");
|
|
113
118
|
if (args.prompt?.trim()) params.set("prompt", args.prompt.trim());
|
|
114
119
|
if (args.query?.trim()) params.set("q", args.query.trim());
|
|
@@ -134,6 +139,21 @@ function pickerPath(args: Partial<OpenAssetPickerArgs>): string {
|
|
|
134
139
|
return `/library?${params.toString()}`;
|
|
135
140
|
}
|
|
136
141
|
|
|
142
|
+
function navigateCommandKey(): string | null {
|
|
143
|
+
const browserTabId = getRequestRunContext()?.browserTabId?.trim();
|
|
144
|
+
if (browserTabId && SAFE_BROWSER_TAB_ID_RE.test(browserTabId)) {
|
|
145
|
+
return `navigate:${browserTabId}`;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function shouldWriteNavigateCommand(
|
|
151
|
+
context: ActionRunContext | undefined,
|
|
152
|
+
commandKey: string | null,
|
|
153
|
+
): commandKey is string {
|
|
154
|
+
return Boolean(commandKey) && context?.caller !== "mcp";
|
|
155
|
+
}
|
|
156
|
+
|
|
137
157
|
const action = defineAction({
|
|
138
158
|
description:
|
|
139
159
|
'Open the image Library picker inline so a person can browse, search, generate, and select an image or video asset. When the user asks to create a specific image and choose the best one, pass prompt, autoGenerate: true, and count: 3 so the Library opens with generated candidates. If the host can only open a browser link (e.g. a CLI or code editor), surface that link: after the user picks, the page auto-copies a short paste-back summary — or the user can simply tell you which candidate they want (e.g. "use image A"). Use search-assets, generate-image, generate-video, and export-asset for unattended flows.',
|
|
@@ -173,8 +193,25 @@ const action = defineAction({
|
|
|
173
193
|
view: "picker",
|
|
174
194
|
};
|
|
175
195
|
},
|
|
176
|
-
run: async (args) => {
|
|
196
|
+
run: async (args, context) => {
|
|
177
197
|
const path = pickerPath(args);
|
|
198
|
+
const commandKey = navigateCommandKey();
|
|
199
|
+
if (shouldWriteNavigateCommand(context, commandKey)) {
|
|
200
|
+
const command = {
|
|
201
|
+
view: "picker" as const,
|
|
202
|
+
mediaType: args.mediaType,
|
|
203
|
+
path,
|
|
204
|
+
libraryId: args.libraryId ?? null,
|
|
205
|
+
query: args.query ?? null,
|
|
206
|
+
prompt: args.prompt ?? null,
|
|
207
|
+
aspectRatio: args.aspectRatio ?? null,
|
|
208
|
+
presetId: args.presetId ?? null,
|
|
209
|
+
_writeId: `open-asset-picker-${Date.now()}-${Math.random()
|
|
210
|
+
.toString(36)
|
|
211
|
+
.slice(2, 8)}`,
|
|
212
|
+
};
|
|
213
|
+
await writeAppState(commandKey, command);
|
|
214
|
+
}
|
|
178
215
|
return {
|
|
179
216
|
app: "assets",
|
|
180
217
|
view: "picker",
|
|
@@ -36,6 +36,14 @@ async function syncImageVariantSlot(
|
|
|
36
36
|
typeof metadata.variantBatchId === "string" && metadata.variantBatchId
|
|
37
37
|
? metadata.variantBatchId
|
|
38
38
|
: null;
|
|
39
|
+
const threadId =
|
|
40
|
+
typeof metadata.threadId === "string" && metadata.threadId
|
|
41
|
+
? metadata.threadId
|
|
42
|
+
: null;
|
|
43
|
+
const variantScopeId =
|
|
44
|
+
typeof metadata.variantScopeId === "string" && metadata.variantScopeId
|
|
45
|
+
? metadata.variantScopeId
|
|
46
|
+
: null;
|
|
39
47
|
const serialized = options.asset ? serializeAsset(options.asset) : null;
|
|
40
48
|
|
|
41
49
|
await upsertVariantSlot({
|
|
@@ -45,6 +53,8 @@ async function syncImageVariantSlot(
|
|
|
45
53
|
collectionId: run.collectionId ?? null,
|
|
46
54
|
presetId: run.presetId ?? null,
|
|
47
55
|
sessionId: run.sessionId ?? null,
|
|
56
|
+
threadId,
|
|
57
|
+
variantScopeId,
|
|
48
58
|
prompt: run.prompt,
|
|
49
59
|
slotId,
|
|
50
60
|
status,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import type { ActionRunContext } from "@agent-native/core/action";
|
|
2
3
|
import {
|
|
3
4
|
deleteAppState,
|
|
4
5
|
readAppState,
|
|
@@ -7,28 +8,29 @@ import {
|
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
|
|
9
10
|
import { markAssetSaved } from "../server/handlers/assets.js";
|
|
10
|
-
import type { AssetVariantState } from "../shared/api.js";
|
|
11
11
|
import { getAssetOrThrow, serializeAsset } from "./_helpers.js";
|
|
12
|
+
import {
|
|
13
|
+
deleteVariantState,
|
|
14
|
+
readVariantState,
|
|
15
|
+
writeVariantState,
|
|
16
|
+
} from "./variant-slots.js";
|
|
12
17
|
|
|
13
18
|
export default defineAction({
|
|
14
19
|
description:
|
|
15
|
-
"Save a generated asset candidate to the library. Accepts an assetId directly or a variant slot ID from application_state.asset-variants.",
|
|
20
|
+
"Save a generated asset candidate to the library. Accepts an assetId directly or a variant slot ID from the current thread's application_state.asset-variants.",
|
|
16
21
|
schema: z.object({
|
|
17
22
|
assetId: z.string().optional(),
|
|
18
23
|
slotId: z.string().optional(),
|
|
19
24
|
folderId: z.string().min(1).nullable().optional(),
|
|
25
|
+
threadId: z.string().nullable().optional(),
|
|
20
26
|
}),
|
|
21
|
-
run: async (
|
|
27
|
+
run: async (
|
|
28
|
+
{ assetId, slotId, folderId, threadId },
|
|
29
|
+
context?: ActionRunContext,
|
|
30
|
+
) => {
|
|
31
|
+
const effectiveThreadId = threadId ?? context?.threadId ?? null;
|
|
22
32
|
let resolvedAssetId = assetId;
|
|
23
|
-
const
|
|
24
|
-
const legacyRaw =
|
|
25
|
-
raw ??
|
|
26
|
-
((await readAppState("image-variants").catch(() => null)) as
|
|
27
|
-
| unknown
|
|
28
|
-
| null);
|
|
29
|
-
const variants = (raw ?? null) as AssetVariantState | null;
|
|
30
|
-
const legacyVariants = (legacyRaw ?? null) as AssetVariantState | null;
|
|
31
|
-
const activeVariants = variants ?? legacyVariants;
|
|
33
|
+
const activeVariants = await readVariantState(effectiveThreadId);
|
|
32
34
|
if (!resolvedAssetId && slotId && activeVariants) {
|
|
33
35
|
resolvedAssetId = activeVariants.slots.find(
|
|
34
36
|
(slot) => slot.slotId === slotId,
|
|
@@ -39,18 +41,18 @@ export default defineAction({
|
|
|
39
41
|
await markAssetSaved(resolvedAssetId, folderId);
|
|
40
42
|
const asset = await getAssetOrThrow(resolvedAssetId);
|
|
41
43
|
if (activeVariants) {
|
|
44
|
+
const stateThreadId =
|
|
45
|
+
effectiveThreadId ??
|
|
46
|
+
activeVariants.variantScopeId ??
|
|
47
|
+
activeVariants.threadId ??
|
|
48
|
+
null;
|
|
42
49
|
activeVariants.slots = activeVariants.slots.filter(
|
|
43
50
|
(slot) => slot.assetId !== resolvedAssetId,
|
|
44
51
|
);
|
|
45
52
|
if (activeVariants.slots.length) {
|
|
46
|
-
await
|
|
47
|
-
"asset-variants",
|
|
48
|
-
activeVariants as unknown as Record<string, unknown>,
|
|
49
|
-
);
|
|
50
|
-
await deleteAppState("image-variants").catch(() => {});
|
|
53
|
+
await writeVariantState(activeVariants, stateThreadId);
|
|
51
54
|
} else {
|
|
52
|
-
await
|
|
53
|
-
await deleteAppState("image-variants").catch(() => {});
|
|
55
|
+
await deleteVariantState(stateThreadId);
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
return serializeAsset({ ...asset, status: "saved" });
|
|
@@ -7,13 +7,21 @@ import {
|
|
|
7
7
|
import { nowIso } from "../server/lib/json.js";
|
|
8
8
|
import type { AssetVariantState } from "../shared/api.js";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const GLOBAL_VARIANT_STATE_KEY = "asset-variants";
|
|
11
|
+
const LEGACY_VARIANT_STATE_KEY = "image-variants";
|
|
12
|
+
|
|
13
|
+
type VariantScopeInput = {
|
|
11
14
|
runId: string;
|
|
12
15
|
batchId?: string | null;
|
|
13
16
|
libraryId: string;
|
|
14
17
|
collectionId?: string | null;
|
|
15
18
|
presetId?: string | null;
|
|
16
19
|
sessionId?: string | null;
|
|
20
|
+
threadId?: string | null;
|
|
21
|
+
variantScopeId?: string | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type VariantSlotInput = VariantScopeInput & {
|
|
17
25
|
prompt: string;
|
|
18
26
|
slotId: string;
|
|
19
27
|
status: "pending" | "ready" | "failed";
|
|
@@ -25,6 +33,29 @@ type VariantSlotInput = {
|
|
|
25
33
|
|
|
26
34
|
let variantStateLock: Promise<void> = Promise.resolve();
|
|
27
35
|
|
|
36
|
+
export function variantStateKey(scopeId?: string | null) {
|
|
37
|
+
const scopedId = normalizeVariantScopeId(scopeId);
|
|
38
|
+
return scopedId
|
|
39
|
+
? `${GLOBAL_VARIANT_STATE_KEY}:${scopedId}`
|
|
40
|
+
: GLOBAL_VARIANT_STATE_KEY;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeVariantScopeId(value: unknown): string | null {
|
|
44
|
+
if (typeof value !== "string") return null;
|
|
45
|
+
const trimmed = value.trim();
|
|
46
|
+
return /^[A-Za-z0-9:_-]{1,160}$/.test(trimmed) ? trimmed : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function variantScopeIdFor(input: {
|
|
50
|
+
threadId?: string | null;
|
|
51
|
+
variantScopeId?: string | null;
|
|
52
|
+
}): string | null {
|
|
53
|
+
return (
|
|
54
|
+
normalizeVariantScopeId(input.variantScopeId) ??
|
|
55
|
+
normalizeVariantScopeId(input.threadId)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
28
59
|
export async function withVariantStateLock<T>(
|
|
29
60
|
operation: () => Promise<T>,
|
|
30
61
|
): Promise<T> {
|
|
@@ -44,9 +75,17 @@ export async function withVariantStateLock<T>(
|
|
|
44
75
|
export async function wasVariantSlotDismissed(
|
|
45
76
|
libraryId: string,
|
|
46
77
|
slotId: string,
|
|
78
|
+
scope?:
|
|
79
|
+
| { threadId?: string | null; variantScopeId?: string | null }
|
|
80
|
+
| string
|
|
81
|
+
| null,
|
|
47
82
|
): Promise<boolean> {
|
|
48
83
|
return withVariantStateLock(async () => {
|
|
49
|
-
const
|
|
84
|
+
const scopeId =
|
|
85
|
+
typeof scope === "object" && scope
|
|
86
|
+
? variantScopeIdFor(scope)
|
|
87
|
+
: normalizeVariantScopeId(scope);
|
|
88
|
+
const state = await readVariantStateUnlocked(scopeId);
|
|
50
89
|
if (!state) return true;
|
|
51
90
|
if (state.libraryId !== libraryId) return false;
|
|
52
91
|
return !state.slots.some((slot) => slot.slotId === slotId);
|
|
@@ -55,26 +94,32 @@ export async function wasVariantSlotDismissed(
|
|
|
55
94
|
|
|
56
95
|
export async function upsertVariantSlot(input: VariantSlotInput) {
|
|
57
96
|
await withVariantStateLock(async () => {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
const scopeId = variantScopeIdFor(input);
|
|
98
|
+
const previous = await readVariantStateUnlocked(scopeId);
|
|
99
|
+
const state =
|
|
100
|
+
previous && isSameVariantScope(previous, input)
|
|
101
|
+
? previous
|
|
102
|
+
: {
|
|
103
|
+
runId: input.runId,
|
|
104
|
+
batchId: input.batchId ?? null,
|
|
105
|
+
libraryId: input.libraryId,
|
|
106
|
+
collectionId: input.collectionId,
|
|
107
|
+
presetId: input.presetId ?? null,
|
|
108
|
+
sessionId: input.sessionId ?? null,
|
|
109
|
+
threadId: input.threadId ?? null,
|
|
110
|
+
variantScopeId: scopeId,
|
|
111
|
+
prompt: input.prompt,
|
|
112
|
+
slots: [],
|
|
113
|
+
updatedAt: nowIso(),
|
|
114
|
+
};
|
|
72
115
|
|
|
73
116
|
state.runId = input.runId;
|
|
74
117
|
state.batchId = input.batchId ?? null;
|
|
75
118
|
state.collectionId = input.collectionId ?? null;
|
|
76
119
|
state.presetId = input.presetId ?? null;
|
|
77
120
|
state.sessionId = input.sessionId ?? null;
|
|
121
|
+
state.threadId = input.threadId ?? null;
|
|
122
|
+
state.variantScopeId = scopeId;
|
|
78
123
|
state.prompt = input.prompt;
|
|
79
124
|
|
|
80
125
|
const now = nowIso();
|
|
@@ -97,14 +142,14 @@ export async function upsertVariantSlot(input: VariantSlotInput) {
|
|
|
97
142
|
else state.slots.push(nextSlot);
|
|
98
143
|
|
|
99
144
|
state.updatedAt = now;
|
|
100
|
-
await writeVariantStateUnlocked(state);
|
|
145
|
+
await writeVariantStateUnlocked(state, scopeId);
|
|
101
146
|
});
|
|
102
147
|
}
|
|
103
148
|
|
|
104
149
|
function isSameVariantScope(
|
|
105
150
|
previous: AssetVariantState | null,
|
|
106
|
-
input:
|
|
107
|
-
):
|
|
151
|
+
input: VariantScopeInput,
|
|
152
|
+
): boolean {
|
|
108
153
|
if (!previous) return false;
|
|
109
154
|
|
|
110
155
|
// The batch/run id is the generation boundary: batch slots may have distinct
|
|
@@ -122,20 +167,78 @@ function variantScopeId(input: { batchId?: string | null; runId: string }) {
|
|
|
122
167
|
return input.batchId ?? input.runId;
|
|
123
168
|
}
|
|
124
169
|
|
|
125
|
-
async function
|
|
126
|
-
|
|
170
|
+
export async function readVariantState(
|
|
171
|
+
scopeId?: string | null,
|
|
172
|
+
): Promise<AssetVariantState | null> {
|
|
173
|
+
return withVariantStateLock(() => readVariantStateUnlocked(scopeId));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function writeVariantState(
|
|
177
|
+
state: AssetVariantState,
|
|
178
|
+
scopeId?: string | null,
|
|
179
|
+
) {
|
|
180
|
+
await withVariantStateLock(() => writeVariantStateUnlocked(state, scopeId));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function deleteVariantState(scopeId?: string | null) {
|
|
184
|
+
await withVariantStateLock(() => deleteVariantStateUnlocked(scopeId));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function readVariantStateUnlocked(
|
|
188
|
+
scopeId?: string | null,
|
|
189
|
+
): Promise<AssetVariantState | null> {
|
|
190
|
+
const key = variantStateKey(scopeId);
|
|
191
|
+
const current = (await readAppState(key)) as unknown | null;
|
|
192
|
+
if (current) return current as AssetVariantState;
|
|
193
|
+
|
|
194
|
+
if (scopeId) {
|
|
195
|
+
const globalCurrent = (await readAppState(GLOBAL_VARIANT_STATE_KEY)) as
|
|
196
|
+
| unknown
|
|
197
|
+
| null;
|
|
198
|
+
const globalState = (globalCurrent ?? null) as AssetVariantState | null;
|
|
199
|
+
return globalState?.threadId === scopeId ||
|
|
200
|
+
globalState?.variantScopeId === scopeId
|
|
201
|
+
? globalState
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
204
|
+
|
|
127
205
|
const legacyCurrent =
|
|
128
206
|
current ??
|
|
129
|
-
((await readAppState(
|
|
207
|
+
((await readAppState(LEGACY_VARIANT_STATE_KEY).catch(() => null)) as
|
|
130
208
|
| unknown
|
|
131
209
|
| null);
|
|
132
210
|
return (legacyCurrent ?? null) as AssetVariantState | null;
|
|
133
211
|
}
|
|
134
212
|
|
|
135
|
-
async function writeVariantStateUnlocked(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
);
|
|
140
|
-
await
|
|
213
|
+
async function writeVariantStateUnlocked(
|
|
214
|
+
state: AssetVariantState,
|
|
215
|
+
scopeId?: string | null,
|
|
216
|
+
) {
|
|
217
|
+
const key = variantStateKey(scopeId);
|
|
218
|
+
await writeAppState(key, state as unknown as Record<string, unknown>);
|
|
219
|
+
if (key !== GLOBAL_VARIANT_STATE_KEY) {
|
|
220
|
+
await writeAppState(
|
|
221
|
+
GLOBAL_VARIANT_STATE_KEY,
|
|
222
|
+
state as unknown as Record<string, unknown>,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
await deleteAppState(LEGACY_VARIANT_STATE_KEY).catch(() => {});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function deleteVariantStateUnlocked(scopeId?: string | null) {
|
|
229
|
+
const key = variantStateKey(scopeId);
|
|
230
|
+
await deleteAppState(key);
|
|
231
|
+
if (key !== GLOBAL_VARIANT_STATE_KEY) {
|
|
232
|
+
const globalCurrent = (await readAppState(GLOBAL_VARIANT_STATE_KEY)) as
|
|
233
|
+
| unknown
|
|
234
|
+
| null;
|
|
235
|
+
const globalState = (globalCurrent ?? null) as AssetVariantState | null;
|
|
236
|
+
if (
|
|
237
|
+
globalState?.threadId === scopeId ||
|
|
238
|
+
globalState?.variantScopeId === scopeId
|
|
239
|
+
) {
|
|
240
|
+
await deleteAppState(GLOBAL_VARIANT_STATE_KEY);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
await deleteAppState(LEGACY_VARIANT_STATE_KEY).catch(() => {});
|
|
141
244
|
}
|
|
@@ -71,6 +71,15 @@ export default defineAction({
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
if (nav?.view === "library" && nav?.selection === "all") {
|
|
75
|
+
screen.libraries = await listLibraries.run({ compact: false });
|
|
76
|
+
screen.assets = await listAssets.run({
|
|
77
|
+
query:
|
|
78
|
+
typeof nav.search === "string" && nav.search.trim()
|
|
79
|
+
? nav.search
|
|
80
|
+
: undefined,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
74
83
|
if (nav?.view === "audit") {
|
|
75
84
|
screen.audit = await listAuditRuns.run({ limit: 20 });
|
|
76
85
|
}
|