@agent-native/core 0.157.2 → 0.157.4

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 (167) 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/actions/add-database-item.ts +9 -0
  36. package/corpus/templates/content/actions/describe-content-database.ts +92 -0
  37. package/corpus/templates/content/actions/list-content-databases.ts +213 -72
  38. package/corpus/templates/content/actions/set-document-property.ts +9 -0
  39. package/corpus/templates/content/actions/submit-content-database-form.ts +9 -0
  40. package/corpus/templates/content/actions/update-document.ts +9 -0
  41. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +22 -10
  42. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +2 -1
  43. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +245 -9
  44. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +192 -134
  45. package/corpus/templates/content/app/components/editor/database/settings.tsx +7 -0
  46. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +61 -45
  47. package/corpus/templates/content/app/components/editor/extensions/ImageNode.ts +6 -0
  48. package/corpus/templates/content/app/components/editor/image-upload.ts +137 -5
  49. package/corpus/templates/content/app/global.css +8 -0
  50. package/corpus/templates/content/app/hooks/use-content-database.ts +91 -11
  51. package/corpus/templates/content/changelog/2026-08-12-image-uploads-now-confirm-the-image-can-be-displayed-before-.md +6 -0
  52. package/corpus/templates/content/changelog/2026-08-14-images-fill-the-page-width-by-default-and-remain-visible-whe.md +6 -0
  53. package/corpus/templates/content/changelog/2026-08-14-images-inserted-from-the-slash-command-stay-saved-after-uplo.md +6 -0
  54. package/corpus/templates/content/changelog/2026-08-14-svg-files-can-be-selected-reliably-from-the-image-picker.md +6 -0
  55. package/corpus/templates/content/parity/matrix.md +1 -1
  56. package/corpus/templates/content/parity/matrix.ts +7 -1
  57. package/corpus/templates/content/server/plugins/agent-chat.ts +3 -0
  58. package/corpus/templates/content/shared/api.ts +9 -0
  59. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  60. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +21 -0
  61. package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  62. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +21 -0
  63. package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  64. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +21 -0
  65. package/corpus/templates/factory/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  66. package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +21 -0
  67. package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  68. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +21 -0
  69. package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  70. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +21 -0
  71. package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  72. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +21 -0
  73. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -1
  74. package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  75. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +21 -0
  76. package/corpus/templates/plan/app/pages/PlansPage.tsx +25 -30
  77. package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  78. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +21 -0
  79. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +2 -1
  80. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +124 -180
  81. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +1 -0
  82. package/corpus/templates/slides/app/global.css +99 -3
  83. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  84. package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  85. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +21 -0
  86. package/dist/chat-threads/schema.d.ts +1 -1
  87. package/dist/cli/code-agent-executor.js +318 -0
  88. package/dist/client/AgentPanel.d.ts +13 -1
  89. package/dist/client/AgentPanel.js +41 -3
  90. package/dist/client/AssistantChat.d.ts +9 -1
  91. package/dist/client/AssistantChat.js +26 -13
  92. package/dist/client/MultiTabAssistantChat.js +18 -5
  93. package/dist/client/agent-chat.d.ts +8 -0
  94. package/dist/client/agent-chat.js +15 -0
  95. package/dist/client/app-chat-sidebar.d.ts +23 -0
  96. package/dist/client/app-chat-sidebar.js +53 -0
  97. package/dist/client/app-providers.js +31 -2
  98. package/dist/client/frame-protocol.d.ts +20 -2
  99. package/dist/client/hooks/index.d.ts +1 -0
  100. package/dist/client/hooks/index.js +1 -0
  101. package/dist/client/sharing/AgentShareSection.d.ts +8 -0
  102. package/dist/client/sharing/AgentShareSection.js +50 -0
  103. package/dist/client/sharing/ShareButton.js +2 -1
  104. package/dist/client/sharing/ShareDialog.d.ts +2 -2
  105. package/dist/client/sharing/ShareDialog.js +21 -29
  106. package/dist/client/sharing/index.d.ts +1 -0
  107. package/dist/client/sharing/index.js +1 -0
  108. package/dist/client/sharing/useShareButtonController.d.ts +1 -0
  109. package/dist/client/sharing/useShareDialogController.d.ts +3 -0
  110. package/dist/client/sharing/useShareDialogController.js +2 -1
  111. package/dist/client/theme.d.ts +21 -0
  112. package/dist/client/theme.js +60 -0
  113. package/dist/client/ui/index.d.ts +1 -1
  114. package/dist/client/ui/index.js +1 -1
  115. package/dist/client/use-session.js +23 -0
  116. package/dist/collab/awareness.d.ts +2 -2
  117. package/dist/collab/routes.d.ts +1 -1
  118. package/dist/collab/struct-routes.d.ts +1 -1
  119. package/dist/dashboard-storage/schema.d.ts +1 -1
  120. package/dist/data-programs/schema.d.ts +1 -1
  121. package/dist/extensions/schema.d.ts +1 -1
  122. package/dist/localization/core-messages/ar-SA.js +6 -0
  123. package/dist/localization/core-messages/de-DE.js +6 -0
  124. package/dist/localization/core-messages/en-US.d.ts +6 -0
  125. package/dist/localization/core-messages/en-US.js +6 -0
  126. package/dist/localization/core-messages/es-ES.js +6 -0
  127. package/dist/localization/core-messages/fr-FR.js +6 -0
  128. package/dist/localization/core-messages/hi-IN.js +6 -0
  129. package/dist/localization/core-messages/ja-JP.js +6 -0
  130. package/dist/localization/core-messages/ko-KR.js +6 -0
  131. package/dist/localization/core-messages/pt-BR.js +6 -0
  132. package/dist/localization/core-messages/zh-CN.js +6 -0
  133. package/dist/localization/core-messages/zh-TW.js +6 -0
  134. package/dist/localization/core-messages.d.ts +6 -0
  135. package/dist/localization/default-messages.d.ts +1 -0
  136. package/dist/localization/default-messages.js +1 -0
  137. package/dist/notifications/routes.d.ts +3 -3
  138. package/dist/observability/routes.d.ts +5 -5
  139. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  140. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  141. package/dist/secrets/routes.d.ts +9 -9
  142. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  143. package/dist/server/auth.js +8 -5
  144. package/dist/server/identity-sso-store.d.ts +6 -1
  145. package/dist/server/identity-sso-store.js +57 -14
  146. package/dist/server/identity-sso.d.ts +7 -8
  147. package/dist/server/identity-sso.js +15 -12
  148. package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
  149. package/dist/sharing/actions/list-resource-shares.js +1 -0
  150. package/dist/sharing/schema.d.ts +1 -1
  151. package/dist/sharing/schema.js +4 -1
  152. package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  153. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  154. package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  155. package/dist/templates/default/.agents/skills/sharing/SKILL.md +21 -0
  156. package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  157. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  158. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +21 -0
  159. package/docs/content/cross-app-sso.mdx +24 -17
  160. package/package.json +3 -3
  161. package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  162. package/src/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  163. package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  164. package/src/templates/default/.agents/skills/sharing/SKILL.md +21 -0
  165. package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  166. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  167. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +21 -0
@@ -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:
@@ -26,6 +26,15 @@ const schema = databaseMutationEnvelopeSchema.extend({
26
26
  export default defineAction({
27
27
  description:
28
28
  "Create one row in an exact ordinary Content database using its discovered schema revision. Strictly validates every non-Blocks property, applies the side effect once per idempotency key, and returns a verified receipt with stable row identity.",
29
+ publicAgent: {
30
+ expose: true,
31
+ readOnly: false,
32
+ requiresAuth: true,
33
+ isConsequential: true,
34
+ title: "Add Content Database Item",
35
+ description:
36
+ "Delegate creation of one page item in an existing Content database.",
37
+ },
29
38
  schema,
30
39
  audit: {
31
40
  recordInputs: false,
@@ -0,0 +1,92 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { accessFilter } from "@agent-native/core/sharing";
3
+ import { and, eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import { getDocumentContextPath } from "../server/lib/document-context.js";
8
+ import type {
9
+ ContentDatabaseDescriptionResponse,
10
+ ContentDatabaseUnavailableResponse,
11
+ } from "../shared/api.js";
12
+ import { resolveContentDatabaseRead } from "./_database-utils.js";
13
+ import {
14
+ listPropertiesForDatabase,
15
+ serializeDatabase,
16
+ } from "./_property-utils.js";
17
+ import listContentDatabases, {
18
+ ContentDatabaseResolutionError,
19
+ } from "./list-content-databases.js";
20
+
21
+ export default defineAction({
22
+ description:
23
+ "Describe one exact ordinary Content database, including its live metadata, views, and property schema but not its rows. Resolve the stable database or document ID with list-content-databases first.",
24
+ schema: z
25
+ .object({
26
+ databaseId: z.string().min(1).optional().describe("Exact database ID"),
27
+ documentId: z
28
+ .string()
29
+ .min(1)
30
+ .optional()
31
+ .describe("Exact database document/page ID"),
32
+ })
33
+ .refine(
34
+ (input) => Boolean(input.databaseId) !== Boolean(input.documentId),
35
+ "Provide exactly one of databaseId or documentId.",
36
+ ),
37
+ http: { method: "GET" },
38
+ readOnly: true,
39
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
40
+ run: async ({
41
+ databaseId,
42
+ documentId,
43
+ }): Promise<
44
+ ContentDatabaseDescriptionResponse | ContentDatabaseUnavailableResponse
45
+ > => {
46
+ let selection: Awaited<ReturnType<typeof listContentDatabases.run>>;
47
+ try {
48
+ selection = await listContentDatabases.run({ databaseId, documentId });
49
+ } catch (error) {
50
+ if (!(error instanceof ContentDatabaseResolutionError)) throw error;
51
+ throw new Error("Content database not found.");
52
+ }
53
+ const selected = selection.databases[0];
54
+ if (!selected) throw new Error("Content database not found.");
55
+
56
+ const resolved = await resolveContentDatabaseRead({
57
+ databaseId: selected.databaseId,
58
+ });
59
+ if (!resolved.available) return resolved;
60
+ if (resolved.database.systemRole) {
61
+ throw new Error("Content database not found.");
62
+ }
63
+
64
+ const db = getDb();
65
+ const [databaseDocument] = await db
66
+ .select({
67
+ id: schema.documents.id,
68
+ parentId: schema.documents.parentId,
69
+ })
70
+ .from(schema.documents)
71
+ .where(
72
+ and(
73
+ eq(schema.documents.id, selected.documentId),
74
+ accessFilter(schema.documents, schema.documentShares),
75
+ ),
76
+ );
77
+ if (!databaseDocument) throw new Error("Content database not found.");
78
+
79
+ const [properties, contextPath] = await Promise.all([
80
+ listPropertiesForDatabase(resolved.database.id),
81
+ getDocumentContextPath(databaseDocument),
82
+ ]);
83
+ return {
84
+ database: serializeDatabase(
85
+ { ...resolved.database, title: selected.title },
86
+ selected.description,
87
+ ),
88
+ contextPath,
89
+ properties,
90
+ };
91
+ },
92
+ });
@@ -1,20 +1,55 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { accessFilter } from "@agent-native/core/sharing";
3
- import { and, asc, eq, isNull, ne, sql } from "drizzle-orm";
3
+ import {
4
+ and,
5
+ asc,
6
+ eq,
7
+ inArray,
8
+ isNull,
9
+ notInArray,
10
+ or,
11
+ sql,
12
+ } from "drizzle-orm";
4
13
  import { z } from "zod";
5
14
 
6
15
  import { getDb, schema } from "../server/db/index.js";
7
16
  import { documentDiscoveryFilter } from "../server/lib/documents.js";
8
17
  import type { ListContentDatabasesResponse } from "../shared/api.js";
18
+ import { documentDiscoveryPagination } from "./_document-discovery-query.js";
19
+
20
+ const DEFAULT_CONTENT_DATABASE_DISCOVERY_LIMIT = 50;
9
21
 
10
22
  function escapeLike(s: string): string {
11
23
  return s.replace(/([\\%_])/g, "\\$1");
12
24
  }
13
25
 
26
+ export class ContentDatabaseResolutionError extends Error {}
27
+
14
28
  export default defineAction({
15
29
  description:
16
- "List the content databases the user can access (owned, shared, or org-shared — matching the sidebar) so any of them can be used as a local-table source. Optionally filters by title or excludes one database (e.g. the one being configured).",
30
+ "Discover one bounded page of ordinary Content databases the user can access from their live title and user-authored description. Returns stable database, document, and space IDs with explicit pagination; follow nextOffset until hasMore is false. Use exact filters before reading a selected database's schema.",
17
31
  schema: z.object({
32
+ spaceId: z
33
+ .string()
34
+ .min(1)
35
+ .optional()
36
+ .describe("Exact Content space ID to search within."),
37
+ databaseId: z
38
+ .string()
39
+ .min(1)
40
+ .optional()
41
+ .describe("Exact Content database ID to resolve."),
42
+ documentId: z
43
+ .string()
44
+ .min(1)
45
+ .optional()
46
+ .describe("Exact Content database document/page ID to resolve."),
47
+ title: z
48
+ .string()
49
+ .trim()
50
+ .min(1)
51
+ .optional()
52
+ .describe("Exact live database title to resolve, case-insensitively."),
18
53
  excludeDatabaseId: z
19
54
  .string()
20
55
  .optional()
@@ -23,63 +58,43 @@ export default defineAction({
23
58
  .array(z.string())
24
59
  .optional()
25
60
  .describe("Database ids to omit from the results."),
26
- query: z.string().optional().describe("Optional title search text."),
61
+ query: z
62
+ .string()
63
+ .optional()
64
+ .describe("Optional title or user-authored description search text."),
27
65
  limit: z.coerce
28
66
  .number()
29
67
  .int()
30
68
  .min(1)
31
69
  .max(50)
32
- .optional()
33
- .describe("Maximum number of databases to return."),
70
+ .default(DEFAULT_CONTENT_DATABASE_DISCOVERY_LIMIT)
71
+ .describe("Maximum number of databases to return. Defaults to 50."),
72
+ offset: z.coerce
73
+ .number()
74
+ .int()
75
+ .min(0)
76
+ .default(0)
77
+ .describe("Zero-based continuation offset."),
34
78
  }),
35
79
  http: { method: "GET" },
36
80
  readOnly: true,
81
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
37
82
  run: async (args): Promise<ListContentDatabasesResponse> => {
38
83
  const db = getDb();
39
84
  const query = args.query?.trim();
40
85
  const pattern = query ? `%${escapeLike(query.toLowerCase())}%` : null;
86
+ const exactTitle = args.title?.toLowerCase();
87
+ const resolvesExactly = !!(
88
+ args.databaseId ||
89
+ args.documentId ||
90
+ exactTitle
91
+ );
41
92
  const excludedDatabaseIds = new Set(
42
93
  [
43
94
  args.excludeDatabaseId?.trim(),
44
95
  ...(args.excludeDatabaseIds ?? []).map((id) => id.trim()),
45
96
  ].filter((id): id is string => !!id),
46
97
  );
47
- // The same access + discovery filter the sidebar uses, so the picker shows
48
- // owned AND shared/org databases and never a trashed/hidden one.
49
- const queryBuilder = db
50
- .select({
51
- id: schema.contentDatabases.id,
52
- documentId: schema.contentDatabases.documentId,
53
- title: schema.documents.title,
54
- })
55
- .from(schema.contentDatabases)
56
- .innerJoin(
57
- schema.documents,
58
- eq(schema.contentDatabases.documentId, schema.documents.id),
59
- )
60
- .where(
61
- and(
62
- accessFilter(schema.documents, schema.documentShares),
63
- isNull(schema.documents.trashedAt),
64
- documentDiscoveryFilter(),
65
- isNull(schema.contentDatabases.deletedAt),
66
- excludedDatabaseIds.size === 1
67
- ? ne(
68
- schema.contentDatabases.id,
69
- Array.from(excludedDatabaseIds)[0]!,
70
- )
71
- : undefined,
72
- pattern
73
- ? sql`lower(${schema.documents.title}) LIKE ${pattern} ESCAPE '\\'`
74
- : undefined,
75
- ),
76
- )
77
- .orderBy(asc(schema.documents.position));
78
-
79
- const rows = args.limit
80
- ? await queryBuilder.limit(args.limit)
81
- : await queryBuilder;
82
-
83
98
  const localTableSources =
84
99
  excludedDatabaseIds.size > 0
85
100
  ? await db
@@ -90,39 +105,165 @@ export default defineAction({
90
105
  .from(schema.contentDatabaseSources)
91
106
  .where(eq(schema.contentDatabaseSources.sourceType, "local-table"))
92
107
  : [];
93
- const localTableTargetByDatabaseId = new Map(
94
- localTableSources.map((source) => [
95
- source.databaseId,
96
- source.sourceTable,
97
- ]),
98
- );
99
- const sourceChainIncludesExcludedDatabase = (databaseId: string) => {
100
- const seen = new Set<string>();
101
- let current: string | undefined = databaseId;
102
- while (current && !seen.has(current)) {
103
- if (excludedDatabaseIds.has(current)) return true;
104
- seen.add(current);
105
- current = localTableTargetByDatabaseId.get(current);
108
+ const excludedDatabaseRows =
109
+ excludedDatabaseIds.size > 0
110
+ ? await db
111
+ .select({ id: schema.contentDatabases.id })
112
+ .from(schema.contentDatabases)
113
+ .innerJoin(
114
+ schema.documents,
115
+ eq(schema.contentDatabases.documentId, schema.documents.id),
116
+ )
117
+ .where(
118
+ and(
119
+ accessFilter(schema.documents, schema.documentShares),
120
+ or(
121
+ inArray(
122
+ schema.contentDatabases.id,
123
+ Array.from(excludedDatabaseIds),
124
+ ),
125
+ inArray(
126
+ schema.contentDatabases.documentId,
127
+ Array.from(excludedDatabaseIds),
128
+ ),
129
+ ),
130
+ ),
131
+ )
132
+ : [];
133
+ const excludedSourceChainDatabaseIds = new Set([
134
+ ...excludedDatabaseIds,
135
+ ...excludedDatabaseRows.map((row) => row.id),
136
+ ]);
137
+ let expandedSourceChain = true;
138
+ while (expandedSourceChain) {
139
+ expandedSourceChain = false;
140
+ for (const source of localTableSources) {
141
+ if (
142
+ excludedSourceChainDatabaseIds.has(source.sourceTable) &&
143
+ !excludedSourceChainDatabaseIds.has(source.databaseId)
144
+ ) {
145
+ excludedSourceChainDatabaseIds.add(source.databaseId);
146
+ expandedSourceChain = true;
147
+ }
106
148
  }
107
- return false;
108
- };
149
+ }
109
150
 
110
- const databases = rows
111
- // Exclusion ids may be database ids OR database document ids — the
112
- // settings panel only has the document id before any source exists.
113
- .filter(
114
- (row) =>
115
- !excludedDatabaseIds.has(row.documentId) &&
116
- !sourceChainIncludesExcludedDatabase(row.id),
117
- )
118
- .map((row) => ({
119
- databaseId: row.id,
120
- documentId: row.documentId,
121
- // The document's live title (matches the sidebar) rather than the
122
- // possibly-stale content_databases.title.
123
- title: row.title ?? "Untitled database",
124
- }));
151
+ // The same access + discovery filter the sidebar uses, so the picker shows
152
+ // owned AND shared/org databases and never a trashed/hidden one. Resolve
153
+ // source-chain exclusions before limiting so every page is truthfully full.
154
+ const where = and(
155
+ accessFilter(schema.documents, schema.documentShares),
156
+ isNull(schema.documents.trashedAt),
157
+ documentDiscoveryFilter(),
158
+ or(
159
+ eq(schema.documents.hideFromSearch, 0),
160
+ isNull(schema.documents.hideFromSearch),
161
+ ),
162
+ isNull(schema.contentDatabases.deletedAt),
163
+ isNull(schema.contentDatabases.systemRole),
164
+ args.spaceId
165
+ ? eq(schema.contentDatabases.spaceId, args.spaceId)
166
+ : undefined,
167
+ args.databaseId
168
+ ? eq(schema.contentDatabases.id, args.databaseId)
169
+ : undefined,
170
+ args.documentId
171
+ ? eq(schema.contentDatabases.documentId, args.documentId)
172
+ : undefined,
173
+ exactTitle
174
+ ? sql`lower(${schema.documents.title}) = ${exactTitle}`
175
+ : undefined,
176
+ excludedSourceChainDatabaseIds.size > 0
177
+ ? notInArray(
178
+ schema.contentDatabases.id,
179
+ Array.from(excludedSourceChainDatabaseIds),
180
+ )
181
+ : undefined,
182
+ excludedDatabaseIds.size > 0
183
+ ? notInArray(
184
+ schema.contentDatabases.documentId,
185
+ Array.from(excludedDatabaseIds),
186
+ )
187
+ : undefined,
188
+ pattern
189
+ ? or(
190
+ sql`lower(${schema.documents.title}) LIKE ${pattern} ESCAPE '\\'`,
191
+ sql`lower(${schema.documents.description}) LIKE ${pattern} ESCAPE '\\'`,
192
+ )
193
+ : undefined,
194
+ );
195
+ const baseQuery = () =>
196
+ db
197
+ .select({
198
+ id: schema.contentDatabases.id,
199
+ documentId: schema.contentDatabases.documentId,
200
+ title: schema.documents.title,
201
+ description: schema.documents.description,
202
+ spaceId: schema.contentDatabases.spaceId,
203
+ })
204
+ .from(schema.contentDatabases)
205
+ .innerJoin(
206
+ schema.documents,
207
+ eq(schema.contentDatabases.documentId, schema.documents.id),
208
+ )
209
+ .where(where)
210
+ .orderBy(
211
+ asc(schema.documents.position),
212
+ asc(schema.contentDatabases.id),
213
+ );
214
+
215
+ // Two visible matches are sufficient to reject an exact selector without
216
+ // materializing every duplicate-title row.
217
+ const rows = resolvesExactly
218
+ ? await baseQuery().limit(2)
219
+ : await baseQuery().limit(args.limit).offset(args.offset);
220
+
221
+ if (resolvesExactly && rows.length !== 1) {
222
+ const selector = args.databaseId
223
+ ? `database ID "${args.databaseId}"`
224
+ : args.documentId
225
+ ? `document ID "${args.documentId}"`
226
+ : `title "${args.title?.trim()}"`;
227
+ throw new ContentDatabaseResolutionError(
228
+ rows.length === 0
229
+ ? `No accessible Content database matched exact ${selector}.`
230
+ : `Exact ${selector} is ambiguous across multiple accessible Content databases.`,
231
+ );
232
+ }
125
233
 
126
- return { databases };
234
+ const databases = rows.map((row) => ({
235
+ databaseId: row.id,
236
+ documentId: row.documentId,
237
+ spaceId: row.spaceId,
238
+ // The document's live title (matches the sidebar) rather than the
239
+ // possibly-stale content_databases.title.
240
+ title: row.title ?? "Untitled database",
241
+ description: row.description,
242
+ }));
243
+
244
+ const totalItems = resolvesExactly
245
+ ? databases.length
246
+ : Number(
247
+ (
248
+ await db
249
+ .select({ count: sql<number>`count(*)` })
250
+ .from(schema.contentDatabases)
251
+ .innerJoin(
252
+ schema.documents,
253
+ eq(schema.contentDatabases.documentId, schema.documents.id),
254
+ )
255
+ .where(where)
256
+ )[0]?.count ?? 0,
257
+ );
258
+
259
+ return {
260
+ databases,
261
+ pagination: documentDiscoveryPagination({
262
+ offset: resolvesExactly ? 0 : args.offset,
263
+ limit: args.limit,
264
+ totalItems,
265
+ returnedItems: databases.length,
266
+ }),
267
+ };
127
268
  },
128
269
  });