@agent-native/core 0.125.0 → 0.127.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 (188) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +79 -0
  3. package/corpus/core/docs/content/external-agents-catalog.mdx +14 -11
  4. package/corpus/core/docs/content/mcp-protocol.mdx +9 -4
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/a2a/index.ts +2 -0
  7. package/corpus/core/src/agent/production-agent.ts +6 -1
  8. package/corpus/core/src/agent/run-manager.ts +37 -0
  9. package/corpus/core/src/agent/run-store.ts +18 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +75 -43
  11. package/corpus/core/src/client/agent-chat.ts +19 -1
  12. package/corpus/core/src/client/chat/tool-call-display.tsx +25 -0
  13. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +23 -0
  14. package/corpus/core/src/client/settings/AgentsSection.tsx +565 -71
  15. package/corpus/core/src/client/settings/SecretsSection.tsx +71 -36
  16. package/corpus/core/src/client/use-chat-models.ts +54 -37
  17. package/corpus/core/src/localization/default-messages.ts +11 -0
  18. package/corpus/core/src/mcp/ask-app-task-handle.ts +183 -0
  19. package/corpus/core/src/mcp/builtin-tools.ts +146 -34
  20. package/corpus/core/src/server/agent-capabilities.ts +8 -1
  21. package/corpus/core/src/server/agent-chat-plugin.ts +17 -0
  22. package/corpus/core/src/server/agent-discovery.ts +1 -1
  23. package/corpus/core/src/server/agent-peer-probe.ts +133 -0
  24. package/corpus/core/src/server/core-routes-plugin.ts +67 -0
  25. package/corpus/core/src/server/email-template.ts +13 -0
  26. package/corpus/core/src/server/email.ts +40 -4
  27. package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
  28. package/corpus/core/src/sharing/actions/share-resource.ts +55 -5
  29. package/corpus/core/src/sharing/registry.ts +41 -1
  30. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  31. package/corpus/templates/assets/actions/edit-image.ts +31 -18
  32. package/corpus/templates/assets/actions/refine-image.ts +43 -29
  33. package/corpus/templates/assets/actions/restyle-image.ts +30 -19
  34. package/corpus/templates/assets/actions/variant-slots.ts +26 -0
  35. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +85 -17
  36. package/corpus/templates/clips/changelog/2026-07-27-share-notification-emails-now-include-a-clickable-recording-.md +6 -0
  37. package/corpus/templates/clips/server/db/index.ts +28 -6
  38. package/corpus/templates/clips/server/lib/share-email-hero.ts +105 -0
  39. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +100 -12
  40. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +17 -0
  41. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +169 -22
  42. package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
  43. package/corpus/templates/design/changelog/2026-07-28-the-model-you-pick-when-starting-a-new-design-is-now-used-fo.md +6 -0
  44. package/corpus/templates/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
  45. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +5 -1
  46. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +5 -0
  47. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +31 -8
  48. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +15 -0
  49. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +78 -8
  50. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +179 -0
  51. package/corpus/templates/slides/app/components/presentation/present-channel.ts +15 -0
  52. package/corpus/templates/slides/app/i18n/en-US.ts +10 -1
  53. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +67 -0
  54. package/corpus/templates/slides/app/lib/export-pptx-client.ts +16 -3
  55. package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -0
  56. package/corpus/templates/slides/app/pages/Presentation.tsx +11 -3
  57. package/corpus/templates/slides/changelog/2026-07-28-export-to-google-slides-now-creates-the-deck-directly-in-you.md +6 -0
  58. package/corpus/templates/slides/changelog/2026-07-28-opening-a-presentation-link-directly-no-longer-bounces-back-.md +6 -0
  59. package/corpus/templates/slides/changelog/2026-07-28-presenter-view-open-a-second-window-while-presenting-to-see-.md +6 -0
  60. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +90 -0
  61. package/corpus/toolkit/CHANGELOG.md +6 -0
  62. package/corpus/toolkit/package.json +1 -1
  63. package/corpus/toolkit/src/composer/TiptapComposer.tsx +143 -111
  64. package/corpus/toolkit/src/composer/runtime-adapters.tsx +2 -0
  65. package/dist/a2a/index.d.ts +2 -0
  66. package/dist/a2a/index.d.ts.map +1 -1
  67. package/dist/a2a/index.js +1 -0
  68. package/dist/a2a/index.js.map +1 -1
  69. package/dist/agent/production-agent.d.ts +2 -2
  70. package/dist/agent/production-agent.d.ts.map +1 -1
  71. package/dist/agent/production-agent.js +6 -3
  72. package/dist/agent/production-agent.js.map +1 -1
  73. package/dist/agent/run-manager.d.ts +11 -0
  74. package/dist/agent/run-manager.d.ts.map +1 -1
  75. package/dist/agent/run-manager.js +34 -1
  76. package/dist/agent/run-manager.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +8 -0
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +16 -0
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  82. package/dist/client/MultiTabAssistantChat.js +60 -31
  83. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  84. package/dist/client/agent-chat.d.ts +6 -0
  85. package/dist/client/agent-chat.d.ts.map +1 -1
  86. package/dist/client/agent-chat.js +13 -1
  87. package/dist/client/agent-chat.js.map +1 -1
  88. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  89. package/dist/client/chat/tool-call-display.js +14 -0
  90. package/dist/client/chat/tool-call-display.js.map +1 -1
  91. package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
  92. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  93. package/dist/client/resources/mcp-integration-catalog.js +16 -0
  94. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  95. package/dist/client/settings/AgentsSection.d.ts.map +1 -1
  96. package/dist/client/settings/AgentsSection.js +250 -30
  97. package/dist/client/settings/AgentsSection.js.map +1 -1
  98. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  99. package/dist/client/settings/SecretsSection.js +16 -2
  100. package/dist/client/settings/SecretsSection.js.map +1 -1
  101. package/dist/client/use-chat-models.d.ts.map +1 -1
  102. package/dist/client/use-chat-models.js +46 -27
  103. package/dist/client/use-chat-models.js.map +1 -1
  104. package/dist/collab/routes.d.ts +2 -2
  105. package/dist/collab/struct-routes.d.ts +1 -1
  106. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  107. package/dist/localization/default-messages.d.ts +9 -0
  108. package/dist/localization/default-messages.d.ts.map +1 -1
  109. package/dist/localization/default-messages.js +9 -0
  110. package/dist/localization/default-messages.js.map +1 -1
  111. package/dist/mcp/ask-app-task-handle.d.ts +25 -0
  112. package/dist/mcp/ask-app-task-handle.d.ts.map +1 -0
  113. package/dist/mcp/ask-app-task-handle.js +136 -0
  114. package/dist/mcp/ask-app-task-handle.js.map +1 -0
  115. package/dist/mcp/builtin-tools.d.ts +3 -3
  116. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  117. package/dist/mcp/builtin-tools.js +115 -38
  118. package/dist/mcp/builtin-tools.js.map +1 -1
  119. package/dist/observability/routes.d.ts +1 -1
  120. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  121. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  122. package/dist/secrets/routes.d.ts +9 -9
  123. package/dist/server/agent-capabilities.d.ts +7 -1
  124. package/dist/server/agent-capabilities.d.ts.map +1 -1
  125. package/dist/server/agent-capabilities.js +1 -0
  126. package/dist/server/agent-capabilities.js.map +1 -1
  127. package/dist/server/agent-chat-plugin.d.ts +7 -0
  128. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  129. package/dist/server/agent-chat-plugin.js +16 -1
  130. package/dist/server/agent-chat-plugin.js.map +1 -1
  131. package/dist/server/agent-discovery.d.ts +1 -0
  132. package/dist/server/agent-discovery.d.ts.map +1 -1
  133. package/dist/server/agent-discovery.js +1 -1
  134. package/dist/server/agent-discovery.js.map +1 -1
  135. package/dist/server/agent-peer-probe.d.ts +37 -0
  136. package/dist/server/agent-peer-probe.d.ts.map +1 -0
  137. package/dist/server/agent-peer-probe.js +90 -0
  138. package/dist/server/agent-peer-probe.js.map +1 -0
  139. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  140. package/dist/server/core-routes-plugin.js +53 -0
  141. package/dist/server/core-routes-plugin.js.map +1 -1
  142. package/dist/server/email-template.d.ts +6 -0
  143. package/dist/server/email-template.d.ts.map +1 -1
  144. package/dist/server/email-template.js +6 -0
  145. package/dist/server/email-template.js.map +1 -1
  146. package/dist/server/email.d.ts +7 -0
  147. package/dist/server/email.d.ts.map +1 -1
  148. package/dist/server/email.js +31 -6
  149. package/dist/server/email.js.map +1 -1
  150. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  151. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  152. package/dist/shared/mcp-embed-headers.js +4 -0
  153. package/dist/shared/mcp-embed-headers.js.map +1 -1
  154. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  155. package/dist/sharing/actions/share-resource.js +49 -5
  156. package/dist/sharing/actions/share-resource.js.map +1 -1
  157. package/dist/sharing/registry.d.ts +40 -1
  158. package/dist/sharing/registry.d.ts.map +1 -1
  159. package/dist/sharing/registry.js.map +1 -1
  160. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  161. package/docs/content/external-agents-catalog.mdx +14 -11
  162. package/docs/content/mcp-protocol.mdx +9 -4
  163. package/package.json +2 -2
  164. package/src/a2a/index.ts +2 -0
  165. package/src/agent/production-agent.ts +6 -1
  166. package/src/agent/run-manager.ts +37 -0
  167. package/src/agent/run-store.ts +18 -0
  168. package/src/client/MultiTabAssistantChat.tsx +75 -43
  169. package/src/client/agent-chat.ts +19 -1
  170. package/src/client/chat/tool-call-display.tsx +25 -0
  171. package/src/client/resources/mcp-integration-catalog.ts +23 -0
  172. package/src/client/settings/AgentsSection.tsx +565 -71
  173. package/src/client/settings/SecretsSection.tsx +71 -36
  174. package/src/client/use-chat-models.ts +54 -37
  175. package/src/localization/default-messages.ts +11 -0
  176. package/src/mcp/ask-app-task-handle.ts +183 -0
  177. package/src/mcp/builtin-tools.ts +146 -34
  178. package/src/server/agent-capabilities.ts +8 -1
  179. package/src/server/agent-chat-plugin.ts +17 -0
  180. package/src/server/agent-discovery.ts +1 -1
  181. package/src/server/agent-peer-probe.ts +133 -0
  182. package/src/server/core-routes-plugin.ts +67 -0
  183. package/src/server/email-template.ts +13 -0
  184. package/src/server/email.ts +40 -4
  185. package/src/shared/mcp-embed-headers.ts +4 -0
  186. package/src/sharing/actions/share-resource.ts +55 -5
  187. package/src/sharing/registry.ts +41 -1
  188. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
@@ -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,
@@ -89,6 +89,9 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
89
89
  const queryClient = useQueryClient();
90
90
  const [clearAllOpen, setClearAllOpen] = useState(false);
91
91
  const [previewSlotId, setPreviewSlotId] = useState<string | null>(null);
92
+ const trayScrollRef = useRef<HTMLDivElement | null>(null);
93
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
94
+ const [canScrollRight, setCanScrollRight] = useState(false);
92
95
  const stateKey = variantStateKey(threadId);
93
96
  const stateQueryKey = useMemo(() => ["app-state", stateKey], [stateKey]);
94
97
  const { data: variants } = useQuery({
@@ -129,6 +132,21 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
129
132
  ),
130
133
  [variants?.slots],
131
134
  );
135
+ // Variant numbers reflect generation order (oldest = 1), not display order:
136
+ // refined candidates render first but keep the next number in sequence
137
+ // instead of stealing "Variant 1" from the newest-first display sort above.
138
+ const variantNumberBySlotId = useMemo(() => {
139
+ const map = new Map<string, number>();
140
+ (variants?.slots ?? [])
141
+ .slice()
142
+ .sort(
143
+ (left, right) =>
144
+ slotTime(left) - slotTime(right) ||
145
+ left.slotId.localeCompare(right.slotId),
146
+ )
147
+ .forEach((slot, index) => map.set(slot.slotId, index + 1));
148
+ return map;
149
+ }, [variants?.slots]);
132
150
  const belongsToThread = Boolean(
133
151
  variants &&
134
152
  (threadId ? variants.threadId === threadId : !variants.threadId),
@@ -165,6 +183,25 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
165
183
  [previewSlotId, slots],
166
184
  );
167
185
 
186
+ const updateScrollEdges = () => {
187
+ const el = trayScrollRef.current;
188
+ if (!el) return;
189
+ setCanScrollLeft(el.scrollLeft > 1);
190
+ setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
191
+ };
192
+
193
+ useEffect(() => {
194
+ // Re-measure after the slot list changes size (new/removed candidates)
195
+ // since that can flip whether either arrow should be enabled.
196
+ updateScrollEdges();
197
+ }, [slots.length]);
198
+
199
+ const scrollTrayBy = (direction: 1 | -1) => {
200
+ const el = trayScrollRef.current;
201
+ if (!el) return;
202
+ el.scrollBy({ left: direction * el.clientWidth * 0.9, behavior: "smooth" });
203
+ };
204
+
168
205
  if (!belongsToThread || !variants) return null;
169
206
  if (slots.length === 0) return null;
170
207
 
@@ -296,6 +333,7 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
296
333
  <GenerationPreviewDialog
297
334
  slot={previewSlot}
298
335
  slots={slots}
336
+ variantNumberBySlotId={variantNumberBySlotId}
299
337
  prompt={variants.prompt}
300
338
  libraryTitle={libraryTitle}
301
339
  isSaving={saveGenerated.isPending}
@@ -351,22 +389,50 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
351
389
  </Button>
352
390
  </div>
353
391
 
354
- <div className="max-h-[min(640px,52vh)] overflow-y-auto p-3">
355
- <div className="assets-library-grid grid grid-cols-2 gap-3 sm:grid-cols-3">
356
- {slots.map((slot, index) => (
357
- <GenerationDraftCard
358
- key={slot.slotId}
359
- slot={slot}
360
- variantNumber={index + 1}
361
- isSaving={saveGenerated.isPending}
362
- isDismissing={dismissSlot.isPending}
363
- onPreview={() => setPreviewSlotId(slot.slotId)}
364
- onSave={() => saveSlot(slot)}
365
- onRefine={() => refineSlot(slot, index + 1)}
366
- onDismiss={() => dismissSlotById(slot)}
367
- />
368
- ))}
392
+ <div className="relative">
393
+ <div
394
+ ref={trayScrollRef}
395
+ onScroll={updateScrollEdges}
396
+ className="assets-library-tray flex gap-3 overflow-x-auto scroll-smooth p-3"
397
+ >
398
+ {slots.map((slot) => {
399
+ const variantNumber =
400
+ variantNumberBySlotId.get(slot.slotId) ?? 1;
401
+ return (
402
+ <GenerationDraftCard
403
+ key={slot.slotId}
404
+ slot={slot}
405
+ variantNumber={variantNumber}
406
+ isSaving={saveGenerated.isPending}
407
+ isDismissing={dismissSlot.isPending}
408
+ onPreview={() => setPreviewSlotId(slot.slotId)}
409
+ onSave={() => saveSlot(slot)}
410
+ onRefine={() => refineSlot(slot, variantNumber)}
411
+ onDismiss={() => dismissSlotById(slot)}
412
+ />
413
+ );
414
+ })}
369
415
  </div>
416
+ {canScrollLeft ? (
417
+ <button
418
+ type="button"
419
+ aria-label={t("library.previousImage")}
420
+ onClick={() => scrollTrayBy(-1)}
421
+ className="absolute left-1 top-1/2 z-10 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full border border-border/80 bg-background/90 text-foreground shadow-sm transition hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
422
+ >
423
+ <IconChevronLeft className="h-4 w-4" />
424
+ </button>
425
+ ) : null}
426
+ {canScrollRight ? (
427
+ <button
428
+ type="button"
429
+ aria-label={t("library.nextImage")}
430
+ onClick={() => scrollTrayBy(1)}
431
+ className="absolute right-1 top-1/2 z-10 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full border border-border/80 bg-background/90 text-foreground shadow-sm transition hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
432
+ >
433
+ <IconChevronRight className="h-4 w-4" />
434
+ </button>
435
+ ) : null}
370
436
  </div>
371
437
  </div>
372
438
  </section>
@@ -399,7 +465,7 @@ function GenerationDraftCard({
399
465
  number: variantNumber,
400
466
  });
401
467
  return (
402
- <div className="overflow-hidden rounded-lg border border-border/80 bg-background transition hover:border-foreground/25 hover:bg-muted/10">
468
+ <div className="w-36 shrink-0 overflow-hidden rounded-lg border border-border/80 bg-background transition hover:border-foreground/25 hover:bg-muted/10 sm:w-44">
403
469
  <div className="group relative">
404
470
  <button
405
471
  type="button"
@@ -510,6 +576,7 @@ function GenerationDraftCard({
510
576
  function GenerationPreviewDialog({
511
577
  slot,
512
578
  slots,
579
+ variantNumberBySlotId,
513
580
  prompt,
514
581
  libraryTitle,
515
582
  isSaving,
@@ -522,6 +589,7 @@ function GenerationPreviewDialog({
522
589
  }: {
523
590
  slot: VariantSlot | null;
524
591
  slots: VariantSlot[];
592
+ variantNumberBySlotId: Map<string, number>;
525
593
  prompt: string;
526
594
  libraryTitle: string | null;
527
595
  isSaving: boolean;
@@ -553,7 +621,7 @@ function GenerationPreviewDialog({
553
621
  const sources = slot ? assetPreviewSources(slot, "preview") : [];
554
622
  const src = sources[0];
555
623
  const variantNumber = slot
556
- ? slots.findIndex((item) => item.slotId === slot.slotId) + 1
624
+ ? (variantNumberBySlotId.get(slot.slotId) ?? 0)
557
625
  : 0;
558
626
  const variantLabel = t("library.variantWithNumber", {
559
627
  number: variantNumber,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-27
4
+ ---
5
+
6
+ Share notification emails now include a clickable recording thumbnail that opens the clip
@@ -1,8 +1,12 @@
1
1
  import { createGetDb, getDbExec } from "@agent-native/core/db";
2
- import { getAppProductionUrl } from "@agent-native/core/server";
2
+ import { organizations } from "@agent-native/core/org";
3
3
  import { registerShareableResource } from "@agent-native/core/sharing";
4
4
  import { eq } from "drizzle-orm";
5
5
 
6
+ import {
7
+ absoluteUrl,
8
+ recordingShareHeroHtml,
9
+ } from "../lib/share-email-hero.js";
6
10
  import * as schema from "./schema.js";
7
11
 
8
12
  export const getDb = createGetDb(schema);
@@ -22,9 +26,20 @@ async function orgBrandLogoUrl(
22
26
  .from(schema.organizationSettings)
23
27
  .where(eq(schema.organizationSettings.organizationId, organizationId))
24
28
  .limit(1);
25
- const logo = row?.brandLogoUrl?.trim();
26
- if (!logo) return undefined;
27
- return logo.startsWith("/") ? `${getAppProductionUrl()}${logo}` : logo;
29
+ return absoluteUrl(row?.brandLogoUrl);
30
+ }
31
+
32
+ /** Show the sharing org's name beside the logo instead of the app name. */
33
+ async function orgBrandName(
34
+ organizationId: string | undefined,
35
+ ): Promise<string | undefined> {
36
+ if (!organizationId) return undefined;
37
+ const [row] = await getDb()
38
+ .select({ name: organizations.name })
39
+ .from(organizations)
40
+ .where(eq(organizations.id, organizationId))
41
+ .limit(1);
42
+ return row?.name?.trim() || undefined;
28
43
  }
29
44
 
30
45
  registerShareableResource({
@@ -34,8 +49,15 @@ registerShareableResource({
34
49
  displayName: "Recording",
35
50
  titleColumn: "title",
36
51
  getResourcePath: (recording) => `/r/${recording.id}`,
37
- getShareEmailLogoUrl: (recording) =>
38
- orgBrandLogoUrl(recording.organizationId),
52
+ getLogoUrl: (recording) => orgBrandLogoUrl(recording.organizationId),
53
+ getBrandName: (recording) => orgBrandName(recording.organizationId),
54
+ // Replies reach the person who shared the clip; the sending address stays
55
+ // the verified one so SPF/DKIM still pass.
56
+ getSender: (_recording, ctx) => ({
57
+ fromName: `${ctx.sender.name} via Clips`,
58
+ replyTo: ctx.sender.email,
59
+ }),
60
+ getHeroHtml: (recording, ctx) => recordingShareHeroHtml(recording, ctx),
39
61
  getDb,
40
62
  ownerAccessIgnoresOrg: true,
41
63
  });
@@ -0,0 +1,105 @@
1
+ import {
2
+ getAppProductionUrl,
3
+ withConfiguredAppBasePath,
4
+ } from "@agent-native/core/server";
5
+
6
+ /**
7
+ * Origin plus the configured mount path. Deployments served under
8
+ * APP_BASE_PATH (e.g. `/clips`) would otherwise link email assets at the
9
+ * gateway root, where nothing is served.
10
+ */
11
+ function appBaseUrl(): string {
12
+ return withConfiguredAppBasePath(getAppProductionUrl());
13
+ }
14
+
15
+ /** Make a stored URL absolute for use in emails (relative paths won't load). */
16
+ export function absoluteUrl(
17
+ url: string | null | undefined,
18
+ ): string | undefined {
19
+ const trimmed = url?.trim();
20
+ if (!trimmed) return undefined;
21
+ return trimmed.startsWith("/") ? `${appBaseUrl()}${trimmed}` : trimmed;
22
+ }
23
+
24
+ function escapeAttr(s: string): string {
25
+ return s
26
+ .replace(/&/g, "&amp;")
27
+ .replace(/</g, "&lt;")
28
+ .replace(/>/g, "&gt;")
29
+ .replace(/"/g, "&quot;")
30
+ .replace(/'/g, "&#39;");
31
+ }
32
+
33
+ /**
34
+ * Percent-encode characters that could terminate a CSS `url('…')`. A style
35
+ * attribute is HTML-decoded before it is parsed as CSS, so HTML escaping alone
36
+ * does not protect the CSS context — an escaped quote decodes back to a real
37
+ * one and lets the value break out into further declarations.
38
+ */
39
+ function escapeCssUrl(url: string): string {
40
+ return url.replace(
41
+ /['"()\\\s]/g,
42
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`,
43
+ );
44
+ }
45
+
46
+ function isHttpUrl(value: string): boolean {
47
+ try {
48
+ const { protocol } = new URL(value);
49
+ return protocol === "https:" || protocol === "http:";
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ export type ShareHeroRecording = {
56
+ thumbnailUrl?: string | null;
57
+ animatedThumbnailUrl?: string | null;
58
+ };
59
+
60
+ /**
61
+ * Build the share-email preview for a recording: a 16:9 thumbnail with a
62
+ * centered play badge, linking to the clip. Uses the background-image + VML
63
+ * technique so the badge stays centered across clients, including Outlook. The
64
+ * play badge is served from Clips' own public assets.
65
+ */
66
+ export function recordingShareHeroHtml(
67
+ recording: ShareHeroRecording,
68
+ ctx: { href: string; alt?: string },
69
+ ): string | undefined {
70
+ // GIF-only recordings leave `thumbnailUrl` empty, so fall back to the
71
+ // animated thumbnail rather than dropping the preview entirely.
72
+ const thumb = absoluteUrl(
73
+ recording.thumbnailUrl || recording.animatedThumbnailUrl,
74
+ );
75
+ if (!thumb || !isHttpUrl(thumb)) return undefined;
76
+ const url = escapeAttr(thumb);
77
+ const cssUrl = escapeAttr(escapeCssUrl(thumb));
78
+ const href = escapeAttr(ctx.href);
79
+ const title = escapeAttr(ctx.alt ?? "Play video");
80
+ const badge = escapeAttr(`${appBaseUrl()}/play-badge.png`);
81
+ const W = 488;
82
+ const H = 275;
83
+ return `
84
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="margin:24px 0 0 0;">
85
+ <tr>
86
+ <td>
87
+ <a href="${href}" style="display:block; text-decoration:none;" title="${title}">
88
+ <!--[if mso]>
89
+ <v:image xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:${W}px;height:${H}px;" src="${url}" />
90
+ <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="false" stroke="false" style="position:absolute;width:${W}px;height:${H}px;">
91
+ <v:textbox inset="0,0,0,0"><center>
92
+ <![endif]-->
93
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="${W}" height="${H}" background="${url}" style="width:100%; max-width:${W}px; height:${H}px; background-image:url('${cssUrl}'); background-size:cover; background-position:center; background-color:#141417; border-radius:12px;">
94
+ <tr>
95
+ <td align="center" valign="middle" height="${H}" style="height:${H}px;">
96
+ <img src="${badge}" alt="Play video" width="64" height="64" style="width:64px; height:64px; border:0; display:inline-block;" />
97
+ </td>
98
+ </tr>
99
+ </table>
100
+ <!--[if mso]></center></v:textbox></v:rect><![endif]-->
101
+ </a>
102
+ </td>
103
+ </tr>
104
+ </table>`;
105
+ }