@agent-native/core 0.84.0 → 0.84.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/docs/content/evals.mdx +34 -12
  4. package/corpus/core/docs/content/locales/ar-SA/evals.mdx +7 -0
  5. package/corpus/core/docs/content/locales/de-DE/evals.mdx +8 -0
  6. package/corpus/core/docs/content/locales/es-ES/evals.mdx +8 -0
  7. package/corpus/core/docs/content/locales/fr-FR/evals.mdx +8 -0
  8. package/corpus/core/docs/content/locales/hi-IN/evals.mdx +8 -0
  9. package/corpus/core/docs/content/locales/ja-JP/evals.mdx +7 -0
  10. package/corpus/core/docs/content/locales/ko-KR/evals.mdx +7 -0
  11. package/corpus/core/docs/content/locales/pt-BR/evals.mdx +8 -0
  12. package/corpus/core/docs/content/locales/zh-CN/evals.mdx +7 -0
  13. package/corpus/core/docs/content/locales/zh-TW/evals.mdx +7 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/eval/define-eval.ts +4 -1
  16. package/corpus/core/src/eval/report.ts +23 -8
  17. package/corpus/core/src/eval/runner.ts +41 -6
  18. package/corpus/core/src/eval/types.ts +11 -0
  19. package/corpus/templates/content/AGENTS.md +9 -2
  20. package/corpus/templates/content/actions/_database-row-batch.ts +230 -0
  21. package/corpus/templates/content/actions/_database-utils.ts +17 -9
  22. package/corpus/templates/content/actions/_notion-action-utils.ts +26 -0
  23. package/corpus/templates/content/actions/connect-notion-status.ts +13 -4
  24. package/corpus/templates/content/actions/create-and-link-notion-page.ts +23 -0
  25. package/corpus/templates/content/actions/delete-database-items.ts +55 -0
  26. package/corpus/templates/content/actions/delete-document.ts +4 -4
  27. package/corpus/templates/content/actions/disconnect-notion.ts +16 -0
  28. package/corpus/templates/content/actions/duplicate-database-item.ts +1 -1
  29. package/corpus/templates/content/actions/duplicate-database-items.ts +191 -0
  30. package/corpus/templates/content/actions/link-notion-page.ts +10 -7
  31. package/corpus/templates/content/actions/list-notion-links.ts +3 -4
  32. package/corpus/templates/content/actions/pull-notion-page.ts +7 -9
  33. package/corpus/templates/content/actions/push-notion-page.ts +7 -9
  34. package/corpus/templates/content/actions/refresh-notion-sync-status.ts +24 -0
  35. package/corpus/templates/content/actions/resolve-notion-sync-conflict.ts +23 -0
  36. package/corpus/templates/content/actions/search-notion-pages.ts +67 -0
  37. package/corpus/templates/content/actions/unlink-notion-page.ts +23 -0
  38. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +27 -34
  39. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +16 -13
  40. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +10 -10
  41. package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +1 -1
  42. package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +18 -9
  43. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +27 -34
  44. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +10 -4
  45. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +32 -17
  46. package/corpus/templates/content/app/hooks/use-content-database.ts +35 -0
  47. package/corpus/templates/content/app/hooks/use-notion.ts +108 -99
  48. package/corpus/templates/content/package.json +3 -0
  49. package/corpus/templates/content/parity/README.md +69 -0
  50. package/corpus/templates/content/parity/eval-scenarios.ts +118 -0
  51. package/corpus/templates/content/parity/exceptions.allowlist.ts +17 -0
  52. package/corpus/templates/content/parity/matrix.md +28 -0
  53. package/corpus/templates/content/parity/matrix.ts +606 -0
  54. package/corpus/templates/content/parity/matrix.types.ts +42 -0
  55. package/corpus/templates/content/parity/parity-evals.eval.ts +4 -0
  56. package/corpus/templates/content/parity/render-matrix.ts +90 -0
  57. package/corpus/templates/content/parity/scenario-to-eval.ts +57 -0
  58. package/corpus/templates/content/shared/api.ts +11 -0
  59. package/dist/collab/awareness.d.ts +2 -2
  60. package/dist/collab/awareness.d.ts.map +1 -1
  61. package/dist/collab/routes.d.ts +1 -1
  62. package/dist/eval/define-eval.d.ts.map +1 -1
  63. package/dist/eval/define-eval.js +2 -1
  64. package/dist/eval/define-eval.js.map +1 -1
  65. package/dist/eval/report.d.ts.map +1 -1
  66. package/dist/eval/report.js +19 -4
  67. package/dist/eval/report.js.map +1 -1
  68. package/dist/eval/runner.d.ts.map +1 -1
  69. package/dist/eval/runner.js +38 -6
  70. package/dist/eval/runner.js.map +1 -1
  71. package/dist/eval/types.d.ts +11 -0
  72. package/dist/eval/types.d.ts.map +1 -1
  73. package/dist/eval/types.js.map +1 -1
  74. package/dist/notifications/routes.d.ts +3 -3
  75. package/dist/progress/routes.d.ts +1 -1
  76. package/dist/resources/handlers.d.ts +1 -1
  77. package/docs/content/evals.mdx +34 -12
  78. package/docs/content/locales/ar-SA/evals.mdx +7 -0
  79. package/docs/content/locales/de-DE/evals.mdx +8 -0
  80. package/docs/content/locales/es-ES/evals.mdx +8 -0
  81. package/docs/content/locales/fr-FR/evals.mdx +8 -0
  82. package/docs/content/locales/hi-IN/evals.mdx +8 -0
  83. package/docs/content/locales/ja-JP/evals.mdx +7 -0
  84. package/docs/content/locales/ko-KR/evals.mdx +7 -0
  85. package/docs/content/locales/pt-BR/evals.mdx +8 -0
  86. package/docs/content/locales/zh-CN/evals.mdx +7 -0
  87. package/docs/content/locales/zh-TW/evals.mdx +7 -0
  88. package/package.json +1 -1
  89. package/corpus/templates/content/server/routes/api/notion/disconnect.post.ts +0 -12
@@ -1,8 +1,11 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { getRequestUserEmail } from "@agent-native/core/server";
3
2
  import { z } from "zod";
4
3
 
5
4
  import { linkDocumentToNotionPage } from "../server/lib/notion-sync.js";
5
+ import {
6
+ getNotionDocumentOwner,
7
+ resolveDocumentId,
8
+ } from "./_notion-action-utils.js";
6
9
 
7
10
  export default defineAction({
8
11
  description: "Link a document to a Notion page for syncing.",
@@ -10,19 +13,19 @@ export default defineAction({
10
13
  documentId: z.string().optional().describe("Document ID (required)"),
11
14
  id: z.string().optional().describe("Alias for --documentId"),
12
15
  pageId: z.string().optional().describe("Notion page ID or URL (required)"),
16
+ pageIdOrUrl: z.string().optional().describe("Alias for --pageId"),
13
17
  url: z.string().optional().describe("Alias for --pageId"),
14
18
  }),
15
- http: false,
19
+ http: { method: "POST" },
16
20
  run: async (args) => {
17
- const owner = getRequestUserEmail();
18
- if (!owner) throw new Error("no authenticated user");
19
- const documentId = args.documentId || args.id;
20
- const pageIdOrUrl = args.pageId || args.url;
21
+ const documentId = resolveDocumentId(args);
22
+ const pageIdOrUrl = args.pageId || args.pageIdOrUrl || args.url;
21
23
 
22
- if (!documentId || !pageIdOrUrl) {
24
+ if (!pageIdOrUrl) {
23
25
  throw new Error("documentId and pageId are required");
24
26
  }
25
27
 
28
+ const owner = await getNotionDocumentOwner(documentId);
26
29
  return linkDocumentToNotionPage(owner, documentId, pageIdOrUrl);
27
30
  },
28
31
  });
@@ -1,16 +1,15 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { getRequestUserEmail } from "@agent-native/core/server";
3
2
  import { z } from "zod";
4
3
 
5
4
  import { listNotionLinks } from "../server/lib/notion-sync.js";
5
+ import { getCurrentNotionOwner } from "./_notion-action-utils.js";
6
6
 
7
7
  export default defineAction({
8
8
  description: "List all documents linked to Notion pages.",
9
9
  schema: z.object({}),
10
- http: false,
10
+ http: { method: "GET" },
11
11
  run: async () => {
12
- const owner = getRequestUserEmail();
13
- if (!owner) throw new Error("no authenticated user");
12
+ const owner = getCurrentNotionOwner();
14
13
  return listNotionLinks(owner);
15
14
  },
16
15
  });
@@ -1,8 +1,11 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { getRequestUserEmail } from "@agent-native/core/server";
3
2
  import { z } from "zod";
4
3
 
5
4
  import { pullDocumentFromNotion } from "../server/lib/notion-sync.js";
5
+ import {
6
+ getNotionDocumentOwner,
7
+ resolveDocumentId,
8
+ } from "./_notion-action-utils.js";
6
9
 
7
10
  export default defineAction({
8
11
  description: "Pull content from a linked Notion page into a local document.",
@@ -10,15 +13,10 @@ export default defineAction({
10
13
  documentId: z.string().optional().describe("Document ID (required)"),
11
14
  id: z.string().optional().describe("Alias for --documentId"),
12
15
  }),
13
- http: false,
16
+ http: { method: "POST" },
14
17
  run: async (args) => {
15
- const documentId = args.documentId || args.id;
16
- if (!documentId) {
17
- throw new Error("documentId is required");
18
- }
19
-
20
- const owner = getRequestUserEmail();
21
- if (!owner) throw new Error("no authenticated user");
18
+ const documentId = resolveDocumentId(args);
19
+ const owner = await getNotionDocumentOwner(documentId);
22
20
  return pullDocumentFromNotion(owner, documentId, true);
23
21
  },
24
22
  });
@@ -1,8 +1,11 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { getRequestUserEmail } from "@agent-native/core/server";
3
2
  import { z } from "zod";
4
3
 
5
4
  import { pushDocumentToNotion } from "../server/lib/notion-sync.js";
5
+ import {
6
+ getNotionDocumentOwner,
7
+ resolveDocumentId,
8
+ } from "./_notion-action-utils.js";
6
9
 
7
10
  export default defineAction({
8
11
  description: "Push local document content to a linked Notion page.",
@@ -10,15 +13,10 @@ export default defineAction({
10
13
  documentId: z.string().optional().describe("Document ID (required)"),
11
14
  id: z.string().optional().describe("Alias for --documentId"),
12
15
  }),
13
- http: false,
16
+ http: { method: "POST" },
14
17
  run: async (args) => {
15
- const documentId = args.documentId || args.id;
16
- if (!documentId) {
17
- throw new Error("documentId is required");
18
- }
19
-
20
- const owner = getRequestUserEmail();
21
- if (!owner) throw new Error("no authenticated user");
18
+ const documentId = resolveDocumentId(args);
19
+ const owner = await getNotionDocumentOwner(documentId);
22
20
  return pushDocumentToNotion(owner, documentId);
23
21
  },
24
22
  });
@@ -0,0 +1,24 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+
4
+ import { refreshDocumentSyncStatus } from "../server/lib/notion-sync.js";
5
+ import {
6
+ getNotionDocumentOwner,
7
+ resolveDocumentId,
8
+ } from "./_notion-action-utils.js";
9
+
10
+ export default defineAction({
11
+ description: "Refresh Notion sync status for a linked Content document.",
12
+ schema: z.object({
13
+ documentId: z.string().optional().describe("Document ID (required)"),
14
+ id: z.string().optional().describe("Alias for --documentId"),
15
+ autoSync: z.boolean().optional(),
16
+ }),
17
+ run: async (args) => {
18
+ const documentId = resolveDocumentId(args);
19
+ const owner = await getNotionDocumentOwner(documentId);
20
+ return refreshDocumentSyncStatus(owner, documentId, {
21
+ autoSync: !!args.autoSync,
22
+ });
23
+ },
24
+ });
@@ -0,0 +1,23 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+
4
+ import { resolveDocumentSyncConflict } from "../server/lib/notion-sync.js";
5
+ import {
6
+ getNotionDocumentOwner,
7
+ resolveDocumentId,
8
+ } from "./_notion-action-utils.js";
9
+
10
+ export default defineAction({
11
+ description: "Resolve a Notion sync conflict by pulling or pushing.",
12
+ schema: z.object({
13
+ documentId: z.string().optional().describe("Document ID (required)"),
14
+ id: z.string().optional().describe("Alias for --documentId"),
15
+ direction: z.enum(["pull", "push"]),
16
+ }),
17
+ http: { method: "POST" },
18
+ run: async (args) => {
19
+ const documentId = resolveDocumentId(args);
20
+ const owner = await getNotionDocumentOwner(documentId);
21
+ return resolveDocumentSyncConflict(owner, documentId, args.direction);
22
+ },
23
+ });
@@ -0,0 +1,67 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+
4
+ import {
5
+ getNotionConnectionForOwner,
6
+ notionFetch,
7
+ } from "../server/lib/notion.js";
8
+ import type {
9
+ NotionSearchResponse,
10
+ NotionSearchResult,
11
+ } from "../shared/api.js";
12
+ import { getCurrentNotionOwner } from "./_notion-action-utils.js";
13
+
14
+ export default defineAction({
15
+ description: "Search connected Notion pages visible to the current user.",
16
+ schema: z.object({
17
+ query: z.string().default(""),
18
+ }),
19
+ http: { method: "GET" },
20
+ run: async ({ query }): Promise<NotionSearchResponse> => {
21
+ const owner = getCurrentNotionOwner();
22
+ const conn = await getNotionConnectionForOwner(owner);
23
+ if (!conn) {
24
+ throw new Error("Notion not connected");
25
+ }
26
+
27
+ const result = await notionFetch<{
28
+ results: Array<{
29
+ id: string;
30
+ object: string;
31
+ icon?: { type: string; emoji?: string } | null;
32
+ url?: string;
33
+ last_edited_time?: string;
34
+ properties?: Record<string, any>;
35
+ }>;
36
+ has_more: boolean;
37
+ }>("/search", conn.accessToken, {
38
+ method: "POST",
39
+ body: JSON.stringify({
40
+ query: query.trim(),
41
+ filter: { value: "page", property: "object" },
42
+ sort: { direction: "descending", timestamp: "last_edited_time" },
43
+ page_size: 20,
44
+ }),
45
+ });
46
+
47
+ const results: NotionSearchResult[] = result.results.map((page) => {
48
+ const titleProp = Object.values(page.properties || {}).find(
49
+ (value: any) => value?.type === "title",
50
+ ) as any;
51
+ const title =
52
+ (titleProp?.title || [])
53
+ .map((part: any) => part.plain_text || "")
54
+ .join("") || "Untitled";
55
+
56
+ return {
57
+ id: page.id,
58
+ title,
59
+ icon: page.icon?.type === "emoji" ? page.icon.emoji || null : null,
60
+ url: page.url || `https://notion.so/${page.id.replace(/-/g, "")}`,
61
+ lastEditedTime: page.last_edited_time || null,
62
+ };
63
+ });
64
+
65
+ return { results, hasMore: result.has_more };
66
+ },
67
+ });
@@ -0,0 +1,23 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+
4
+ import { unlinkDocumentFromNotion } from "../server/lib/notion-sync.js";
5
+ import {
6
+ getNotionDocumentOwner,
7
+ resolveDocumentId,
8
+ } from "./_notion-action-utils.js";
9
+
10
+ export default defineAction({
11
+ description: "Unlink a Content document from its Notion page.",
12
+ schema: z.object({
13
+ documentId: z.string().optional().describe("Document ID (required)"),
14
+ id: z.string().optional().describe("Alias for --documentId"),
15
+ }),
16
+ http: { method: "DELETE" },
17
+ run: async (args) => {
18
+ const documentId = resolveDocumentId(args);
19
+ const owner = await getNotionDocumentOwner(documentId);
20
+ await unlinkDocumentFromNotion(owner, documentId);
21
+ return { success: true };
22
+ },
23
+ });
@@ -143,8 +143,10 @@ import {
143
143
  useChangeContentDatabaseSourceRole,
144
144
  useContentDatabase,
145
145
  useContentDatabases,
146
+ useDeleteDatabaseItems,
146
147
  useDisconnectContentDatabaseSource,
147
148
  useDuplicateDatabaseItem,
149
+ useDuplicateDatabaseItems,
148
150
  useExecuteBuilderSourceBatch,
149
151
  useMoveDatabaseItem,
150
152
  usePrepareBuilderSourceReview,
@@ -2862,9 +2864,9 @@ function DatabaseTableView({
2862
2864
  const db = useDatabaseT();
2863
2865
  const queryClient = useQueryClient();
2864
2866
  const moveItem = useMoveDatabaseItem(databaseDocumentId);
2865
- const duplicateItem = useDuplicateDatabaseItem(databaseDocumentId);
2867
+ const duplicateItems = useDuplicateDatabaseItems(databaseDocumentId);
2866
2868
  const setProperty = useSetDocumentProperty(databaseDocumentId);
2867
- const deleteDocument = useDeleteDocument();
2869
+ const deleteItems = useDeleteDatabaseItems(databaseDocumentId);
2868
2870
  const [draggedItemId, setDraggedItemId] = useState<string | null>(null);
2869
2871
  const [dropTargetItemId, setDropTargetItemId] = useState<string | null>(null);
2870
2872
  const [draggedPropertyId, setDraggedPropertyId] = useState<string | null>(
@@ -3113,14 +3115,15 @@ function DatabaseTableView({
3113
3115
  async function deleteSelectedRows() {
3114
3116
  if (selectedItems.length === 0) return;
3115
3117
  const selectedSnapshot = selectedItems;
3116
- onClearSelection();
3117
3118
  setConfirmDeleteSelectedOpen(false);
3118
3119
 
3119
3120
  try {
3121
+ await deleteItems.mutateAsync({
3122
+ documentId: databaseDocumentId,
3123
+ itemIds: selectedSnapshot.map((item) => item.id),
3124
+ });
3125
+ onClearSelection();
3120
3126
  onDeletedPreviewItems(selectedSnapshot);
3121
- for (const item of selectedSnapshot) {
3122
- await deleteDocument.mutateAsync({ id: item.document.id });
3123
- }
3124
3127
  await queryClient.invalidateQueries({
3125
3128
  queryKey: [
3126
3129
  "action",
@@ -3144,22 +3147,11 @@ function DatabaseTableView({
3144
3147
  const selectedSnapshot = selectedItems;
3145
3148
  setIsDuplicatingSelected(true);
3146
3149
 
3147
- let duplicatedPreviewItem: ContentDatabaseItem | null = null;
3148
- let duplicatedCount = 0;
3149
- let failedCount = 0;
3150
-
3151
3150
  try {
3152
- for (const item of selectedSnapshot) {
3153
- try {
3154
- const response = await duplicateItem.mutateAsync({ itemId: item.id });
3155
- duplicatedCount += 1;
3156
- duplicatedPreviewItem =
3157
- databaseDuplicatedItemFromResponse(response) ??
3158
- duplicatedPreviewItem;
3159
- } catch {
3160
- failedCount += 1;
3161
- }
3162
- }
3151
+ const response = await duplicateItems.mutateAsync({
3152
+ documentId: databaseDocumentId,
3153
+ itemIds: selectedSnapshot.map((item) => item.id),
3154
+ });
3163
3155
 
3164
3156
  await queryClient.invalidateQueries({
3165
3157
  queryKey: [
@@ -3172,16 +3164,15 @@ function DatabaseTableView({
3172
3164
  queryKey: ["action", "list-documents"],
3173
3165
  });
3174
3166
 
3167
+ const duplicatedPreviewItem =
3168
+ databaseDuplicatedItemFromResponse(response);
3175
3169
  if (duplicatedPreviewItem) onPreview(duplicatedPreviewItem);
3176
- if (duplicatedCount > 0) onClearSelection();
3177
- if (failedCount > 0) {
3178
- toast.error(db("failedToDuplicateEverySelectedRow"), {
3179
- description:
3180
- duplicatedCount > 0
3181
- ? `${duplicatedCount} duplicated, ${failedCount} failed.`
3182
- : "No rows were duplicated.",
3183
- });
3184
- }
3170
+ onClearSelection();
3171
+ } catch (err) {
3172
+ toast.error(db("failedToDuplicateEverySelectedRow"), {
3173
+ description:
3174
+ err instanceof Error ? err.message : db("somethingWentWrong"),
3175
+ });
3185
3176
  } finally {
3186
3177
  setIsDuplicatingSelected(false);
3187
3178
  }
@@ -3240,9 +3231,11 @@ function DatabaseTableView({
3240
3231
  canEdit={canEdit}
3241
3232
  properties={bulkEditableProperties}
3242
3233
  duplicateDisabled={
3243
- isDuplicatingSelected || deleteDocument.isPending
3234
+ isDuplicatingSelected ||
3235
+ duplicateItems.isPending ||
3236
+ deleteItems.isPending
3244
3237
  }
3245
- deleteDisabled={deleteDocument.isPending}
3238
+ deleteDisabled={deleteItems.isPending}
3246
3239
  updateDisabled={setProperty.isPending}
3247
3240
  onClearSelection={onClearSelection}
3248
3241
  onSetPropertyValue={setSelectedPropertyValue}
@@ -3466,10 +3459,10 @@ function DatabaseTableView({
3466
3459
  </AlertDialogCancel>
3467
3460
  <AlertDialogAction
3468
3461
  className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
3469
- disabled={deleteDocument.isPending}
3462
+ disabled={deleteItems.isPending}
3470
3463
  onClick={() => void deleteSelectedRows()}
3471
3464
  >
3472
- {deleteDocument.isPending ? "Deleting..." : "Delete"}
3465
+ {deleteItems.isPending ? "Deleting..." : "Delete"}
3473
3466
  </AlertDialogAction>
3474
3467
  </AlertDialogFooter>
3475
3468
  </AlertDialogContent>
@@ -6,7 +6,6 @@ import {
6
6
  emailToName,
7
7
  useSession,
8
8
  useT,
9
- appApiPath,
10
9
  agentNativePath,
11
10
  type CollabUser,
12
11
  } from "@agent-native/core/client";
@@ -41,7 +40,11 @@ import {
41
40
  useUpdateDocument,
42
41
  } from "@/hooks/use-documents";
43
42
  import { useLocalStorage } from "@/hooks/use-local-storage";
44
- import { useDocumentSyncStatus } from "@/hooks/use-notion";
43
+ import {
44
+ documentSyncStatusQueryKey,
45
+ useDocumentSyncStatus,
46
+ usePushDocumentToNotion,
47
+ } from "@/hooks/use-notion";
45
48
  import {
46
49
  canWriteLinkedLocalSource,
47
50
  readDocumentFromLinkedLocalSource,
@@ -360,6 +363,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
360
363
  useDocumentSyncStatus(canEdit && !isLocalFileDocument ? documentId : null, {
361
364
  autoSync,
362
365
  });
366
+ const pushDocumentToNotion = usePushDocumentToNotion(documentId);
363
367
  const [localTitle, setLocalTitle] = useState("");
364
368
  const [localContent, setLocalContent] = useState("");
365
369
  const [localContentUpdatedAt, setLocalContentUpdatedAt] = useState<
@@ -775,20 +779,18 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
775
779
  // the debounce and the next save, reading the previous content.
776
780
  // Pulls remain driven by the polling refetch in useDocumentSyncStatus.
777
781
  if (autoSync) {
778
- const status = queryClient.getQueryData<DocumentSyncStatus>([
779
- "document-sync",
780
- documentId,
781
- ]);
782
+ const status = queryClient.getQueryData<DocumentSyncStatus>(
783
+ documentSyncStatusQueryKey(documentId, { autoSync }),
784
+ );
782
785
  if (status?.pageId && !status.hasConflict) {
783
786
  try {
784
- const res = await fetch(
785
- appApiPath(`/api/documents/${documentId}/notion/push`),
786
- { method: "POST" },
787
+ const next = await pushDocumentToNotion.mutateAsync({
788
+ documentId,
789
+ });
790
+ queryClient.setQueryData(
791
+ documentSyncStatusQueryKey(documentId, { autoSync }),
792
+ next,
787
793
  );
788
- if (res.ok) {
789
- const next = (await res.json()) as DocumentSyncStatus;
790
- queryClient.setQueryData(["document-sync", documentId], next);
791
- }
792
794
  } catch {
793
795
  // Non-fatal — next polling refetch will surface any error.
794
796
  }
@@ -803,6 +805,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
803
805
  autoSync,
804
806
  isLinkedLocalSourceDocument,
805
807
  persistDocumentUpdates,
808
+ pushDocumentToNotion,
806
809
  queryClient,
807
810
  ],
808
811
  );
@@ -533,7 +533,7 @@ export function DocumentToolbar({
533
533
  async (pageId: string) => {
534
534
  setLinkingPageId(pageId);
535
535
  try {
536
- await linkDocument.mutateAsync({ pageIdOrUrl: pageId });
536
+ await linkDocument.mutateAsync({ documentId, pageIdOrUrl: pageId });
537
537
  toast.success(t("editor.toolbar.linkedToNotionPage"));
538
538
  setSearchQuery("");
539
539
  } catch (error) {
@@ -546,34 +546,34 @@ export function DocumentToolbar({
546
546
  setLinkingPageId(null);
547
547
  }
548
548
  },
549
- [linkDocument, t],
549
+ [documentId, linkDocument, t],
550
550
  );
551
551
 
552
552
  const handlePull = useCallback(async () => {
553
553
  try {
554
- await pullDocument.mutateAsync();
554
+ await pullDocument.mutateAsync({ documentId });
555
555
  toast.success(t("editor.toolbar.pulledFromNotion"));
556
556
  } catch (error) {
557
557
  toast.error(
558
558
  error instanceof Error ? error.message : t("editor.toolbar.pullFailed"),
559
559
  );
560
560
  }
561
- }, [pullDocument, t]);
561
+ }, [documentId, pullDocument, t]);
562
562
 
563
563
  const handlePush = useCallback(async () => {
564
564
  try {
565
- await pushDocument.mutateAsync();
565
+ await pushDocument.mutateAsync({ documentId });
566
566
  toast.success(t("editor.toolbar.pushedToNotion"));
567
567
  } catch (error) {
568
568
  toast.error(
569
569
  error instanceof Error ? error.message : t("editor.toolbar.pushFailed"),
570
570
  );
571
571
  }
572
- }, [pushDocument, t]);
572
+ }, [documentId, pushDocument, t]);
573
573
 
574
574
  const handleUnlink = useCallback(async () => {
575
575
  try {
576
- await unlinkDocument.mutateAsync();
576
+ await unlinkDocument.mutateAsync({ documentId });
577
577
  toast.success(t("editor.toolbar.unlinkedFromNotion"));
578
578
  } catch (error) {
579
579
  toast.error(
@@ -582,13 +582,13 @@ export function DocumentToolbar({
582
582
  : t("editor.toolbar.unlinkFailed"),
583
583
  );
584
584
  }
585
- }, [unlinkDocument, t]);
585
+ }, [documentId, unlinkDocument, t]);
586
586
 
587
587
  const handleCreateAndLink = useCallback(
588
588
  (parentPageIdOrUrl?: string) => {
589
589
  if (parentPageIdOrUrl) setCreatingParentPageId(parentPageIdOrUrl);
590
590
  createAndLink.mutate(
591
- parentPageIdOrUrl ? { parentPageIdOrUrl } : undefined,
591
+ { documentId, ...(parentPageIdOrUrl ? { parentPageIdOrUrl } : {}) },
592
592
  {
593
593
  onSuccess: () => {
594
594
  toast.success(t("editor.toolbar.createdAndLinkedToNotionPage"));
@@ -605,7 +605,7 @@ export function DocumentToolbar({
605
605
  },
606
606
  );
607
607
  },
608
- [createAndLink, t],
608
+ [createAndLink, documentId, t],
609
609
  );
610
610
 
611
611
  const handleSetup = () => {
@@ -34,7 +34,7 @@ export function NotionConflictBanner({
34
34
  const handleResolve = async (dir: "pull" | "push") => {
35
35
  setDirection(dir);
36
36
  try {
37
- await resolveConflict.mutateAsync({ direction: dir });
37
+ await resolveConflict.mutateAsync({ documentId, direction: dir });
38
38
  toast.success(
39
39
  dir === "pull"
40
40
  ? t("editor.notionConflictResolvedPulled")
@@ -19,6 +19,7 @@ import {
19
19
  useDocumentSyncStatus,
20
20
  useLinkDocumentToNotion,
21
21
  useNotionConnection,
22
+ openNotionOAuthUrl,
22
23
  usePullDocumentFromNotion,
23
24
  usePushDocumentToNotion,
24
25
  useResolveDocumentSyncConflict,
@@ -63,12 +64,20 @@ export function NotionSyncBar({ documentId }: NotionSyncBarProps) {
63
64
  resolveConflict.isPending ||
64
65
  disconnectNotion.isPending;
65
66
 
66
- const handleConnect = () => {
67
- if (!connection?.authUrl) {
67
+ const handleConnect = async () => {
68
+ if (connection?.error === "missing_credentials") {
68
69
  toast.error(t("editor.toolbar.setUpNotionFirst"));
69
70
  return;
70
71
  }
71
- window.location.href = connection.authUrl;
72
+ try {
73
+ window.location.href = await openNotionOAuthUrl();
74
+ } catch (error) {
75
+ toast.error(
76
+ error instanceof Error
77
+ ? error.message
78
+ : t("editor.toolbar.setUpNotionFirst"),
79
+ );
80
+ }
72
81
  };
73
82
 
74
83
  const handleLink = async () => {
@@ -77,7 +86,7 @@ export function NotionSyncBar({ documentId }: NotionSyncBarProps) {
77
86
  return;
78
87
  }
79
88
  try {
80
- await linkDocument.mutateAsync({ pageIdOrUrl });
89
+ await linkDocument.mutateAsync({ documentId, pageIdOrUrl });
81
90
  setPageIdOrUrl("");
82
91
  toast.success(t("editor.toolbar.linkedToNotion"));
83
92
  } catch (error) {
@@ -91,7 +100,7 @@ export function NotionSyncBar({ documentId }: NotionSyncBarProps) {
91
100
 
92
101
  const handlePull = async () => {
93
102
  try {
94
- await pullDocument.mutateAsync();
103
+ await pullDocument.mutateAsync({ documentId });
95
104
  toast.success(t("editor.toolbar.pulledFromNotion"));
96
105
  } catch (error) {
97
106
  toast.error(
@@ -102,7 +111,7 @@ export function NotionSyncBar({ documentId }: NotionSyncBarProps) {
102
111
 
103
112
  const handlePush = async () => {
104
113
  try {
105
- await pushDocument.mutateAsync();
114
+ await pushDocument.mutateAsync({ documentId });
106
115
  toast.success(t("editor.toolbar.pushedToNotion"));
107
116
  } catch (error) {
108
117
  toast.error(
@@ -113,7 +122,7 @@ export function NotionSyncBar({ documentId }: NotionSyncBarProps) {
113
122
 
114
123
  const handleResolve = async (direction: "pull" | "push") => {
115
124
  try {
116
- await resolveConflict.mutateAsync({ direction });
125
+ await resolveConflict.mutateAsync({ documentId, direction });
117
126
  toast.success(
118
127
  direction === "pull"
119
128
  ? t("editor.toolbar.conflictResolvedFromNotion")
@@ -130,7 +139,7 @@ export function NotionSyncBar({ documentId }: NotionSyncBarProps) {
130
139
 
131
140
  const handleUnlink = async () => {
132
141
  try {
133
- await unlinkDocument.mutateAsync();
142
+ await unlinkDocument.mutateAsync({ documentId });
134
143
  toast.success(t("editor.toolbar.unlinkedFromNotion"));
135
144
  } catch (error) {
136
145
  toast.error(
@@ -143,7 +152,7 @@ export function NotionSyncBar({ documentId }: NotionSyncBarProps) {
143
152
 
144
153
  const handleDisconnect = async () => {
145
154
  try {
146
- await disconnectNotion.mutateAsync();
155
+ await disconnectNotion.mutateAsync({});
147
156
  toast.success(t("editor.toolbar.disconnectedNotionWorkspace"));
148
157
  } catch (error) {
149
158
  toast.error(