@agent-native/core 0.160.0 → 0.161.0
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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/actions/bigquery.ts +57 -0
- package/corpus/templates/analytics/actions/restore-analysis-revision.ts +5 -2
- package/corpus/templates/analytics/actions/restore-dashboard-revision.ts +5 -2
- package/corpus/templates/analytics/app/components/analysis/AnalysisHistoryPanel.tsx +14 -3
- package/corpus/templates/analytics/app/components/dashboard/DashboardHistoryPanel.tsx +14 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/analytics/app/hooks/use-analysis-revisions.ts +1 -0
- package/corpus/templates/analytics/app/hooks/use-dashboard-revisions.ts +16 -1
- package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +19 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +0 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +14 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +19 -0
- package/corpus/templates/analytics/changelog/2026-08-17-fixed-dashboard-editing-focus-history-recovery-panel-setup-f.md +6 -0
- package/corpus/templates/calendar/actions/create-event.ts +7 -3
- package/corpus/templates/calendar/actions/manage-event-draft.ts +8 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +64 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +5 -1
- package/corpus/templates/calendar/app/hooks/use-events.ts +1 -0
- package/corpus/templates/calendar/changelog/2026-08-17-booking-links-now-respect-each-required-host-s-saved-availab.md +6 -0
- package/corpus/templates/calendar/server/lib/booking-host-availability.ts +76 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +2 -0
- package/corpus/templates/calendar/shared/api.ts +1 -1
- package/corpus/templates/clips/app/components/player/signed-out-share-actions.tsx +47 -0
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +3 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +27 -19
- package/corpus/templates/clips/changelog/2026-08-17-shared-clips-sign-in-comment-no-audio.md +6 -0
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -5
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +13 -0
- package/corpus/templates/content/AGENTS.md +2 -0
- package/corpus/templates/content/actions/_blocks-field-identity.ts +17 -0
- package/corpus/templates/content/actions/_database-block-actions.ts +958 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +7 -7
- package/corpus/templates/content/actions/list-content-database-blocks.ts +31 -0
- package/corpus/templates/content/actions/mutate-content-database-block.ts +49 -0
- package/corpus/templates/content/changelog/2026-08-10-agents-can-safely-insert-update-upsert-delete-and-reorder-on.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.block.md +6 -2
- package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +2 -1
- package/corpus/templates/content/parity/matrix.md +30 -30
- package/corpus/templates/content/parity/matrix.ts +4 -1
- package/corpus/templates/content/shared/blocks-field-identity.ts +114 -37
- package/corpus/templates/content/shared/database-block-actions.ts +82 -0
- package/corpus/templates/content/shared/database-block-mutations.ts +366 -0
- package/corpus/templates/content/shared/nfm.ts +3 -2
- package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +24 -0
- package/corpus/templates/mail/actions/get-mail-settings.ts +1 -1
- package/corpus/templates/mail/actions/update-mail-settings.ts +7 -3
- package/corpus/templates/mail/app/i18n/en-US.ts +3 -1
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +63 -0
- package/corpus/templates/mail/changelog/2026-08-17-gmail-signatures-now-preserve-images-and-support-paste-or-up.md +6 -0
- package/corpus/templates/mail/changelog/2026-08-17-mail-now-updates-durable-drafting-preferences-without-openin.md +6 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +19 -0
- package/corpus/templates/mail/shared/gmail-signature.ts +14 -2
- package/corpus/templates/mail/shared/signature.ts +1 -10
- package/corpus/templates/slides/actions/generate-slides-ai.ts +2 -0
- package/corpus/templates/slides/actions/patch-deck.ts +1 -1
- package/corpus/templates/slides/app/components/ThemeToggle.tsx +1 -0
- package/corpus/templates/slides/app/components/editor/SlideThumbnailContextMenu.tsx +59 -0
- package/corpus/templates/slides/app/global.css +1 -0
- package/corpus/templates/slides/app/lib/normalize-slide-padding.ts +40 -11
- package/corpus/templates/slides/changelog/2026-08-17-stable-layout-and-slide-editing.md +6 -0
- package/corpus/templates/slides/shared/deck-title.ts +12 -4
- package/dist/agent/production-agent.js +27 -15
- package/dist/agent/tool-approval-migrations.d.ts +2 -0
- package/dist/agent/tool-approval-migrations.js +20 -0
- package/dist/agent/tool-approval-store.d.ts +17 -3
- package/dist/agent/tool-approval-store.js +56 -6
- package/dist/agent/types.d.ts +3 -1
- package/dist/cli/code-agent-connector.js +79 -7
- package/dist/cli/portal-transfer.d.ts +43 -0
- package/dist/cli/portal-transfer.js +201 -0
- package/dist/client/AssistantChat.js +3 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/core-messages/ar-SA.js +1 -0
- package/dist/localization/core-messages/de-DE.js +1 -0
- package/dist/localization/core-messages/en-US.d.ts +1 -0
- package/dist/localization/core-messages/en-US.js +1 -0
- package/dist/localization/core-messages/es-ES.js +1 -0
- package/dist/localization/core-messages/fr-FR.js +1 -0
- package/dist/localization/core-messages/hi-IN.js +1 -0
- package/dist/localization/core-messages/ja-JP.js +1 -0
- package/dist/localization/core-messages/ko-KR.js +1 -0
- package/dist/localization/core-messages/pt-BR.js +1 -0
- package/dist/localization/core-messages/zh-CN.js +1 -0
- package/dist/localization/core-messages/zh-TW.js +1 -0
- package/dist/localization/core-messages.d.ts +1 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/resources/read.js +4 -4
- package/dist/scripts/resources/save-memory.js +15 -2
- package/dist/scripts/resources/write.js +3 -3
- package/dist/server/agent-chat/script-entries.d.ts +1 -0
- package/dist/server/agent-chat/script-entries.js +8 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/onboarding.mdx +13 -2
- package/package.json +1 -1
|
@@ -4,6 +4,83 @@ export const BLOCKS_FIELD_IDENTITY_VERSION = 1;
|
|
|
4
4
|
export const BLOCK_TOMBSTONE_REVISION_WINDOW = 20;
|
|
5
5
|
export const MAX_BLOCK_TOMBSTONES_PER_FIELD = 500;
|
|
6
6
|
|
|
7
|
+
export const BLOCKS_FIELD_BLOCK_KINDS = [
|
|
8
|
+
"paragraph",
|
|
9
|
+
"heading",
|
|
10
|
+
"horizontalRule",
|
|
11
|
+
"codeBlock",
|
|
12
|
+
"blockquote",
|
|
13
|
+
"bulletList",
|
|
14
|
+
"orderedList",
|
|
15
|
+
"listItem",
|
|
16
|
+
"taskList",
|
|
17
|
+
"taskItem",
|
|
18
|
+
"notionToggle",
|
|
19
|
+
"notionCallout",
|
|
20
|
+
"notionColumns",
|
|
21
|
+
"notionColumn",
|
|
22
|
+
"notionSyncedBlock",
|
|
23
|
+
"table",
|
|
24
|
+
"tableRow",
|
|
25
|
+
"tableHeader",
|
|
26
|
+
"tableCell",
|
|
27
|
+
"image",
|
|
28
|
+
"video",
|
|
29
|
+
"audio",
|
|
30
|
+
"notionBlockAtom",
|
|
31
|
+
"registryBlock",
|
|
32
|
+
"contentReference",
|
|
33
|
+
"localMdxComponent",
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export type BlocksFieldBlockKind = (typeof BLOCKS_FIELD_BLOCK_KINDS)[number];
|
|
37
|
+
export type BlocksFieldBlockOperation =
|
|
38
|
+
| "insert"
|
|
39
|
+
| "update"
|
|
40
|
+
| "upsert"
|
|
41
|
+
| "delete"
|
|
42
|
+
| "reorder";
|
|
43
|
+
|
|
44
|
+
const FULL_BLOCK_OPERATIONS = [
|
|
45
|
+
"insert",
|
|
46
|
+
"update",
|
|
47
|
+
"upsert",
|
|
48
|
+
"delete",
|
|
49
|
+
"reorder",
|
|
50
|
+
] as const;
|
|
51
|
+
const ORDER_ONLY_BLOCK_OPERATIONS = ["delete", "reorder"] as const;
|
|
52
|
+
|
|
53
|
+
export const BLOCKS_FIELD_OPERATION_CAPABILITIES: Readonly<
|
|
54
|
+
Record<BlocksFieldBlockKind, readonly BlocksFieldBlockOperation[]>
|
|
55
|
+
> = {
|
|
56
|
+
paragraph: FULL_BLOCK_OPERATIONS,
|
|
57
|
+
heading: FULL_BLOCK_OPERATIONS,
|
|
58
|
+
horizontalRule: FULL_BLOCK_OPERATIONS,
|
|
59
|
+
codeBlock: FULL_BLOCK_OPERATIONS,
|
|
60
|
+
blockquote: FULL_BLOCK_OPERATIONS,
|
|
61
|
+
bulletList: [],
|
|
62
|
+
orderedList: [],
|
|
63
|
+
listItem: ORDER_ONLY_BLOCK_OPERATIONS,
|
|
64
|
+
taskList: [],
|
|
65
|
+
taskItem: ORDER_ONLY_BLOCK_OPERATIONS,
|
|
66
|
+
notionToggle: FULL_BLOCK_OPERATIONS,
|
|
67
|
+
notionCallout: FULL_BLOCK_OPERATIONS,
|
|
68
|
+
notionColumns: FULL_BLOCK_OPERATIONS,
|
|
69
|
+
notionColumn: FULL_BLOCK_OPERATIONS,
|
|
70
|
+
notionSyncedBlock: FULL_BLOCK_OPERATIONS,
|
|
71
|
+
table: FULL_BLOCK_OPERATIONS,
|
|
72
|
+
tableRow: [],
|
|
73
|
+
tableHeader: [],
|
|
74
|
+
tableCell: [],
|
|
75
|
+
image: FULL_BLOCK_OPERATIONS,
|
|
76
|
+
video: FULL_BLOCK_OPERATIONS,
|
|
77
|
+
audio: FULL_BLOCK_OPERATIONS,
|
|
78
|
+
notionBlockAtom: FULL_BLOCK_OPERATIONS,
|
|
79
|
+
registryBlock: FULL_BLOCK_OPERATIONS,
|
|
80
|
+
contentReference: FULL_BLOCK_OPERATIONS,
|
|
81
|
+
localMdxComponent: FULL_BLOCK_OPERATIONS,
|
|
82
|
+
};
|
|
83
|
+
|
|
7
84
|
export type BlocksFieldIdentityStatus = "legacy" | "materialized" | "stale";
|
|
8
85
|
|
|
9
86
|
export interface BlocksFieldBlock {
|
|
@@ -46,7 +123,7 @@ export interface StoredBlocksFieldIdentity {
|
|
|
46
123
|
blocks: StoredBlocksFieldBlock[];
|
|
47
124
|
}
|
|
48
125
|
|
|
49
|
-
interface
|
|
126
|
+
export interface BlocksFieldBlockSnapshot {
|
|
50
127
|
path: string;
|
|
51
128
|
parentPath: string | null;
|
|
52
129
|
kind: string;
|
|
@@ -57,34 +134,7 @@ interface BlockSnapshot {
|
|
|
57
134
|
preferredId: string | null;
|
|
58
135
|
}
|
|
59
136
|
|
|
60
|
-
const BLOCK_NODE_TYPES = new Set(
|
|
61
|
-
"paragraph",
|
|
62
|
-
"heading",
|
|
63
|
-
"horizontalRule",
|
|
64
|
-
"codeBlock",
|
|
65
|
-
"blockquote",
|
|
66
|
-
"bulletList",
|
|
67
|
-
"orderedList",
|
|
68
|
-
"listItem",
|
|
69
|
-
"taskList",
|
|
70
|
-
"taskItem",
|
|
71
|
-
"notionToggle",
|
|
72
|
-
"notionCallout",
|
|
73
|
-
"notionColumns",
|
|
74
|
-
"notionColumn",
|
|
75
|
-
"notionSyncedBlock",
|
|
76
|
-
"table",
|
|
77
|
-
"tableRow",
|
|
78
|
-
"tableHeader",
|
|
79
|
-
"tableCell",
|
|
80
|
-
"image",
|
|
81
|
-
"video",
|
|
82
|
-
"audio",
|
|
83
|
-
"notionBlockAtom",
|
|
84
|
-
"registryBlock",
|
|
85
|
-
"contentReference",
|
|
86
|
-
"localMdxComponent",
|
|
87
|
-
]);
|
|
137
|
+
const BLOCK_NODE_TYPES = new Set<string>(BLOCKS_FIELD_BLOCK_KINDS);
|
|
88
138
|
|
|
89
139
|
const NON_ADDRESSABLE_NODE_TYPES = new Set([
|
|
90
140
|
"bulletList",
|
|
@@ -133,8 +183,10 @@ function nodeMarkdown(node: PMNode): string {
|
|
|
133
183
|
return docToNfm({ type: "doc", content: [node] });
|
|
134
184
|
}
|
|
135
185
|
|
|
136
|
-
function
|
|
137
|
-
|
|
186
|
+
export function snapshotBlocksFieldMarkdown(
|
|
187
|
+
markdown: string,
|
|
188
|
+
): BlocksFieldBlockSnapshot[] {
|
|
189
|
+
const snapshots: BlocksFieldBlockSnapshot[] = [];
|
|
138
190
|
const doc = nfmToDoc(markdown);
|
|
139
191
|
|
|
140
192
|
function visit(nodes: PMNode[] | undefined, parentPath: string | null) {
|
|
@@ -172,7 +224,7 @@ function snapshotMarkdown(markdown: string): BlockSnapshot[] {
|
|
|
172
224
|
|
|
173
225
|
function deterministicBlockId(
|
|
174
226
|
fieldId: string,
|
|
175
|
-
snapshot: Pick<
|
|
227
|
+
snapshot: Pick<BlocksFieldBlockSnapshot, "path" | "kind" | "contentHash">,
|
|
176
228
|
): string {
|
|
177
229
|
return `block_${hashString(
|
|
178
230
|
`${fieldId}\0${snapshot.path}\0${snapshot.kind}\0${snapshot.contentHash}`,
|
|
@@ -225,7 +277,7 @@ export function legacyBlocksFieldIdentity(args: {
|
|
|
225
277
|
markdown: string;
|
|
226
278
|
}): BlocksFieldIdentity {
|
|
227
279
|
const fieldId = blocksFieldId(args.documentId, args.propertyId);
|
|
228
|
-
const snapshots =
|
|
280
|
+
const snapshots = snapshotBlocksFieldMarkdown(args.markdown);
|
|
229
281
|
const idByPath = new Map<string, string>();
|
|
230
282
|
const usedIds = new Set<string>();
|
|
231
283
|
const blocks: StoredBlocksFieldBlock[] = snapshots.map((snapshot) => {
|
|
@@ -308,9 +360,10 @@ export function reconcileBlocksFieldIdentity(args: {
|
|
|
308
360
|
previous: StoredBlocksFieldIdentity;
|
|
309
361
|
markdown: string;
|
|
310
362
|
createId: () => string;
|
|
363
|
+
preferredIdsByPath?: Readonly<Record<string, string>>;
|
|
311
364
|
}): StoredBlocksFieldIdentity {
|
|
312
365
|
const nextRevision = args.previous.revision + 1;
|
|
313
|
-
const snapshots =
|
|
366
|
+
const snapshots = snapshotBlocksFieldMarkdown(args.markdown);
|
|
314
367
|
const previousLive = args.previous.blocks.filter(
|
|
315
368
|
(block) => block.state === "live",
|
|
316
369
|
);
|
|
@@ -320,6 +373,21 @@ export function reconcileBlocksFieldIdentity(args: {
|
|
|
320
373
|
const matchedPrevious = new Set<number>();
|
|
321
374
|
const assigned = new Map<number, StoredBlocksFieldBlock>();
|
|
322
375
|
const usedIds = new Set(args.previous.blocks.map((block) => block.id));
|
|
376
|
+
const assignedNextIds = new Set<string>();
|
|
377
|
+
|
|
378
|
+
const previousLiveById = new Map(
|
|
379
|
+
previousLive.map((block, index) => [block.id, index]),
|
|
380
|
+
);
|
|
381
|
+
for (let nextIndex = 0; nextIndex < snapshots.length; nextIndex++) {
|
|
382
|
+
const explicitId = args.preferredIdsByPath?.[snapshots[nextIndex]!.path];
|
|
383
|
+
if (!explicitId) continue;
|
|
384
|
+
const previousIndex = previousLiveById.get(explicitId);
|
|
385
|
+
if (previousIndex === undefined || matchedPrevious.has(previousIndex)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
matchedPrevious.add(previousIndex);
|
|
389
|
+
assigned.set(nextIndex, previousLive[previousIndex]!);
|
|
390
|
+
}
|
|
323
391
|
|
|
324
392
|
const previousExact = uniqueIndexByKey(
|
|
325
393
|
previousLive,
|
|
@@ -440,8 +508,9 @@ export function reconcileBlocksFieldIdentity(args: {
|
|
|
440
508
|
const recoveredIds = new Set<string>();
|
|
441
509
|
const idByPath = new Map<string, string>();
|
|
442
510
|
const nextBlocks = snapshots.map((snapshot, nextIndex) => {
|
|
511
|
+
const explicitId = args.preferredIdsByPath?.[snapshot.path];
|
|
443
512
|
let previous = assigned.get(nextIndex);
|
|
444
|
-
if (!previous) {
|
|
513
|
+
if (!previous && !explicitId) {
|
|
445
514
|
const recoveredIndex = recoverable.get(
|
|
446
515
|
`${snapshot.kind}\0${snapshot.contentHash}`,
|
|
447
516
|
);
|
|
@@ -453,16 +522,24 @@ export function reconcileBlocksFieldIdentity(args: {
|
|
|
453
522
|
}
|
|
454
523
|
}
|
|
455
524
|
}
|
|
525
|
+
if (explicitId && usedIds.has(explicitId) && explicitId !== previous?.id) {
|
|
526
|
+
throw new Error(`Preferred Block ID is already reserved: ${explicitId}`);
|
|
527
|
+
}
|
|
456
528
|
let id =
|
|
529
|
+
explicitId ??
|
|
457
530
|
previous?.id ??
|
|
458
531
|
fieldScopedBlockId(
|
|
459
532
|
args.previous.fieldId,
|
|
460
533
|
snapshot.preferredId ?? args.createId(),
|
|
461
534
|
);
|
|
462
|
-
while (
|
|
535
|
+
while (
|
|
536
|
+
assignedNextIds.has(id) ||
|
|
537
|
+
(!explicitId && usedIds.has(id) && id !== previous?.id)
|
|
538
|
+
) {
|
|
463
539
|
id = fieldScopedBlockId(args.previous.fieldId, args.createId());
|
|
464
540
|
}
|
|
465
541
|
usedIds.add(id);
|
|
542
|
+
assignedNextIds.add(id);
|
|
466
543
|
idByPath.set(snapshot.path, id);
|
|
467
544
|
return {
|
|
468
545
|
id,
|
|
@@ -537,7 +614,7 @@ export function materializeLegacyBlocksFieldIdentity(args: {
|
|
|
537
614
|
markdown: string;
|
|
538
615
|
}): StoredBlocksFieldIdentity {
|
|
539
616
|
const publicState = legacyBlocksFieldIdentity(args);
|
|
540
|
-
const snapshots =
|
|
617
|
+
const snapshots = snapshotBlocksFieldMarkdown(args.markdown);
|
|
541
618
|
return {
|
|
542
619
|
fieldId: publicState.fieldId,
|
|
543
620
|
revision: 0,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ContentDatabaseMutationTarget } from "./api.js";
|
|
2
|
+
import type {
|
|
3
|
+
BlocksFieldBlockKind,
|
|
4
|
+
BlocksFieldBlockOperation,
|
|
5
|
+
BlocksFieldIdentityStatus,
|
|
6
|
+
} from "./blocks-field-identity.js";
|
|
7
|
+
|
|
8
|
+
export interface ContentDatabaseBlockTarget extends ContentDatabaseMutationTarget {
|
|
9
|
+
itemId: string;
|
|
10
|
+
rowDocumentId: string;
|
|
11
|
+
propertyId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ContentDatabaseBlockValue {
|
|
15
|
+
format: "nfm";
|
|
16
|
+
nfm: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ContentDatabaseBlock {
|
|
20
|
+
id: string;
|
|
21
|
+
parentId: string | null;
|
|
22
|
+
kind: BlocksFieldBlockKind;
|
|
23
|
+
index: number;
|
|
24
|
+
addressable: boolean;
|
|
25
|
+
value: ContentDatabaseBlockValue;
|
|
26
|
+
supportedOperations: readonly BlocksFieldBlockOperation[];
|
|
27
|
+
degraded: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ContentDatabaseBlocksReadResult {
|
|
31
|
+
target: ContentDatabaseBlockTarget;
|
|
32
|
+
rowLink: { urlPath: string; label: string };
|
|
33
|
+
schemaRevision: string;
|
|
34
|
+
rowRevision: string;
|
|
35
|
+
fieldRevision: number;
|
|
36
|
+
identityStatus: BlocksFieldIdentityStatus;
|
|
37
|
+
total: number;
|
|
38
|
+
order: string[];
|
|
39
|
+
blocks: ContentDatabaseBlock[];
|
|
40
|
+
page: { offset: number; limit: number; nextCursor: string | null };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ContentDatabaseBlockMutationOperation =
|
|
44
|
+
| "insert"
|
|
45
|
+
| "update"
|
|
46
|
+
| "upsert"
|
|
47
|
+
| "delete"
|
|
48
|
+
| "reorder";
|
|
49
|
+
|
|
50
|
+
export interface ContentDatabaseBlockMutationReceipt {
|
|
51
|
+
receiptId: string;
|
|
52
|
+
operation: ContentDatabaseBlockMutationOperation;
|
|
53
|
+
outcome: "inserted" | "updated" | "deleted" | "reordered" | "unchanged";
|
|
54
|
+
target: ContentDatabaseBlockTarget;
|
|
55
|
+
rowLink: { urlPath: string; label: string };
|
|
56
|
+
schemaRevision: string;
|
|
57
|
+
idempotency: {
|
|
58
|
+
key: string;
|
|
59
|
+
result: "applied" | "replayed";
|
|
60
|
+
payloadDigest: string;
|
|
61
|
+
};
|
|
62
|
+
revisions: {
|
|
63
|
+
row: { before: string; after: string };
|
|
64
|
+
field: { before: number; after: number };
|
|
65
|
+
};
|
|
66
|
+
affected: {
|
|
67
|
+
blockIds: string[];
|
|
68
|
+
deletedBlockIds: string[];
|
|
69
|
+
order: string[];
|
|
70
|
+
};
|
|
71
|
+
readback: {
|
|
72
|
+
verified: true;
|
|
73
|
+
fieldRevision: number;
|
|
74
|
+
contentHash: string;
|
|
75
|
+
order: string[];
|
|
76
|
+
blocks: ContentDatabaseBlock[];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ContentDatabaseBlockMutationResult {
|
|
81
|
+
receipt: ContentDatabaseBlockMutationReceipt;
|
|
82
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BLOCKS_FIELD_BLOCK_KINDS,
|
|
3
|
+
BLOCKS_FIELD_OPERATION_CAPABILITIES,
|
|
4
|
+
type BlocksFieldBlockKind,
|
|
5
|
+
type BlocksFieldBlockOperation,
|
|
6
|
+
type BlocksFieldIdentity,
|
|
7
|
+
} from "./blocks-field-identity.js";
|
|
8
|
+
import { docToNfm, nfmToDoc, type PMDoc, type PMNode } from "./nfm.js";
|
|
9
|
+
|
|
10
|
+
type Placement =
|
|
11
|
+
| { placement: "start" | "end"; parentBlockId?: string | null }
|
|
12
|
+
| { placement: "before" | "after"; anchorBlockId: string };
|
|
13
|
+
|
|
14
|
+
export type BlockDocumentMutation =
|
|
15
|
+
| {
|
|
16
|
+
operation: "insert";
|
|
17
|
+
block: { kind: BlocksFieldBlockKind; nfm: string };
|
|
18
|
+
position: Placement;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
operation: "update" | "upsert";
|
|
22
|
+
blockId: string;
|
|
23
|
+
block: { kind: BlocksFieldBlockKind; nfm: string };
|
|
24
|
+
position?: Placement;
|
|
25
|
+
}
|
|
26
|
+
| { operation: "delete"; blockId: string }
|
|
27
|
+
| { operation: "reorder"; blockId: string; position: Placement };
|
|
28
|
+
|
|
29
|
+
interface NodeRef {
|
|
30
|
+
node: PMNode;
|
|
31
|
+
nodes: PMNode[];
|
|
32
|
+
nodeIndex: number;
|
|
33
|
+
path: string;
|
|
34
|
+
parentPath: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BlockDocumentMutationResult {
|
|
38
|
+
markdown: string;
|
|
39
|
+
preferredIdsByPath: Record<string, string>;
|
|
40
|
+
requestedBlockId: string | null;
|
|
41
|
+
deletedCandidateIds: string[];
|
|
42
|
+
changed: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const BLOCK_KIND_SET = new Set<string>(BLOCKS_FIELD_BLOCK_KINDS);
|
|
46
|
+
|
|
47
|
+
function collectNodeRefs(doc: PMDoc): NodeRef[] {
|
|
48
|
+
const refs: NodeRef[] = [];
|
|
49
|
+
const visit = (nodes: PMNode[] | undefined, parentPath: string | null) => {
|
|
50
|
+
let blockPosition = 0;
|
|
51
|
+
for (let nodeIndex = 0; nodeIndex < (nodes?.length ?? 0); nodeIndex++) {
|
|
52
|
+
const node = nodes![nodeIndex]!;
|
|
53
|
+
if (!BLOCK_KIND_SET.has(node.type)) continue;
|
|
54
|
+
const path =
|
|
55
|
+
parentPath === null
|
|
56
|
+
? `${blockPosition}`
|
|
57
|
+
: `${parentPath}.${blockPosition}`;
|
|
58
|
+
refs.push({ node, nodes: nodes!, nodeIndex, path, parentPath });
|
|
59
|
+
visit(node.content, path);
|
|
60
|
+
blockPosition++;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
visit(doc.content, null);
|
|
64
|
+
return refs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function indexIdentity(markdown: string, identity: BlocksFieldIdentity) {
|
|
68
|
+
const doc = nfmToDoc(markdown);
|
|
69
|
+
const refs = collectNodeRefs(doc);
|
|
70
|
+
if (refs.length !== identity.blocks.length) {
|
|
71
|
+
throw new Error("Blocks identity does not match the current field body.");
|
|
72
|
+
}
|
|
73
|
+
const byId = new Map<string, NodeRef>();
|
|
74
|
+
const idByNode = new Map<PMNode, string>();
|
|
75
|
+
refs.forEach((ref, index) => {
|
|
76
|
+
const block = identity.blocks[index]!;
|
|
77
|
+
if (block.kind !== ref.node.type) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"Blocks identity kind does not match the current field body.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
byId.set(block.id, ref);
|
|
83
|
+
idByNode.set(ref.node, block.id);
|
|
84
|
+
});
|
|
85
|
+
return { doc, byId, idByNode };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parsedBlock(kind: BlocksFieldBlockKind, nfm: string): PMNode {
|
|
89
|
+
const doc = nfmToDoc(nfm);
|
|
90
|
+
if (doc.content.length !== 1 || doc.content[0]?.type !== kind) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Block value must be canonical NFM containing exactly one top-level ${kind} block.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return doc.content[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function requireOperation(
|
|
99
|
+
kind: BlocksFieldBlockKind,
|
|
100
|
+
operation: BlocksFieldBlockOperation,
|
|
101
|
+
) {
|
|
102
|
+
const supported = BLOCKS_FIELD_OPERATION_CAPABILITIES[
|
|
103
|
+
kind
|
|
104
|
+
] as readonly string[];
|
|
105
|
+
if (!supported.includes(operation as string)) {
|
|
106
|
+
throw new Error(`Block kind "${kind}" does not support ${operation}.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function destination(
|
|
111
|
+
position: Placement,
|
|
112
|
+
byId: Map<string, NodeRef>,
|
|
113
|
+
doc: PMDoc,
|
|
114
|
+
): { nodes: PMNode[]; index: number; parentPath: string | null } {
|
|
115
|
+
if ("anchorBlockId" in position) {
|
|
116
|
+
const anchor = byId.get(position.anchorBlockId);
|
|
117
|
+
if (!anchor) throw new Error("Anchor block not found.");
|
|
118
|
+
return {
|
|
119
|
+
nodes: anchor.nodes,
|
|
120
|
+
index: anchor.nodeIndex + (position.placement === "after" ? 1 : 0),
|
|
121
|
+
parentPath: anchor.parentPath,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!position.parentBlockId) {
|
|
125
|
+
return {
|
|
126
|
+
nodes: doc.content,
|
|
127
|
+
index: position.placement === "start" ? 0 : doc.content.length,
|
|
128
|
+
parentPath: null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const parent = byId.get(position.parentBlockId);
|
|
132
|
+
if (!parent) throw new Error("Parent block not found.");
|
|
133
|
+
const nodes = (parent.node.content ??= []);
|
|
134
|
+
return {
|
|
135
|
+
nodes,
|
|
136
|
+
index: position.placement === "start" ? 0 : nodes.length,
|
|
137
|
+
parentPath: parent.path,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function assertCompatibleParent(
|
|
142
|
+
parentPath: string | null,
|
|
143
|
+
kind: BlocksFieldBlockKind,
|
|
144
|
+
byId: Map<string, NodeRef>,
|
|
145
|
+
) {
|
|
146
|
+
const parent = [...byId.values()].find(
|
|
147
|
+
(ref) => ref.path === parentPath,
|
|
148
|
+
)?.node;
|
|
149
|
+
const parentKind = parent?.type;
|
|
150
|
+
const generalContainerKinds = new Set([
|
|
151
|
+
"blockquote",
|
|
152
|
+
"listItem",
|
|
153
|
+
"taskItem",
|
|
154
|
+
"notionToggle",
|
|
155
|
+
"notionCallout",
|
|
156
|
+
"notionColumn",
|
|
157
|
+
"notionSyncedBlock",
|
|
158
|
+
"tableHeader",
|
|
159
|
+
"tableCell",
|
|
160
|
+
]);
|
|
161
|
+
const valid =
|
|
162
|
+
parentKind === undefined
|
|
163
|
+
? ![
|
|
164
|
+
"listItem",
|
|
165
|
+
"taskItem",
|
|
166
|
+
"notionColumn",
|
|
167
|
+
"tableRow",
|
|
168
|
+
"tableHeader",
|
|
169
|
+
"tableCell",
|
|
170
|
+
].includes(kind)
|
|
171
|
+
: parentKind === "bulletList" || parentKind === "orderedList"
|
|
172
|
+
? kind === "listItem"
|
|
173
|
+
: parentKind === "taskList"
|
|
174
|
+
? kind === "taskItem"
|
|
175
|
+
: parentKind === "notionColumns"
|
|
176
|
+
? kind === "notionColumn"
|
|
177
|
+
: parentKind === "table"
|
|
178
|
+
? kind === "tableRow"
|
|
179
|
+
: parentKind === "tableRow"
|
|
180
|
+
? kind === "tableHeader" || kind === "tableCell"
|
|
181
|
+
: generalContainerKinds.has(parentKind) &&
|
|
182
|
+
![
|
|
183
|
+
"listItem",
|
|
184
|
+
"taskItem",
|
|
185
|
+
"notionColumn",
|
|
186
|
+
"tableRow",
|
|
187
|
+
"tableHeader",
|
|
188
|
+
"tableCell",
|
|
189
|
+
].includes(kind);
|
|
190
|
+
if (!valid) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Block kind "${kind}" is not valid inside ${parentKind ?? "the field root"}.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function repositionNode(args: {
|
|
198
|
+
blockId: string;
|
|
199
|
+
current: NodeRef;
|
|
200
|
+
node: PMNode;
|
|
201
|
+
position: Placement;
|
|
202
|
+
byId: Map<string, NodeRef>;
|
|
203
|
+
doc: PMDoc;
|
|
204
|
+
}) {
|
|
205
|
+
if (
|
|
206
|
+
"anchorBlockId" in args.position &&
|
|
207
|
+
args.position.anchorBlockId === args.blockId
|
|
208
|
+
) {
|
|
209
|
+
throw new Error("A block cannot be reordered relative to itself.");
|
|
210
|
+
}
|
|
211
|
+
const target = destination(args.position, args.byId, args.doc);
|
|
212
|
+
if (target.parentPath !== args.current.parentPath) {
|
|
213
|
+
throw new Error("Cross-parent block reorder is not supported.");
|
|
214
|
+
}
|
|
215
|
+
args.current.nodes.splice(args.current.nodeIndex, 1);
|
|
216
|
+
const targetIndex =
|
|
217
|
+
"anchorBlockId" in args.position
|
|
218
|
+
? (() => {
|
|
219
|
+
const anchor = args.byId.get(args.position.anchorBlockId);
|
|
220
|
+
if (!anchor || anchor.nodes !== target.nodes) {
|
|
221
|
+
throw new Error("Reorder anchor is outside the current parent.");
|
|
222
|
+
}
|
|
223
|
+
const anchorIndex = target.nodes.indexOf(anchor.node);
|
|
224
|
+
return anchorIndex + (args.position.placement === "after" ? 1 : 0);
|
|
225
|
+
})()
|
|
226
|
+
: args.position.placement === "start"
|
|
227
|
+
? 0
|
|
228
|
+
: target.nodes.length;
|
|
229
|
+
target.nodes.splice(targetIndex, 0, args.node);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function preferredIds(
|
|
233
|
+
doc: PMDoc,
|
|
234
|
+
idByNode: Map<PMNode, string>,
|
|
235
|
+
requestedNode?: PMNode,
|
|
236
|
+
requestedId?: string,
|
|
237
|
+
createInsertedDescendantId?: () => string,
|
|
238
|
+
) {
|
|
239
|
+
const requestedDescendants = new Set<PMNode>();
|
|
240
|
+
const collectRequestedDescendants = (nodes: PMNode[] | undefined) => {
|
|
241
|
+
for (const node of nodes ?? []) {
|
|
242
|
+
requestedDescendants.add(node);
|
|
243
|
+
collectRequestedDescendants(node.content);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
if (requestedId && requestedNode) {
|
|
247
|
+
collectRequestedDescendants(requestedNode.content);
|
|
248
|
+
}
|
|
249
|
+
return Object.fromEntries(
|
|
250
|
+
collectNodeRefs(doc).flatMap((ref) => {
|
|
251
|
+
const id =
|
|
252
|
+
idByNode.get(ref.node) ??
|
|
253
|
+
(ref.node === requestedNode
|
|
254
|
+
? requestedId
|
|
255
|
+
: requestedDescendants.has(ref.node)
|
|
256
|
+
? createInsertedDescendantId?.()
|
|
257
|
+
: undefined);
|
|
258
|
+
if (
|
|
259
|
+
requestedDescendants.has(ref.node) &&
|
|
260
|
+
!id &&
|
|
261
|
+
!createInsertedDescendantId
|
|
262
|
+
) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
"Inserting a nested block requires fresh descendant Block IDs.",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return id ? [[ref.path, id]] : [];
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function mutateBlocksFieldDocument(args: {
|
|
273
|
+
markdown: string;
|
|
274
|
+
identity: BlocksFieldIdentity;
|
|
275
|
+
mutation: BlockDocumentMutation;
|
|
276
|
+
insertedBlockId?: string;
|
|
277
|
+
createInsertedDescendantId?: () => string;
|
|
278
|
+
}): BlockDocumentMutationResult {
|
|
279
|
+
const indexed = indexIdentity(args.markdown, args.identity);
|
|
280
|
+
const { doc, byId, idByNode } = indexed;
|
|
281
|
+
const before = docToNfm(doc);
|
|
282
|
+
let requestedNode: PMNode | undefined;
|
|
283
|
+
let requestedBlockId: string | null = null;
|
|
284
|
+
let deletedCandidateIds: string[] = [];
|
|
285
|
+
|
|
286
|
+
if (args.mutation.operation === "insert") {
|
|
287
|
+
requireOperation(args.mutation.block.kind, "insert");
|
|
288
|
+
requestedNode = parsedBlock(
|
|
289
|
+
args.mutation.block.kind,
|
|
290
|
+
args.mutation.block.nfm,
|
|
291
|
+
);
|
|
292
|
+
const target = destination(args.mutation.position, byId, doc);
|
|
293
|
+
assertCompatibleParent(target.parentPath, args.mutation.block.kind, byId);
|
|
294
|
+
target.nodes.splice(target.index, 0, requestedNode);
|
|
295
|
+
} else {
|
|
296
|
+
const current = byId.get(args.mutation.blockId);
|
|
297
|
+
if (!current) throw new Error("Block not found.");
|
|
298
|
+
const currentKind = current.node.type as BlocksFieldBlockKind;
|
|
299
|
+
requireOperation(currentKind, args.mutation.operation);
|
|
300
|
+
requestedBlockId = args.mutation.blockId;
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
args.mutation.operation === "update" ||
|
|
304
|
+
args.mutation.operation === "upsert"
|
|
305
|
+
) {
|
|
306
|
+
if (args.mutation.block.kind !== currentKind) {
|
|
307
|
+
throw new Error("Individual block mutations cannot change block kind.");
|
|
308
|
+
}
|
|
309
|
+
requestedNode = parsedBlock(currentKind, args.mutation.block.nfm);
|
|
310
|
+
current.nodes[current.nodeIndex] = requestedNode;
|
|
311
|
+
current.node = requestedNode;
|
|
312
|
+
idByNode.set(requestedNode, args.mutation.blockId);
|
|
313
|
+
if (args.mutation.operation === "upsert" && args.mutation.position) {
|
|
314
|
+
repositionNode({
|
|
315
|
+
blockId: args.mutation.blockId,
|
|
316
|
+
current,
|
|
317
|
+
node: requestedNode,
|
|
318
|
+
position: args.mutation.position,
|
|
319
|
+
byId,
|
|
320
|
+
doc,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
} else if (args.mutation.operation === "delete") {
|
|
324
|
+
deletedCandidateIds = args.identity.blocks
|
|
325
|
+
.filter((block) => {
|
|
326
|
+
const ref = byId.get(block.id);
|
|
327
|
+
return (
|
|
328
|
+
ref?.path === current.path ||
|
|
329
|
+
ref?.path.startsWith(`${current.path}.`)
|
|
330
|
+
);
|
|
331
|
+
})
|
|
332
|
+
.map((block) => block.id);
|
|
333
|
+
current.nodes.splice(current.nodeIndex, 1);
|
|
334
|
+
} else if (args.mutation.operation === "reorder") {
|
|
335
|
+
repositionNode({
|
|
336
|
+
blockId: args.mutation.blockId,
|
|
337
|
+
current,
|
|
338
|
+
node: current.node,
|
|
339
|
+
position: args.mutation.position,
|
|
340
|
+
byId,
|
|
341
|
+
doc,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const markdown = docToNfm(doc);
|
|
347
|
+
const preferredIdsByPath = preferredIds(
|
|
348
|
+
doc,
|
|
349
|
+
idByNode,
|
|
350
|
+
requestedNode,
|
|
351
|
+
args.insertedBlockId,
|
|
352
|
+
args.createInsertedDescendantId,
|
|
353
|
+
);
|
|
354
|
+
const identityOrderChanged = collectNodeRefs(doc).some(
|
|
355
|
+
(ref, index) =>
|
|
356
|
+
preferredIdsByPath[ref.path] !== undefined &&
|
|
357
|
+
preferredIdsByPath[ref.path] !== args.identity.blocks[index]?.id,
|
|
358
|
+
);
|
|
359
|
+
return {
|
|
360
|
+
markdown,
|
|
361
|
+
preferredIdsByPath,
|
|
362
|
+
requestedBlockId,
|
|
363
|
+
deletedCandidateIds,
|
|
364
|
+
changed: markdown !== before || identityOrderChanged,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
@@ -986,7 +986,8 @@ function serializeToggle(node: PMNode, ind: number): string[] {
|
|
|
986
986
|
return out;
|
|
987
987
|
}
|
|
988
988
|
const attrStr = serializeAttrs([["color", isColor(color) ? color : null]]);
|
|
989
|
-
|
|
989
|
+
const openAttr = node.attrs?.open === true ? " open" : "";
|
|
990
|
+
out.push(indentStr(ind) + `<details${attrStr}${openAttr}>`);
|
|
990
991
|
out.push(indentStr(ind) + `<summary>${summary}</summary>`);
|
|
991
992
|
out.push(...serializeBlocks(node.content || [], ind + 1));
|
|
992
993
|
out.push(indentStr(ind) + "</details>");
|
|
@@ -2006,7 +2007,7 @@ function parseContainer(
|
|
|
2006
2007
|
attrs: {
|
|
2007
2008
|
summary,
|
|
2008
2009
|
headingLevel: null,
|
|
2009
|
-
open:
|
|
2010
|
+
open: attrs.open !== undefined || /(?:^|\s)open(?:\s|>)/.test(openLine),
|
|
2010
2011
|
color: isColor(attrs.color) ? attrs.color : null,
|
|
2011
2012
|
indent: 0,
|
|
2012
2013
|
},
|