@agent-native/core 0.157.3 → 0.157.5

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 (211) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  3. package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +21 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +40 -1
  5. package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  6. package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +21 -0
  7. package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  8. package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +21 -0
  9. package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  10. package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +21 -0
  11. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +3 -1
  12. package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  13. package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  14. package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  15. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -7
  16. package/corpus/templates/clips/actions/approve-recording-access-request.ts +147 -0
  17. package/corpus/templates/clips/actions/request-recording-access.ts +121 -23
  18. package/corpus/templates/clips/actions/view-screen.ts +1 -1
  19. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +34 -53
  20. package/corpus/templates/clips/app/components/player/request-access-dialog.tsx +104 -0
  21. package/corpus/templates/clips/app/components/player/share-dialog.tsx +36 -45
  22. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +3 -6
  23. package/corpus/templates/clips/app/i18n/en-US.ts +26 -1
  24. package/corpus/templates/clips/app/lib/public-meeting.ts +1 -1
  25. package/corpus/templates/clips/app/routes/access-request.approve.tsx +250 -0
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -32
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +123 -37
  28. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +41 -1
  29. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +1 -0
  30. package/corpus/templates/clips/server/lib/emails.ts +40 -11
  31. package/corpus/templates/clips/shared/recording-link.ts +32 -1
  32. package/corpus/templates/clips/shared/share-attribution.ts +13 -0
  33. package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  34. package/corpus/templates/content/.agents/skills/sharing/SKILL.md +21 -0
  35. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +22 -10
  36. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +2 -1
  37. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +245 -9
  38. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +185 -134
  39. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +61 -45
  40. package/corpus/templates/content/app/components/editor/extensions/ImageNode.ts +6 -0
  41. package/corpus/templates/content/app/components/editor/image-upload.ts +137 -5
  42. package/corpus/templates/content/app/global.css +8 -0
  43. package/corpus/templates/content/changelog/2026-08-12-image-uploads-now-confirm-the-image-can-be-displayed-before-.md +6 -0
  44. package/corpus/templates/content/changelog/2026-08-14-images-fill-the-page-width-by-default-and-remain-visible-whe.md +6 -0
  45. package/corpus/templates/content/changelog/2026-08-14-images-inserted-from-the-slash-command-stay-saved-after-uplo.md +6 -0
  46. package/corpus/templates/content/changelog/2026-08-14-svg-files-can-be-selected-reliably-from-the-image-picker.md +6 -0
  47. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  48. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +21 -0
  49. package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  50. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +21 -0
  51. package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  52. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +21 -0
  53. package/corpus/templates/factory/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  54. package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +21 -0
  55. package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  56. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +21 -0
  57. package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  58. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +21 -0
  59. package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  60. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +21 -0
  61. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -1
  62. package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  63. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +21 -0
  64. package/corpus/templates/plan/app/pages/PlansPage.tsx +25 -30
  65. package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  66. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +21 -0
  67. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +2 -1
  68. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +124 -180
  69. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +1 -0
  70. package/corpus/templates/slides/app/global.css +99 -3
  71. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  72. package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  73. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +21 -0
  74. package/dist/chat-threads/schema.d.ts +1 -1
  75. package/dist/cli/code-agent-executor.js +318 -0
  76. package/dist/client/AgentPanel.d.ts +13 -1
  77. package/dist/client/AgentPanel.js +41 -3
  78. package/dist/client/AssistantChat.d.ts +9 -1
  79. package/dist/client/AssistantChat.js +26 -13
  80. package/dist/client/MultiTabAssistantChat.js +18 -5
  81. package/dist/client/agent-chat.d.ts +8 -0
  82. package/dist/client/agent-chat.js +15 -0
  83. package/dist/client/app-chat-sidebar.d.ts +23 -0
  84. package/dist/client/app-chat-sidebar.js +53 -0
  85. package/dist/client/app-providers.js +31 -2
  86. package/dist/client/frame-protocol.d.ts +20 -2
  87. package/dist/client/hooks/index.d.ts +1 -0
  88. package/dist/client/hooks/index.js +1 -0
  89. package/dist/client/sharing/AgentShareSection.d.ts +8 -0
  90. package/dist/client/sharing/AgentShareSection.js +50 -0
  91. package/dist/client/sharing/ShareButton.js +2 -1
  92. package/dist/client/sharing/ShareDialog.d.ts +2 -2
  93. package/dist/client/sharing/ShareDialog.js +21 -29
  94. package/dist/client/sharing/index.d.ts +1 -0
  95. package/dist/client/sharing/index.js +1 -0
  96. package/dist/client/sharing/useShareButtonController.d.ts +1 -0
  97. package/dist/client/sharing/useShareDialogController.d.ts +3 -0
  98. package/dist/client/sharing/useShareDialogController.js +2 -1
  99. package/dist/client/theme.d.ts +21 -0
  100. package/dist/client/theme.js +60 -0
  101. package/dist/client/ui/index.d.ts +1 -1
  102. package/dist/client/ui/index.js +1 -1
  103. package/dist/client/use-session.js +23 -0
  104. package/dist/collab/awareness.d.ts +2 -2
  105. package/dist/collab/struct-routes.d.ts +1 -1
  106. package/dist/dashboard-storage/schema.d.ts +1 -1
  107. package/dist/data-programs/schema.d.ts +1 -1
  108. package/dist/extensions/schema.d.ts +1 -1
  109. package/dist/localization/core-messages/ar-SA.js +6 -0
  110. package/dist/localization/core-messages/de-DE.js +6 -0
  111. package/dist/localization/core-messages/en-US.d.ts +6 -0
  112. package/dist/localization/core-messages/en-US.js +6 -0
  113. package/dist/localization/core-messages/es-ES.js +6 -0
  114. package/dist/localization/core-messages/fr-FR.js +6 -0
  115. package/dist/localization/core-messages/hi-IN.js +6 -0
  116. package/dist/localization/core-messages/ja-JP.js +6 -0
  117. package/dist/localization/core-messages/ko-KR.js +6 -0
  118. package/dist/localization/core-messages/pt-BR.js +6 -0
  119. package/dist/localization/core-messages/zh-CN.js +6 -0
  120. package/dist/localization/core-messages/zh-TW.js +6 -0
  121. package/dist/localization/core-messages.d.ts +6 -0
  122. package/dist/localization/default-messages.d.ts +1 -0
  123. package/dist/localization/default-messages.js +1 -0
  124. package/dist/notifications/routes.d.ts +3 -3
  125. package/dist/progress/routes.d.ts +1 -1
  126. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  127. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  128. package/dist/server/auth.js +8 -5
  129. package/dist/server/identity-sso-store.d.ts +6 -1
  130. package/dist/server/identity-sso-store.js +57 -14
  131. package/dist/server/identity-sso.d.ts +7 -8
  132. package/dist/server/identity-sso.js +15 -12
  133. package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
  134. package/dist/sharing/actions/list-resource-shares.js +1 -0
  135. package/dist/sharing/schema.d.ts +1 -1
  136. package/dist/sharing/schema.js +4 -1
  137. package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  138. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  139. package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  140. package/dist/templates/default/.agents/skills/sharing/SKILL.md +21 -0
  141. package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  142. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  143. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +21 -0
  144. package/docs/content/actions-access-control.mdx +226 -0
  145. package/docs/content/actions-advanced.mdx +152 -0
  146. package/docs/content/actions-defining.mdx +275 -0
  147. package/docs/content/actions-other-surfaces.mdx +167 -0
  148. package/docs/content/actions-overview.mdx +214 -0
  149. package/docs/content/actions-run-context.mdx +180 -0
  150. package/docs/content/agent-surfaces.mdx +1 -1
  151. package/docs/content/agent-teams.mdx +1 -1
  152. package/docs/content/agent-web-surfaces.mdx +1 -1
  153. package/docs/content/audit-log.mdx +1 -1
  154. package/docs/content/automation-connectors.mdx +1 -1
  155. package/docs/content/automations.mdx +1 -1
  156. package/docs/content/blueprint-installer.mdx +1 -1
  157. package/docs/content/cli-adapters.mdx +1 -1
  158. package/docs/content/client-agent-chat.mdx +1 -1
  159. package/docs/content/client-data.mdx +2 -2
  160. package/docs/content/client-overview.mdx +2 -2
  161. package/docs/content/components.mdx +1 -1
  162. package/docs/content/cross-app-sso.mdx +24 -17
  163. package/docs/content/data-programs.mdx +2 -2
  164. package/docs/content/database.mdx +1 -1
  165. package/docs/content/deployment.mdx +1 -1
  166. package/docs/content/docs-components.mdx +14 -3
  167. package/docs/content/drop-in-agent.mdx +2 -2
  168. package/docs/content/durable-resume.mdx +1 -1
  169. package/docs/content/evals.mdx +1 -1
  170. package/docs/content/extensions.mdx +1 -1
  171. package/docs/content/external-agents-catalog.mdx +2 -2
  172. package/docs/content/external-agents.mdx +1 -1
  173. package/docs/content/generative-ui.mdx +1 -1
  174. package/docs/content/getting-started.mdx +1 -1
  175. package/docs/content/http-api.mdx +4 -4
  176. package/docs/content/human-approval.mdx +1 -1
  177. package/docs/content/key-concepts.mdx +1 -1
  178. package/docs/content/mcp-apps.mdx +1 -1
  179. package/docs/content/mcp-clients.mdx +1 -1
  180. package/docs/content/mcp-protocol.mdx +1 -1
  181. package/docs/content/native-chat-ui.mdx +1 -1
  182. package/docs/content/observability.mdx +1 -1
  183. package/docs/content/organizations-teams-permissions.mdx +1 -1
  184. package/docs/content/processors.mdx +1 -1
  185. package/docs/content/pure-agent-apps.mdx +1 -1
  186. package/docs/content/recurring-jobs.mdx +1 -1
  187. package/docs/content/server-database.mdx +1 -1
  188. package/docs/content/server-overview.mdx +3 -3
  189. package/docs/content/server-routes.mdx +2 -2
  190. package/docs/content/template-analytics-developers.mdx +1 -1
  191. package/docs/content/template-assets-developers.mdx +1 -1
  192. package/docs/content/template-brain-developers.mdx +1 -1
  193. package/docs/content/template-calendar-developers.mdx +1 -1
  194. package/docs/content/template-chat-first-edits.mdx +1 -1
  195. package/docs/content/template-chat.mdx +1 -1
  196. package/docs/content/template-content-developers.mdx +1 -1
  197. package/docs/content/template-forms-developers.mdx +1 -1
  198. package/docs/content/template-forms.mdx +1 -1
  199. package/docs/content/template-mail-developers.mdx +2 -2
  200. package/docs/content/toolkit-org-team.mdx +1 -1
  201. package/docs/content/tracking.mdx +1 -1
  202. package/docs/content/what-is-agent-native.mdx +1 -1
  203. package/package.json +2 -2
  204. package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  205. package/src/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  206. package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  207. package/src/templates/default/.agents/skills/sharing/SKILL.md +21 -0
  208. package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  209. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  210. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +21 -0
  211. package/docs/content/actions.mdx +0 -750
@@ -52,12 +52,39 @@ const CLIPS_SENDER =
52
52
  'From is the configured EMAIL_FROM with the display name "Agent-Native Clips"; on first-party agent-native.com deployments it becomes clips@agent-native.com. Reply-to is hello@agent-native.com.';
53
53
 
54
54
  let registered = false;
55
+ const GLOBAL_REGISTRATION_KEY =
56
+ "__agentNativeClipsTransactionalEmailCatalogRegistered";
57
+ const globalRegistrationState = globalThis as typeof globalThis &
58
+ Record<string, boolean | undefined>;
59
+
60
+ function defineClipsTransactionalEmail(
61
+ definition: Parameters<typeof defineTransactionalEmail>[0],
62
+ ): boolean {
63
+ try {
64
+ defineTransactionalEmail(definition);
65
+ return true;
66
+ } catch (error) {
67
+ if (
68
+ error instanceof Error &&
69
+ error.message.startsWith('Duplicate transactional email id "clips.')
70
+ ) {
71
+ // The same Clips catalog can be evaluated twice by Nitro/Vite. The
72
+ // first definition is canonical, so a duplicate is already registered.
73
+ return false;
74
+ }
75
+ throw error;
76
+ }
77
+ }
55
78
 
56
79
  export function registerClipsEmails(): void {
57
- if (registered) return;
80
+ // Nitro/Vite can evaluate the same server module through two module
81
+ // instances during dev reloads. Keep the catalog registration process-wide
82
+ // so those instances cannot submit the same id twice.
83
+ if (registered || globalRegistrationState[GLOBAL_REGISTRATION_KEY]) return;
58
84
  registered = true;
85
+ globalRegistrationState[GLOBAL_REGISTRATION_KEY] = true;
59
86
 
60
- defineTransactionalEmail({
87
+ defineClipsTransactionalEmail({
61
88
  id: CLIPS_ACCESS_REQUEST_EMAIL_ID,
62
89
  name: "Clip access request",
63
90
  trigger:
@@ -73,10 +100,12 @@ export function registerClipsEmails(): void {
73
100
  requesterEmail: "sam.rivera@example.com",
74
101
  recordingTitle: SAMPLE_TITLE,
75
102
  url: "https://example.com/share/rec_sample",
103
+ allowAccessUrl:
104
+ "https://example.com/access-request/approve?recordingId=rec_sample&token=preview-token",
76
105
  }),
77
106
  });
78
107
 
79
- defineTransactionalEmail({
108
+ defineClipsTransactionalEmail({
80
109
  id: CLIPS_FIRST_VIEW_EMAIL_ID,
81
110
  name: "First view on a Clip",
82
111
  trigger:
@@ -96,7 +125,7 @@ export function registerClipsEmails(): void {
96
125
  }),
97
126
  });
98
127
 
99
- defineTransactionalEmail({
128
+ defineClipsTransactionalEmail({
100
129
  id: CLIPS_UNVIEWED_REMINDER_EMAIL_ID,
101
130
  name: "Unviewed Clip reminder",
102
131
  trigger:
@@ -118,7 +147,7 @@ export function registerClipsEmails(): void {
118
147
  }),
119
148
  });
120
149
 
121
- defineTransactionalEmail({
150
+ defineClipsTransactionalEmail({
122
151
  id: CLIPS_FIRST_AGENT_VIEW_EMAIL_ID,
123
152
  name: "First agent read of a Clip",
124
153
  trigger:
@@ -138,7 +167,7 @@ export function registerClipsEmails(): void {
138
167
  }),
139
168
  });
140
169
 
141
- defineTransactionalEmail({
170
+ defineClipsTransactionalEmail({
142
171
  id: CLIPS_FIRST_IMPORT_EMAIL_ID,
143
172
  name: "First imported video is ready",
144
173
  trigger:
@@ -157,7 +186,7 @@ export function registerClipsEmails(): void {
157
186
  }),
158
187
  });
159
188
 
160
- defineTransactionalEmail({
189
+ defineClipsTransactionalEmail({
161
190
  id: CLIPS_MONTHLY_RECAP_EMAIL_ID,
162
191
  name: "Monthly Clips recap",
163
192
  trigger:
@@ -191,7 +220,7 @@ export function registerClipsEmails(): void {
191
220
  }),
192
221
  });
193
222
 
194
- defineTransactionalEmail({
223
+ defineClipsTransactionalEmail({
195
224
  id: CLIPS_TWO_CLIPS_EMAIL_ID,
196
225
  name: "Two Clips received",
197
226
  trigger:
@@ -210,7 +239,7 @@ export function registerClipsEmails(): void {
210
239
  }),
211
240
  });
212
241
 
213
- defineTransactionalEmail({
242
+ defineClipsTransactionalEmail({
214
243
  id: CLIPS_ACTIVITY_COMMENT_EMAIL_ID,
215
244
  name: "Clip comment",
216
245
  trigger:
@@ -234,7 +263,7 @@ export function registerClipsEmails(): void {
234
263
  }),
235
264
  });
236
265
 
237
- defineTransactionalEmail({
266
+ defineClipsTransactionalEmail({
238
267
  id: CLIPS_ACTIVITY_REACTION_EMAIL_ID,
239
268
  name: "Clip reaction",
240
269
  trigger: "A viewer reacts with an emoji on a Clip.",
@@ -256,7 +285,7 @@ export function registerClipsEmails(): void {
256
285
  }),
257
286
  });
258
287
 
259
- defineTransactionalEmail({
288
+ defineClipsTransactionalEmail({
260
289
  id: CLIPS_ORGANIZATION_INVITE_EMAIL_ID,
261
290
  name: "Organization invitation",
262
291
  trigger:
@@ -12,15 +12,46 @@
12
12
  */
13
13
  import { withShareAttribution } from "./share-attribution";
14
14
 
15
- /** Short-lived capability used by a signed-in private share viewer to request access. */
15
+ /** Short-lived capability used by a private share viewer to request access. */
16
16
  export const CLIPS_ACCESS_REQUEST_TOKEN_PREFIX = "clips-access-request";
17
17
  export const CLIPS_ACCESS_REQUEST_TOKEN_TTL_SECONDS = 10 * 60;
18
18
 
19
+ /** Signed capability embedded in the owner's access-request email. */
20
+ export const CLIPS_ACCESS_APPROVAL_TOKEN_PREFIX = "clips-access-approval";
21
+ export const CLIPS_ACCESS_APPROVAL_TOKEN_TTL_SECONDS = 7 * 24 * 60 * 60;
22
+ export const CLIPS_ACCESS_APPROVAL_SESSION_KEY_PREFIX =
23
+ "clips-access-approval-token:";
24
+
19
25
  /** Public share path for a recording, relative to the app base path. */
20
26
  export function recordingSharePath(recordingId: string): string {
21
27
  return `/share/${encodeURIComponent(recordingId)}`;
22
28
  }
23
29
 
30
+ /** Owner-only route used by the one-click approval button in access emails. */
31
+ export function recordingAccessApprovalPath(
32
+ recordingId: string,
33
+ approvalToken: string,
34
+ ): string {
35
+ const params = new URLSearchParams({
36
+ recordingId,
37
+ token: approvalToken,
38
+ });
39
+ return `/access-request/approve?${params.toString()}`;
40
+ }
41
+
42
+ /** Token-free continuation used after the approver signs in. */
43
+ export function recordingAccessApprovalContinuationPath(
44
+ recordingId: string,
45
+ ): string {
46
+ const params = new URLSearchParams({ recordingId });
47
+ return `/access-request/approve?${params.toString()}`;
48
+ }
49
+
50
+ /** Tab-scoped key for carrying an approval token across the sign-in redirect. */
51
+ export function recordingAccessApprovalSessionKey(recordingId: string): string {
52
+ return `${CLIPS_ACCESS_APPROVAL_SESSION_KEY_PREFIX}${encodeURIComponent(recordingId)}`;
53
+ }
54
+
24
55
  export interface RecordingShareUrlParams {
25
56
  recordingId: string;
26
57
  /** Absolute origin, e.g. `https://clips.example.com` or a desktop serverUrl. */
@@ -51,6 +51,19 @@ export type ShareAttribution = {
51
51
  via: string | undefined;
52
52
  };
53
53
 
54
+ /**
55
+ * Keep only non-sensitive attribution state when a share page redirects to
56
+ * sign-in. Passwords and capability tokens must never enter auth URLs.
57
+ */
58
+ export function buildShareContinuationQuery(
59
+ attribution: ShareAttribution,
60
+ ): string {
61
+ const params = new URLSearchParams();
62
+ if (attribution.ref) params.set(REF_PARAM, attribution.ref);
63
+ if (attribution.via) params.set(VIA_PARAM, attribution.via);
64
+ return params.toString();
65
+ }
66
+
54
67
  /**
55
68
  * Read `ref`/`via` from a query string (e.g. `window.location.search`). Falls
56
69
  * back to `ref=clip_share` so downstream attribution stays meaningful even when
@@ -182,6 +182,11 @@ in the moment of agent use.
182
182
  five-item sidebar preview and a footer row with New chat followed by an
183
183
  ellipsis disclosure up to fifteen. Apps inject routing, labels, and domain
184
184
  actions.
185
+ - **Data grid kit**: provider-agnostic spreadsheet mechanics belong in
186
+ `@agent-native/toolkit/data-grid`. Apps provide rows, typed columns, editor
187
+ slots, selection and width state, persistence callbacks, and product-level
188
+ row/body slots. Keep database models, access checks, grouping, drag/drop,
189
+ and domain actions in the app adapter.
185
190
  - **Agent page kit**: the full-page `/agent` surface (`AgentTabsPage` from
186
191
  `@agent-native/core/client`) with Context, Files, Connections, Jobs, and
187
192
  Access tabs plus a Personal/Organization scope toggle. The canonical home
@@ -184,6 +184,27 @@ import { ShareButton } from "@agent-native/core/client/sharing";
184
184
 
185
185
  For list views, show `<VisibilityBadge visibility={row.visibility} />` next to each resource.
186
186
 
187
+ ## Standard share surface
188
+
189
+ All app share popovers should use the same compact surface contract:
190
+
191
+ - Use the text-only `ShareTrigger` from `@agent-native/toolkit/sharing`.
192
+ - Render ordinary links with `ShareCopyRow`, which exposes a Copy action without
193
+ printing the raw URL.
194
+ - Keep general access and individual people access in the standard Core sharing
195
+ flow. The people flow supports email invites, roles, notifications, and
196
+ removal through the shared share actions.
197
+ - Add `ShareAgentsSection` only when the resource has a real agent-readable
198
+ link or prompt. Keep it collapsed by default and supply domain-specific
199
+ content through the shared section shell.
200
+ - App-specific tabs or controls may remain when they represent a real domain
201
+ action (for example, an embed-code editor), but they should retain the same
202
+ trigger, copy-row, access, and spacing language.
203
+
204
+ `ShareDisclosureSection` is the toolkit-owned shell for optional expandable
205
+ share details; use its `ShareAgentsSection` or `SharePeopleSection` wrappers
206
+ instead of creating another collapsible share panel in a template.
207
+
187
208
  ## Actions available everywhere
188
209
 
189
210
  The framework auto-mounts these actions in every template — no per-template boilerplate:
@@ -696,6 +696,7 @@ export function useBlockFieldEditor({
696
696
  content: string;
697
697
  editorResetVersion: number;
698
698
  onChange: (markdown: string) => void;
699
+ onSaveContent: (markdown: string) => Promise<boolean>;
699
700
  } {
700
701
  const key = `${documentId}:${propertyId}`;
701
702
 
@@ -876,7 +877,16 @@ export function useBlockFieldEditor({
876
877
  controllerRef.current?.change(markdown);
877
878
  }
878
879
 
879
- return { content, editorResetVersion, onChange };
880
+ async function onSaveContent(markdown: string) {
881
+ setContent(markdown);
882
+ const controller = controllerRef.current;
883
+ if (!controller) return false;
884
+ controller.change(markdown);
885
+ await controller.flush();
886
+ return controller.lastSaved === markdown;
887
+ }
888
+
889
+ return { content, editorResetVersion, onChange, onSaveContent };
880
890
  }
881
891
 
882
892
  /**
@@ -904,15 +914,16 @@ function AdditionalBlockEditor({
904
914
  const propertyId = property.definition.id;
905
915
  const initialContent =
906
916
  typeof property.value === "string" ? property.value : "";
907
- const { content, editorResetVersion, onChange } = useBlockFieldEditor({
908
- documentId,
909
- propertyId,
910
- initialContent,
911
- initialRevision: property.blocksField?.revision ?? 0,
912
- save: setProperty.mutateAsync,
913
- onRevisionConflict: () =>
914
- toast.error(t("editor.blocksFieldRevisionConflict")),
915
- });
917
+ const { content, editorResetVersion, onChange, onSaveContent } =
918
+ useBlockFieldEditor({
919
+ documentId,
920
+ propertyId,
921
+ initialContent,
922
+ initialRevision: property.blocksField?.revision ?? 0,
923
+ save: setProperty.mutateAsync,
924
+ onRevisionConflict: () =>
925
+ toast.error(t("editor.blocksFieldRevisionConflict")),
926
+ });
916
927
 
917
928
  return (
918
929
  <VisualEditor
@@ -920,6 +931,7 @@ function AdditionalBlockEditor({
920
931
  documentId={documentId}
921
932
  content={content}
922
933
  onChange={onChange}
934
+ onSaveContent={onSaveContent}
923
935
  editable={canEdit}
924
936
  localFileMode
925
937
  />
@@ -49,6 +49,7 @@ import { cn } from "@/lib/utils";
49
49
  import { localContentComponents } from "@/local-components";
50
50
 
51
51
  import { focusMostRecentEmptyToggleSummary } from "./extensions/NotionExtensions";
52
+ import { createImagePickerId } from "./image-upload";
52
53
  import { buildLocalComponentSlashItems } from "./localComponentSlashItems";
53
54
  import { MathRenderer } from "./MathRenderer";
54
55
  import { buildRegistrySlashItems } from "./registrySlashItems";
@@ -179,7 +180,7 @@ export function insertMediaPlaceholder(
179
180
  ) {
180
181
  const attrs =
181
182
  type === "image"
182
- ? { src: null, alt: "" }
183
+ ? { src: null, alt: "", uploadId: createImagePickerId() }
183
184
  : type === "video"
184
185
  ? { src: null, sourcePanelOpen: true }
185
186
  : { src: null };
@@ -95,9 +95,13 @@ import {
95
95
  hasImageFiles,
96
96
  hasVideoFiles,
97
97
  audioUploadErrorMessage,
98
+ completeImageFileUpload,
99
+ createMediaUploadId,
100
+ ImageRenderError,
98
101
  imageUploadErrorMessage,
99
102
  uploadAudioFile,
100
103
  uploadImageFile,
104
+ waitForRenderedImage,
101
105
  uploadVideoFile,
102
106
  videoUploadErrorMessage,
103
107
  } from "./image-upload";
@@ -1020,6 +1024,7 @@ interface VisualEditorExtensionOptions {
1020
1024
  avatarUrl?: string;
1021
1025
  } | null;
1022
1026
  onImageComment?: (quotedText: string, offsetTop: number) => void;
1027
+ onImageFilePickerRequest?: (request: PendingImagePicker) => void;
1023
1028
  onJoinTitle?: (text: string) => void;
1024
1029
  resolveNotionPageLink?: (notionPageId: string) => NotionPageLink | null;
1025
1030
  onOpenNotionPageLink?: (documentId: string) => void;
@@ -1143,19 +1148,17 @@ function mediaNodeLabel(typeName: MediaNodeType) {
1143
1148
  return "Audio";
1144
1149
  }
1145
1150
 
1146
- function createMediaUploadId(kind: MediaNodeType) {
1147
- const random =
1148
- typeof crypto !== "undefined" && "randomUUID" in crypto
1149
- ? crypto.randomUUID()
1150
- : Math.random().toString(36).slice(2);
1151
- return `${kind}-upload-${random}`;
1152
- }
1153
-
1154
1151
  interface PendingMediaUpload {
1155
1152
  file: File;
1156
1153
  uploadId: string;
1157
1154
  }
1158
1155
 
1156
+ export interface PendingImagePicker {
1157
+ pickerId: string;
1158
+ position: number;
1159
+ attrs: Record<string, unknown>;
1160
+ }
1161
+
1159
1162
  function insertPendingMediaNodes(
1160
1163
  view: EditorView,
1161
1164
  typeName: MediaNodeType,
@@ -1204,7 +1207,7 @@ function updatePendingMediaNode(
1204
1207
  tr = tr.setNodeMarkup(pos, undefined, {
1205
1208
  ...node.attrs,
1206
1209
  ...attrs,
1207
- uploadId: null,
1210
+ uploadId: attrs.uploadId ?? null,
1208
1211
  });
1209
1212
  found = true;
1210
1213
  return false;
@@ -1218,6 +1221,130 @@ function updatePendingMediaNode(
1218
1221
  return found;
1219
1222
  }
1220
1223
 
1224
+ function replacePendingMediaUploadId(
1225
+ view: EditorView,
1226
+ typeName: MediaNodeType,
1227
+ currentUploadId: string,
1228
+ nextUploadId: string,
1229
+ ) {
1230
+ let found = false;
1231
+ let tr = view.state.tr;
1232
+
1233
+ view.state.doc.descendants((node, pos) => {
1234
+ if (found) return false;
1235
+ if (
1236
+ node.type.name === typeName &&
1237
+ node.attrs.uploadId === currentUploadId
1238
+ ) {
1239
+ tr = tr.setNodeMarkup(pos, undefined, {
1240
+ ...node.attrs,
1241
+ uploadId: nextUploadId,
1242
+ });
1243
+ found = true;
1244
+ return false;
1245
+ }
1246
+ return true;
1247
+ });
1248
+
1249
+ if (found) view.dispatch(tr);
1250
+ return found;
1251
+ }
1252
+
1253
+ function insertImageNodeAtPendingPosition(
1254
+ view: EditorView,
1255
+ request: PendingImagePicker,
1256
+ attrs: Record<string, unknown>,
1257
+ restoreSelection: boolean,
1258
+ ) {
1259
+ const imageType = view.state.schema.nodes.image;
1260
+ if (!imageType) return false;
1261
+ const position = Math.min(
1262
+ Math.max(request.position, 0),
1263
+ view.state.doc.content.size,
1264
+ );
1265
+ try {
1266
+ let tr = view.state.tr.insert(
1267
+ position,
1268
+ imageType.create({ ...request.attrs, ...attrs }),
1269
+ );
1270
+ if (restoreSelection) {
1271
+ tr = tr.setSelection(NodeSelection.create(tr.doc, position));
1272
+ }
1273
+ view.dispatch(tr.scrollIntoView());
1274
+ if (restoreSelection) view.focus();
1275
+ return true;
1276
+ } catch (error) {
1277
+ console.error("Could not restore the pending image node:", error);
1278
+ return false;
1279
+ }
1280
+ }
1281
+
1282
+ export function ensurePendingImageUpload(
1283
+ view: EditorView,
1284
+ request: PendingImagePicker,
1285
+ uploadId: string,
1286
+ ) {
1287
+ if (replacePendingMediaUploadId(view, "image", request.pickerId, uploadId)) {
1288
+ return true;
1289
+ }
1290
+ return insertImageNodeAtPendingPosition(view, request, { uploadId }, false);
1291
+ }
1292
+
1293
+ export function commitPendingImageUpload(
1294
+ view: EditorView,
1295
+ request: PendingImagePicker,
1296
+ uploadId: string,
1297
+ attrs: Record<string, unknown>,
1298
+ ) {
1299
+ if (updatePendingMediaNode(view, "image", uploadId, attrs)) return true;
1300
+ return insertImageNodeAtPendingPosition(view, request, attrs, false);
1301
+ }
1302
+
1303
+ function findPendingImageElement(view: EditorView, uploadId: string) {
1304
+ return (
1305
+ Array.from(view.dom.querySelectorAll<HTMLElement>("[data-image-upload-id]"))
1306
+ .find((element) => element.dataset.imageUploadId === uploadId)
1307
+ ?.querySelector<HTMLImageElement>("img") ?? null
1308
+ );
1309
+ }
1310
+
1311
+ export function restorePendingImagePicker(
1312
+ view: EditorView,
1313
+ request: PendingImagePicker,
1314
+ currentUploadId = request.pickerId,
1315
+ ) {
1316
+ let found = false;
1317
+ let nodePosition: number | null = null;
1318
+ let tr = view.state.tr;
1319
+
1320
+ view.state.doc.descendants((node, pos) => {
1321
+ if (found) return false;
1322
+ if (node.type.name === "image" && node.attrs.uploadId === currentUploadId) {
1323
+ tr = tr.setNodeMarkup(pos, undefined, {
1324
+ ...request.attrs,
1325
+ uploadId: null,
1326
+ });
1327
+ nodePosition = pos;
1328
+ found = true;
1329
+ return false;
1330
+ }
1331
+ return true;
1332
+ });
1333
+
1334
+ if (found && nodePosition !== null) {
1335
+ tr = tr.setSelection(NodeSelection.create(tr.doc, nodePosition));
1336
+ view.dispatch(tr);
1337
+ view.focus();
1338
+ return true;
1339
+ }
1340
+ return insertImageNodeAtPendingPosition(
1341
+ view,
1342
+ request,
1343
+ { uploadId: null },
1344
+ true,
1345
+ );
1346
+ }
1347
+
1221
1348
  function getVisualEditorPlaceholder({
1222
1349
  editor,
1223
1350
  node,
@@ -1499,6 +1626,7 @@ export function createVisualEditorExtensions({
1499
1626
  localAwareness,
1500
1627
  user,
1501
1628
  onImageComment,
1629
+ onImageFilePickerRequest,
1502
1630
  onJoinTitle,
1503
1631
  resolveNotionPageLink,
1504
1632
  onOpenNotionPageLink,
@@ -1558,6 +1686,7 @@ export function createVisualEditorExtensions({
1558
1686
  HTMLAttributes: { class: "notion-image" },
1559
1687
  documentId,
1560
1688
  onImageComment,
1689
+ onImageFilePickerRequest,
1561
1690
  }),
1562
1691
  VideoNode.configure({
1563
1692
  HTMLAttributes: { class: "notion-video" },
@@ -1880,6 +2009,8 @@ export function VisualEditor({
1880
2009
  const t = useT();
1881
2010
  const [isDraggingMedia, setIsDraggingMedia] = useState(false);
1882
2011
  const wrapperRef = useRef<HTMLDivElement>(null);
2012
+ const imageFileInputRef = useRef<HTMLInputElement>(null);
2013
+ const pendingImagePickerRef = useRef<PendingImagePicker | null>(null);
1883
2014
  const onChangeRef = useRef(onChange);
1884
2015
  onChangeRef.current = onChange;
1885
2016
  const onSaveContentRef = useRef(onSaveContent);
@@ -1895,6 +2026,14 @@ export function VisualEditor({
1895
2026
  },
1896
2027
  [],
1897
2028
  );
2029
+ const onImageFilePickerRequest = useCallback(
2030
+ (request: PendingImagePicker) => {
2031
+ if (pendingImagePickerRef.current) return;
2032
+ pendingImagePickerRef.current = request;
2033
+ imageFileInputRef.current?.click();
2034
+ },
2035
+ [],
2036
+ );
1898
2037
  const resolveNotionPageLink = useCallback((notionPageId: string) => {
1899
2038
  const normalized = notionPageId.replace(/-/g, "").toLowerCase();
1900
2039
  return (
@@ -1946,6 +2085,7 @@ export function VisualEditor({
1946
2085
  localAwareness,
1947
2086
  user,
1948
2087
  onImageComment: onComment,
2088
+ onImageFilePickerRequest,
1949
2089
  onJoinTitle,
1950
2090
  resolveNotionPageLink,
1951
2091
  onOpenNotionPageLink,
@@ -1962,6 +2102,7 @@ export function VisualEditor({
1962
2102
  user?.email,
1963
2103
  user?.color,
1964
2104
  onComment,
2105
+ onImageFilePickerRequest,
1965
2106
  onJoinTitle,
1966
2107
  resolveNotionPageLink,
1967
2108
  onOpenNotionPageLink,
@@ -2014,6 +2155,7 @@ export function VisualEditor({
2014
2155
  if (options?.immediate && onSaveContentRef.current) {
2015
2156
  return onSaveContentRef.current(normalized);
2016
2157
  }
2158
+ if (options?.immediate) return false;
2017
2159
  // Don't persist an empty doc before Collaboration has seeded (would
2018
2160
  // clobber DB content with an empty string). `registerEmitted` records
2019
2161
  // this as the last-emitted value and returns false to skip the save.
@@ -2225,6 +2367,93 @@ export function VisualEditor({
2225
2367
  },
2226
2368
  });
2227
2369
 
2370
+ const handleImageFileInputChange = useCallback(
2371
+ async (event: Event) => {
2372
+ const input = event.currentTarget as HTMLInputElement;
2373
+ const file = input.files?.[0];
2374
+ input.value = "";
2375
+ const request = pendingImagePickerRef.current;
2376
+ pendingImagePickerRef.current = null;
2377
+ if (!editor || !file || !request) return;
2378
+
2379
+ const uploadId = createMediaUploadId("image");
2380
+ if (!ensurePendingImageUpload(editor.view, request, uploadId)) return;
2381
+
2382
+ const toastId = toast.loading(t("editor.media.uploadingImage"));
2383
+ let staged = false;
2384
+ let committed = false;
2385
+ try {
2386
+ await completeImageFileUpload({
2387
+ file,
2388
+ stageAttributes: (src) => {
2389
+ if (editor.isDestroyed) return;
2390
+ staged = commitPendingImageUpload(editor.view, request, uploadId, {
2391
+ src,
2392
+ uploadId,
2393
+ });
2394
+ },
2395
+ waitForRender: async () => {
2396
+ if (!staged) throw new Error(t("empty.genericError"));
2397
+ await waitForRenderedImage(() =>
2398
+ findPendingImageElement(editor.view, uploadId),
2399
+ );
2400
+ },
2401
+ commitAttributes: (src) => {
2402
+ if (editor.isDestroyed) return;
2403
+ committed = commitPendingImageUpload(
2404
+ editor.view,
2405
+ request,
2406
+ uploadId,
2407
+ { src, uploadId: null },
2408
+ );
2409
+ },
2410
+ persistCommittedImage: async () => {
2411
+ if (!committed || editor.isDestroyed) return false;
2412
+ return await persistEditorContent(editor, {
2413
+ immediate: true,
2414
+ userInitiated: true,
2415
+ });
2416
+ },
2417
+ });
2418
+ if (!committed) throw new Error(t("empty.genericError"));
2419
+ toast.success(t("editor.media.imageAdded"), { id: toastId });
2420
+ } catch (error) {
2421
+ if (editor.isDestroyed) {
2422
+ toast.dismiss(toastId);
2423
+ return;
2424
+ }
2425
+ if (!committed) {
2426
+ restorePendingImagePicker(editor.view, request, uploadId);
2427
+ }
2428
+ toast.error(
2429
+ error instanceof ImageRenderError
2430
+ ? t("editor.media.imageBroken")
2431
+ : imageUploadErrorMessage(error),
2432
+ { id: toastId },
2433
+ );
2434
+ }
2435
+ },
2436
+ [editor, t],
2437
+ );
2438
+
2439
+ const handleImageFileInputCancel = useCallback(() => {
2440
+ const request = pendingImagePickerRef.current;
2441
+ pendingImagePickerRef.current = null;
2442
+ if (!editor || !request) return;
2443
+ restorePendingImagePicker(editor.view, request);
2444
+ }, [editor]);
2445
+
2446
+ useEffect(() => {
2447
+ const input = imageFileInputRef.current;
2448
+ if (!input) return;
2449
+ input.addEventListener("change", handleImageFileInputChange);
2450
+ input.addEventListener("cancel", handleImageFileInputCancel);
2451
+ return () => {
2452
+ input.removeEventListener("change", handleImageFileInputChange);
2453
+ input.removeEventListener("cancel", handleImageFileInputCancel);
2454
+ };
2455
+ }, [editor, handleImageFileInputCancel, handleImageFileInputChange]);
2456
+
2228
2457
  // The shared seed / reconcile / lead-client / onUpdate-guard logic, with
2229
2458
  // Content's NFM serializer injected so the editor reads/writes the exact same
2230
2459
  // bytes as before (docToNfm / nfmToDoc / canonicalizeNfm, and the
@@ -2563,6 +2792,13 @@ export function VisualEditor({
2563
2792
  <RegistryBlockDataProvider value={registryBlockDataValue}>
2564
2793
  <EditorContent editor={editor} />
2565
2794
  </RegistryBlockDataProvider>
2795
+ <input
2796
+ ref={imageFileInputRef}
2797
+ type="file"
2798
+ className="hidden"
2799
+ tabIndex={-1}
2800
+ aria-hidden="true"
2801
+ />
2566
2802
  </div>
2567
2803
  );
2568
2804
  }