@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
@@ -28,6 +28,15 @@ import {
28
28
 
29
29
  export default defineAction({
30
30
  description: "Set a Notion-style property value on a document.",
31
+ publicAgent: {
32
+ expose: true,
33
+ readOnly: false,
34
+ requiresAuth: true,
35
+ isConsequential: true,
36
+ title: "Set Content Document Property",
37
+ description:
38
+ "Delegate one property update on an existing Content database document.",
39
+ },
31
40
  schema: z.object({
32
41
  documentId: z.string().describe("Document ID (required)"),
33
42
  databaseId: z
@@ -195,6 +195,15 @@ function resolveSubmittedProperties(
195
195
  export default defineAction({
196
196
  description:
197
197
  "Submit one row through a Content database form. Validates that form's required questions, resolves option labels safely, writes the title, Blocks, and property values atomically, verifies the saved row, and returns its exact page link.",
198
+ publicAgent: {
199
+ expose: true,
200
+ readOnly: false,
201
+ requiresAuth: true,
202
+ isConsequential: true,
203
+ title: "Submit Content Database Form",
204
+ description:
205
+ "Delegate a validated, atomic submission to an existing Content database form.",
206
+ },
198
207
  schema: submitContentDatabaseFormSchema,
199
208
  mcpApp: {
200
209
  compactCatalog: true,
@@ -290,6 +290,15 @@ export function isStaleBuilderImageSourceComponentSave(args: {
290
290
  export default defineAction({
291
291
  description:
292
292
  "Update an existing document's title, content, icon, or favorite status.",
293
+ publicAgent: {
294
+ expose: true,
295
+ readOnly: false,
296
+ requiresAuth: true,
297
+ isConsequential: true,
298
+ title: "Update Content Document",
299
+ description:
300
+ "Delegate a sparse update to an existing Content document while preserving omitted fields.",
301
+ },
293
302
  schema: z.object({
294
303
  id: z.string().optional().describe("Document ID (required)"),
295
304
  title: z.string().optional().describe("New title"),
@@ -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
  }