@agent-native/core 0.78.0 → 0.78.1

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 (92) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +9 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/types.ts +22 -0
  5. package/corpus/core/src/client/AgentPanel.tsx +8 -0
  6. package/corpus/core/src/client/AssistantChat.tsx +40 -0
  7. package/corpus/core/src/client/agent-chat.ts +173 -0
  8. package/corpus/core/src/client/composer/TiptapComposer.tsx +316 -60
  9. package/corpus/core/src/client/composer/extensions/MentionReference.tsx +3 -0
  10. package/corpus/core/src/client/composer/types.ts +22 -0
  11. package/corpus/core/src/client/index.ts +5 -0
  12. package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
  13. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +10 -2
  14. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +14 -7
  15. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +12 -0
  16. package/corpus/templates/assets/AGENTS.md +18 -3
  17. package/corpus/templates/assets/actions/_image-model-default.ts +24 -0
  18. package/corpus/templates/assets/actions/dismiss-variant-slots.ts +15 -22
  19. package/corpus/templates/assets/actions/generate-asset.ts +30 -23
  20. package/corpus/templates/assets/actions/generate-image-batch.ts +76 -29
  21. package/corpus/templates/assets/actions/generate-image.ts +46 -30
  22. package/corpus/templates/assets/actions/generate-video.ts +18 -3
  23. package/corpus/templates/assets/actions/list-assets.ts +39 -8
  24. package/corpus/templates/assets/actions/list-libraries.ts +24 -2
  25. package/corpus/templates/assets/actions/navigate.ts +1 -1
  26. package/corpus/templates/assets/actions/open-asset-picker.ts +38 -1
  27. package/corpus/templates/assets/actions/refresh-generation-run.ts +10 -0
  28. package/corpus/templates/assets/actions/save-generated-image.ts +21 -19
  29. package/corpus/templates/assets/actions/variant-slots.ts +131 -28
  30. package/corpus/templates/assets/actions/view-screen.ts +9 -0
  31. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +425 -0
  32. package/corpus/templates/assets/app/components/layout/Header.tsx +3 -1
  33. package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
  34. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -6
  35. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +47 -21
  36. package/corpus/templates/assets/app/i18n-data.ts +1815 -0
  37. package/corpus/templates/assets/app/lib/libraries.ts +6 -0
  38. package/corpus/templates/assets/app/lib/picker-chat-handoff.ts +71 -0
  39. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  40. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  41. package/corpus/templates/assets/app/routes/audit.tsx +1 -1
  42. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1557 -2164
  43. package/corpus/templates/assets/app/routes/brand-kits._index.tsx +7 -191
  44. package/corpus/templates/assets/app/routes/brand-kits.tsx +1 -5
  45. package/corpus/templates/assets/app/routes/libraries.tsx +4 -4
  46. package/corpus/templates/assets/app/routes/library.$id.tsx +9 -7
  47. package/corpus/templates/assets/app/routes/library.tsx +1454 -175
  48. package/corpus/templates/assets/server/db/index.ts +1 -1
  49. package/corpus/templates/assets/server/lib/generation.ts +86 -15
  50. package/corpus/templates/assets/server/plugins/agent-chat.ts +196 -0
  51. package/corpus/templates/assets/server/plugins/db.ts +5 -0
  52. package/corpus/templates/assets/shared/api.ts +6 -3
  53. package/dist/agent/types.d.ts +21 -0
  54. package/dist/agent/types.d.ts.map +1 -1
  55. package/dist/agent/types.js.map +1 -1
  56. package/dist/client/AgentPanel.d.ts +5 -1
  57. package/dist/client/AgentPanel.d.ts.map +1 -1
  58. package/dist/client/AgentPanel.js +2 -2
  59. package/dist/client/AgentPanel.js.map +1 -1
  60. package/dist/client/AssistantChat.d.ts +6 -0
  61. package/dist/client/AssistantChat.d.ts.map +1 -1
  62. package/dist/client/AssistantChat.js +15 -3
  63. package/dist/client/AssistantChat.js.map +1 -1
  64. package/dist/client/agent-chat.d.ts +32 -0
  65. package/dist/client/agent-chat.d.ts.map +1 -1
  66. package/dist/client/agent-chat.js +113 -0
  67. package/dist/client/agent-chat.js.map +1 -1
  68. package/dist/client/composer/TiptapComposer.d.ts +2 -1
  69. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  70. package/dist/client/composer/TiptapComposer.js +228 -56
  71. package/dist/client/composer/TiptapComposer.js.map +1 -1
  72. package/dist/client/composer/extensions/MentionReference.d.ts.map +1 -1
  73. package/dist/client/composer/extensions/MentionReference.js +3 -0
  74. package/dist/client/composer/extensions/MentionReference.js.map +1 -1
  75. package/dist/client/composer/types.d.ts +21 -0
  76. package/dist/client/composer/types.d.ts.map +1 -1
  77. package/dist/client/composer/types.js.map +1 -1
  78. package/dist/client/index.d.ts +1 -1
  79. package/dist/client/index.d.ts.map +1 -1
  80. package/dist/client/index.js +1 -1
  81. package/dist/client/index.js.map +1 -1
  82. package/dist/collab/routes.d.ts +2 -2
  83. package/dist/notifications/routes.d.ts +3 -3
  84. package/dist/observability/routes.d.ts +6 -6
  85. package/dist/progress/routes.d.ts +1 -1
  86. package/dist/resources/handlers.d.ts +3 -3
  87. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  88. package/dist/server/agent-chat-plugin.js +5 -0
  89. package/dist/server/agent-chat-plugin.js.map +1 -1
  90. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  91. package/dist/server/transcribe-voice.d.ts +1 -1
  92. 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 an asset library. Poll the returned run with refresh-generation-run until it completes and creates a video asset.",
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.string(),
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 (args) => {
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 { and, desc, eq, isNull } from "drizzle-orm";
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 a library, optionally filtered by folder, collection, media type, status, role, category, or text query.",
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
- await requireLibrary(libraryId);
55
- const filters = [eq(schema.assets.libraryId, libraryId)];
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(eq(schema.assets.libraryId, libraryId)),
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) => serializeAsset(asset, lineageById.get(asset.id) ?? null));
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
- ? { id: base.id, title: base.title, description: base.description }
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 browser), libraries (the Brand Kits list), library (a single Brand Kit), 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.",
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 ({ assetId, slotId, folderId }) => {
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 raw = (await readAppState("asset-variants")) as unknown | null;
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 writeAppState(
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 deleteAppState("asset-variants");
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
- type VariantSlotInput = {
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 state = await readVariantStateUnlocked();
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 previous = await readVariantStateUnlocked();
59
- const state = isSameVariantScope(previous, input)
60
- ? previous
61
- : {
62
- runId: input.runId,
63
- batchId: input.batchId ?? null,
64
- libraryId: input.libraryId,
65
- collectionId: input.collectionId,
66
- presetId: input.presetId ?? null,
67
- sessionId: input.sessionId ?? null,
68
- prompt: input.prompt,
69
- slots: [],
70
- updatedAt: nowIso(),
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: VariantSlotInput,
107
- ): previous is AssetVariantState {
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 readVariantStateUnlocked(): Promise<AssetVariantState | null> {
126
- const current = (await readAppState("asset-variants")) as unknown | null;
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("image-variants").catch(() => null)) as
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(state: AssetVariantState) {
136
- await writeAppState(
137
- "asset-variants",
138
- state as unknown as Record<string, unknown>,
139
- );
140
- await deleteAppState("image-variants").catch(() => {});
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
  }