@agent-native/core-corpus 0.216.0 → 0.218.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/.agents/skills/session-replay/SKILL.md +61 -1
- package/corpus/templates/analytics/AGENTS.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +3 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
- package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
- package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
- package/corpus/templates/analytics/actions/view-screen.ts +29 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
- package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
- package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
- package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +67 -0
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
- package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
- package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
- package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
- package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
- package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
- package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
- package/corpus/templates/analytics/server/plugins/db.ts +46 -0
- package/corpus/templates/analytics/shared/output-use.ts +205 -0
- package/corpus/templates/analytics/shared/session-events.ts +6 -0
- package/corpus/templates/assets/CHANGELOG.md +6 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
- package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
- package/corpus/templates/brain/app/routes/sources.tsx +1 -1
- package/corpus/templates/brain/vitest.config.ts +7 -0
- package/corpus/templates/calendar/CHANGELOG.md +12 -0
- package/corpus/templates/calendar/actions/create-event.ts +2 -4
- package/corpus/templates/calendar/actions/update-event.ts +35 -6
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
- package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
- package/corpus/templates/clips/CHANGELOG.md +7 -20
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
- package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
- package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
- package/corpus/templates/content/CHANGELOG.md +10 -0
- package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
- package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
- package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
- package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
- package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
- package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
- package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
- package/corpus/templates/content/actions/_database-utils.ts +93 -7
- package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
- package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
- package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
- package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
- package/corpus/templates/content/actions/delete-document.ts +41 -3
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
- package/corpus/templates/content/actions/get-document.ts +2 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
- package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
- package/corpus/templates/content/actions/update-document.ts +139 -20
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +119 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +56 -13
- package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
- package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
- package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
- package/corpus/templates/content/app/global.css +31 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
- package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
- package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +3 -1
- package/corpus/templates/content/server/db/schema.ts +20 -0
- package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
- package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
- package/corpus/templates/content/server/live-copy/binding.ts +31 -0
- package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
- package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
- package/corpus/templates/content/server/live-copy/holds.ts +59 -0
- package/corpus/templates/content/server/live-copy/persist.ts +656 -0
- package/corpus/templates/content/server/live-copy/switch.ts +136 -0
- package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
- package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
- package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
- package/corpus/templates/content/server/live-copy/write.ts +85 -0
- package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
- package/corpus/templates/content/server/plugins/collab.ts +3 -0
- package/corpus/templates/content/server/plugins/db.ts +28 -0
- package/corpus/templates/content/shared/api.ts +30 -1
- package/corpus/templates/content/shared/database-item-layout.ts +54 -22
- package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
- package/corpus/templates/content/shared/og-description.ts +258 -36
- package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
- package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
- package/corpus/templates/content/shared/suggestion-text.ts +20 -5
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
- package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
- package/corpus/templates/design/CHANGELOG.md +5 -0
- package/corpus/templates/design/actions/get-getting-started.ts +53 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
- package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
- package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
- package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
- package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
- package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
- package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
- package/corpus/templates/design/app/global.css +6 -0
- package/corpus/templates/design/app/i18n/en-US.ts +17 -0
- package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
- package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
- package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +28 -67
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
- package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
- package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
- package/corpus/templates/design/server/lib/getting-started.ts +267 -0
- package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
- package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
- package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
- package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
- package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
- package/corpus/templates/design/shared/design-template-presets.ts +8 -0
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/plan/CHANGELOG.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
- package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +7 -0
- package/corpus/templates/slides/actions/delete-deck.ts +7 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +7 -2
- package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
- package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
- package/package.json +1 -1
- package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
- package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ActionRunContext } from "@agent-native/core/action";
|
|
2
|
+
import {
|
|
3
|
+
mutateLiveCopy,
|
|
4
|
+
readLiveCopy,
|
|
5
|
+
seedLiveCopy,
|
|
6
|
+
} from "@agent-native/core/collab";
|
|
7
|
+
import { eq } from "drizzle-orm";
|
|
8
|
+
|
|
9
|
+
import { assertSourceAllowsPageEdit } from "../../actions/_document-source.js";
|
|
10
|
+
import { getDb, schema } from "../db/index.js";
|
|
11
|
+
import {
|
|
12
|
+
liveBodyWriterFlagsOn,
|
|
13
|
+
liveBodyWriterGate,
|
|
14
|
+
liveBodyWriterPageColumns,
|
|
15
|
+
type LiveBodyWriterGate,
|
|
16
|
+
} from "../lib/live-body-writer.js";
|
|
17
|
+
import { CONTENT_LIVE_COPY_EDITS, contentLiveCopy } from "./binding.js";
|
|
18
|
+
import {
|
|
19
|
+
LiveBodyConversionError,
|
|
20
|
+
persistLiveBodyConversion,
|
|
21
|
+
} from "./persist.js";
|
|
22
|
+
import { withLiveBodyConversion } from "./write-cause.js";
|
|
23
|
+
|
|
24
|
+
const CONVERSION_ATTEMPTS = 3;
|
|
25
|
+
|
|
26
|
+
async function readPage(documentId: string) {
|
|
27
|
+
// guard:allow-unscoped — set-live-body-switch checks editor access before it switches a page.
|
|
28
|
+
const [page] = await getDb()
|
|
29
|
+
.select({
|
|
30
|
+
...liveBodyWriterPageColumns,
|
|
31
|
+
content: schema.documents.content,
|
|
32
|
+
sourceMode: schema.documents.sourceMode,
|
|
33
|
+
sourceRootPath: schema.documents.sourceRootPath,
|
|
34
|
+
sourcePath: schema.documents.sourcePath,
|
|
35
|
+
})
|
|
36
|
+
.from(schema.documents)
|
|
37
|
+
.where(eq(schema.documents.id, documentId))
|
|
38
|
+
.limit(1);
|
|
39
|
+
return page;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Switches a page to its live copy: makes the live copy hold the stored body
|
|
44
|
+
* (seeding it, or editing it to match, so open editors merge rather than
|
|
45
|
+
* reload), then marks the page switched in the same commit. The stored body
|
|
46
|
+
* wins over Yjs-only edits, so switch a page before an editor binds to it.
|
|
47
|
+
* Throws `LiveBodyConversionError` `flag_off` unless both flags are on for
|
|
48
|
+
* the page's owner, and `base_changed` when the body keeps moving.
|
|
49
|
+
*/
|
|
50
|
+
export async function switchPageToLiveCopy(
|
|
51
|
+
documentId: string,
|
|
52
|
+
options: { ctx?: ActionRunContext } = {},
|
|
53
|
+
): Promise<LiveBodyWriterGate> {
|
|
54
|
+
for (let attempt = 0; attempt < CONVERSION_ATTEMPTS; attempt += 1) {
|
|
55
|
+
const page = await readPage(documentId);
|
|
56
|
+
if (!page) return { writePath: "legacy", reason: "no_page" };
|
|
57
|
+
assertSourceAllowsPageEdit(page);
|
|
58
|
+
const gate = await liveBodyWriterGate(documentId);
|
|
59
|
+
if (gate.writePath === "live_copy") return gate;
|
|
60
|
+
if (!(await liveBodyWriterFlagsOn(page))) {
|
|
61
|
+
throw new LiveBodyConversionError(
|
|
62
|
+
"flag_off",
|
|
63
|
+
documentId,
|
|
64
|
+
`The live-copy writer is off for page ${documentId}'s owner.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const conversion = { documentId, baseRevision: page.bodyRevision };
|
|
68
|
+
try {
|
|
69
|
+
await withLiveBodyConversion(
|
|
70
|
+
conversion,
|
|
71
|
+
{ ctx: options.ctx, operation: "switch-live-body" },
|
|
72
|
+
async () => {
|
|
73
|
+
const live = await readLiveCopy(contentLiveCopy, documentId);
|
|
74
|
+
if (live.kind === "live") {
|
|
75
|
+
await mutateLiveCopy(contentLiveCopy, documentId, {
|
|
76
|
+
edit: CONTENT_LIVE_COPY_EDITS.replaceBody,
|
|
77
|
+
input: { content: page.content },
|
|
78
|
+
// An edit that changes nothing commits no live copy, so the
|
|
79
|
+
// binding's persist does not run; switch the page here.
|
|
80
|
+
persist: async (tx, change) => {
|
|
81
|
+
if (!change.changed) {
|
|
82
|
+
await persistLiveBodyConversion(
|
|
83
|
+
tx,
|
|
84
|
+
conversion,
|
|
85
|
+
change.text,
|
|
86
|
+
change.version,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const seeded = await seedLiveCopy(contentLiveCopy, documentId, {
|
|
94
|
+
source: page.content,
|
|
95
|
+
});
|
|
96
|
+
if (!seeded.seeded) {
|
|
97
|
+
throw new LiveBodyConversionError(
|
|
98
|
+
"base_changed",
|
|
99
|
+
documentId,
|
|
100
|
+
`Page ${documentId}'s live copy was seeded while it was being switched.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (
|
|
107
|
+
error instanceof LiveBodyConversionError &&
|
|
108
|
+
error.code === "base_changed"
|
|
109
|
+
) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
return liveBodyWriterGate(documentId);
|
|
115
|
+
}
|
|
116
|
+
throw new LiveBodyConversionError(
|
|
117
|
+
"base_changed",
|
|
118
|
+
documentId,
|
|
119
|
+
`Page ${documentId} kept changing while it was being switched to its live copy.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Sends a page's body writes back to the legacy path. Its live copy stays;
|
|
125
|
+
* the first legacy body write leaves it behind, so switching back converts
|
|
126
|
+
* the page again.
|
|
127
|
+
*/
|
|
128
|
+
export async function switchPageToLegacy(
|
|
129
|
+
documentId: string,
|
|
130
|
+
): Promise<LiveBodyWriterGate> {
|
|
131
|
+
await getDb()
|
|
132
|
+
.update(schema.documents)
|
|
133
|
+
.set({ liveBodySwitchedAt: null })
|
|
134
|
+
.where(eq(schema.documents.id, documentId));
|
|
135
|
+
return liveBodyWriterGate(documentId);
|
|
136
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineLiveCopyTasks,
|
|
3
|
+
type LiveCopyTaskRefusal,
|
|
4
|
+
} from "@agent-native/core/collab/live-copy-tasks";
|
|
5
|
+
import type { Node as PMNode, Schema } from "@tiptap/pm/model";
|
|
6
|
+
import {
|
|
7
|
+
initProseMirrorDoc,
|
|
8
|
+
prosemirrorToYXmlFragment,
|
|
9
|
+
updateYFragment,
|
|
10
|
+
} from "@tiptap/y-tiptap";
|
|
11
|
+
import * as Y from "yjs";
|
|
12
|
+
|
|
13
|
+
import { createContentEditorStructuralSchema } from "../../shared/content-editor-structural-schema.js";
|
|
14
|
+
import { bodyHoldsChanges } from "../../shared/document-intent-merge.js";
|
|
15
|
+
import {
|
|
16
|
+
LIVE_BODY_FIELD,
|
|
17
|
+
materializeLiveBody,
|
|
18
|
+
} from "../../shared/live-body.js";
|
|
19
|
+
import { docToNfm, nfmToDoc, parseNfmWithLosses } from "../../shared/nfm.js";
|
|
20
|
+
import { CONTENT_HOLDS_BODY_TASK, type HoldsBodyResult } from "./task-names.js";
|
|
21
|
+
|
|
22
|
+
// The worker half of Content's live copy: every handler here runs in the
|
|
23
|
+
// bounded task runner, so import only what a worker bundle can carry.
|
|
24
|
+
|
|
25
|
+
let structuralSchema: Schema | undefined;
|
|
26
|
+
|
|
27
|
+
function schema(): Schema {
|
|
28
|
+
structuralSchema ??= createContentEditorStructuralSchema();
|
|
29
|
+
return structuralSchema;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function refused(reason: string, detail?: string): LiveCopyTaskRefusal {
|
|
33
|
+
return { kind: "refused", reason, ...(detail ? { detail } : {}) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function assertBodyField(field: string): void {
|
|
37
|
+
if (field !== LIVE_BODY_FIELD) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Content's live copy binds "${LIVE_BODY_FIELD}", not "${field}".`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function deriveBody(doc: Y.Doc, field: string): string | LiveCopyTaskRefusal {
|
|
45
|
+
assertBodyField(field);
|
|
46
|
+
// Building the tree deletes what the schema rejects, and derive must not
|
|
47
|
+
// edit the doc it is given.
|
|
48
|
+
const copy = new Y.Doc();
|
|
49
|
+
try {
|
|
50
|
+
Y.applyUpdate(copy, Y.encodeStateAsUpdate(doc));
|
|
51
|
+
const body = materializeLiveBody(copy);
|
|
52
|
+
if (body.kind === "lossy") {
|
|
53
|
+
return refused("lossy", "the page schema cannot carry part of the body");
|
|
54
|
+
}
|
|
55
|
+
if (body.kind === "uninitialized") return refused("empty");
|
|
56
|
+
return body.markdown;
|
|
57
|
+
} finally {
|
|
58
|
+
copy.destroy();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The tree a Markdown body becomes in the editor. A body the parser or the
|
|
64
|
+
* schema cannot keep in full is refused: the live copy would keep the rest
|
|
65
|
+
* and the page would lose that part on its next commit.
|
|
66
|
+
*/
|
|
67
|
+
function buildBody(markdown: string): PMNode | LiveCopyTaskRefusal {
|
|
68
|
+
// What the parser dropped is in neither serialization compared below.
|
|
69
|
+
const { doc: parsed, losses } = parseNfmWithLosses(markdown);
|
|
70
|
+
if (losses.length > 0) {
|
|
71
|
+
const kinds = [...new Set(losses.map((loss) => loss.kind))].join(", ");
|
|
72
|
+
return refused("lossy", `the parser drops part of this body (${kinds})`);
|
|
73
|
+
}
|
|
74
|
+
let root: PMNode;
|
|
75
|
+
try {
|
|
76
|
+
root = schema().nodeFromJSON(parsed);
|
|
77
|
+
root.check();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return refused(
|
|
80
|
+
"unrepresentable",
|
|
81
|
+
error instanceof Error ? error.message : undefined,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (docToNfm(root.toJSON()) !== docToNfm(parsed)) {
|
|
85
|
+
return refused("lossy", "the page schema drops part of this body");
|
|
86
|
+
}
|
|
87
|
+
return root;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function replaceBodyInput(
|
|
91
|
+
input: unknown,
|
|
92
|
+
): { content: string; expected?: string } | LiveCopyTaskRefusal {
|
|
93
|
+
if (typeof input !== "object" || input === null) {
|
|
94
|
+
return refused("invalid_input", "expected { content, expected? }");
|
|
95
|
+
}
|
|
96
|
+
const { content, expected } = input as Record<string, unknown>;
|
|
97
|
+
if (typeof content !== "string") {
|
|
98
|
+
return refused("invalid_input", "content must be a string");
|
|
99
|
+
}
|
|
100
|
+
if (expected !== undefined && typeof expected !== "string") {
|
|
101
|
+
return refused("invalid_input", "expected must be a string");
|
|
102
|
+
}
|
|
103
|
+
return expected === undefined ? { content } : { content, expected };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isRefusal(value: unknown): value is LiveCopyTaskRefusal {
|
|
107
|
+
return (
|
|
108
|
+
typeof value === "object" &&
|
|
109
|
+
value !== null &&
|
|
110
|
+
(value as { kind?: unknown }).kind === "refused"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isCanonical(markdown: string): boolean {
|
|
115
|
+
try {
|
|
116
|
+
return docToNfm(nfmToDoc(markdown)) === markdown;
|
|
117
|
+
} catch {
|
|
118
|
+
// coercion-ok: a body the parser rejects is not canonical, and its save keeps its draft.
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function holdsBodyInput(input: unknown): {
|
|
124
|
+
base: string;
|
|
125
|
+
stored: string;
|
|
126
|
+
candidate: string;
|
|
127
|
+
} {
|
|
128
|
+
const { base, stored, candidate } = (input ?? {}) as Record<string, unknown>;
|
|
129
|
+
if (
|
|
130
|
+
typeof base !== "string" ||
|
|
131
|
+
typeof stored !== "string" ||
|
|
132
|
+
typeof candidate !== "string"
|
|
133
|
+
) {
|
|
134
|
+
throw new TypeError("expected { base, stored, candidate } as strings");
|
|
135
|
+
}
|
|
136
|
+
return { base, stored, candidate };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const liveCopyTasks = defineLiveCopyTasks({
|
|
140
|
+
derive: deriveBody,
|
|
141
|
+
|
|
142
|
+
seed(doc, field, source) {
|
|
143
|
+
assertBodyField(field);
|
|
144
|
+
const root = buildBody(source);
|
|
145
|
+
if (isRefusal(root)) return root;
|
|
146
|
+
prosemirrorToYXmlFragment(root, doc.getXmlFragment(field));
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
edits: {
|
|
150
|
+
/**
|
|
151
|
+
* Makes the body `content`, editing only the blocks that differ so open
|
|
152
|
+
* editors keep their place. With `expected`, refuses `base_changed`
|
|
153
|
+
* unless the current body is exactly that text.
|
|
154
|
+
*/
|
|
155
|
+
"replace-body"(doc, field, input) {
|
|
156
|
+
const parsed = replaceBodyInput(input);
|
|
157
|
+
if (isRefusal(parsed)) return parsed;
|
|
158
|
+
if (parsed.expected !== undefined) {
|
|
159
|
+
const current = deriveBody(doc, field);
|
|
160
|
+
if (isRefusal(current)) return current;
|
|
161
|
+
if (current !== parsed.expected) return refused("base_changed");
|
|
162
|
+
}
|
|
163
|
+
const root = buildBody(parsed.content);
|
|
164
|
+
if (isRefusal(root)) return root;
|
|
165
|
+
const fragment = doc.getXmlFragment(field);
|
|
166
|
+
const { meta } = initProseMirrorDoc(fragment, schema());
|
|
167
|
+
updateYFragment(doc, fragment, root, meta);
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export const tasks = {
|
|
173
|
+
...liveCopyTasks,
|
|
174
|
+
/** Whether the stored body holds every change `candidate` made to `base`. */
|
|
175
|
+
async [CONTENT_HOLDS_BODY_TASK](input: unknown): Promise<HoldsBodyResult> {
|
|
176
|
+
const { base, stored, candidate } = holdsBodyInput(input);
|
|
177
|
+
// The comparison reads the candidate in its parsed form, and a parse can
|
|
178
|
+
// drop text, such as text directly inside a table. Only an editor's own
|
|
179
|
+
// serialization says what the save holds.
|
|
180
|
+
if (!isCanonical(candidate)) return "noncanonical";
|
|
181
|
+
return bodyHoldsChanges(base, stored, candidate) ? "held" : "unheld";
|
|
182
|
+
},
|
|
183
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
import type { ActionRunContext } from "@agent-native/core/action";
|
|
4
|
+
|
|
5
|
+
import type { DocumentBodyIntent } from "../../shared/document-intent-order.js";
|
|
6
|
+
|
|
7
|
+
export interface ContentBodyIntentRecord {
|
|
8
|
+
/**
|
|
9
|
+
* Without `authoredBaseRevision`, persist records the revision the edit
|
|
10
|
+
* was applied to: the page's, under its lock.
|
|
11
|
+
*/
|
|
12
|
+
intent: Omit<DocumentBodyIntent, "authoredBaseRevision"> & {
|
|
13
|
+
authoredBaseRevision?: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* For a writer that knows its result only once the edit runs: without
|
|
17
|
+
* these, persist takes the committed body's hash and the blocks it changed.
|
|
18
|
+
*/
|
|
19
|
+
candidateHash?: string;
|
|
20
|
+
metadataHash?: string;
|
|
21
|
+
changedBlockIndexes?: number[];
|
|
22
|
+
displacedCheckpointId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why a server write changed a page body. A live-copy commit's `persist`
|
|
27
|
+
* records it: a History transition for every commit, and the body intent once
|
|
28
|
+
* the commit's revision is known. Server writers pass it to
|
|
29
|
+
* `editContentLiveCopy`, which also records an unchanged edit's intent; core
|
|
30
|
+
* runs no binding persist for one. Give each edit its own cause; an intent
|
|
31
|
+
* recorded twice would claim two revisions.
|
|
32
|
+
*/
|
|
33
|
+
export interface ContentBodyWriteCause {
|
|
34
|
+
ctx?: ActionRunContext;
|
|
35
|
+
/** The History operation, such as `"accept-suggestion"`. */
|
|
36
|
+
operation: string;
|
|
37
|
+
origin?: string;
|
|
38
|
+
historySessionId?: string;
|
|
39
|
+
intent?: ContentBodyIntentRecord;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A page being switched to its live copy, from the body at `baseRevision`. */
|
|
43
|
+
export interface LiveBodyConversion {
|
|
44
|
+
documentId: string;
|
|
45
|
+
baseRevision: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface WriteScope {
|
|
49
|
+
cause: ContentBodyWriteCause;
|
|
50
|
+
conversion?: LiveBodyConversion;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const scopes = new AsyncLocalStorage<WriteScope>();
|
|
54
|
+
|
|
55
|
+
export function withContentBodyWriteCause<T>(
|
|
56
|
+
cause: ContentBodyWriteCause,
|
|
57
|
+
run: () => Promise<T>,
|
|
58
|
+
): Promise<T> {
|
|
59
|
+
return scopes.run({ cause }, run);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function currentContentBodyWriteCause():
|
|
63
|
+
| ContentBodyWriteCause
|
|
64
|
+
| undefined {
|
|
65
|
+
return scopes.getStore()?.cause;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Runs a conversion. The page is not switched until its commit lands, so the
|
|
70
|
+
* binding treats `conversion.documentId` as a live copy only inside `run`.
|
|
71
|
+
*/
|
|
72
|
+
export function withLiveBodyConversion<T>(
|
|
73
|
+
conversion: LiveBodyConversion,
|
|
74
|
+
cause: ContentBodyWriteCause,
|
|
75
|
+
run: () => Promise<T>,
|
|
76
|
+
): Promise<T> {
|
|
77
|
+
return scopes.run({ cause, conversion }, run);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function liveBodyConversionFor(
|
|
81
|
+
documentId: string,
|
|
82
|
+
): LiveBodyConversion | undefined {
|
|
83
|
+
const conversion = scopes.getStore()?.conversion;
|
|
84
|
+
return conversion?.documentId === documentId ? conversion : undefined;
|
|
85
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mutateLiveCopy,
|
|
3
|
+
type LiveCopyCallChange,
|
|
4
|
+
type LiveCopyMutationResult,
|
|
5
|
+
} from "@agent-native/core/collab";
|
|
6
|
+
import type { DbExec } from "@agent-native/core/db";
|
|
7
|
+
|
|
8
|
+
import { CONTENT_LIVE_COPY_EDITS, contentLiveCopy } from "./binding.js";
|
|
9
|
+
import { persistUnchangedContentBodyEdit } from "./persist.js";
|
|
10
|
+
import {
|
|
11
|
+
withContentBodyWriteCause,
|
|
12
|
+
type ContentBodyWriteCause,
|
|
13
|
+
} from "./write-cause.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A server writer's replace names the body it was built from. Core retries
|
|
17
|
+
* an edit a peer's commit overtook, and a replace without `expected` would
|
|
18
|
+
* silently overwrite the peer's text on that retry.
|
|
19
|
+
*/
|
|
20
|
+
type ContentLiveCopyEditInput<Edit extends string> =
|
|
21
|
+
typeof CONTENT_LIVE_COPY_EDITS.replaceBody extends Edit
|
|
22
|
+
? { content: string; expected: string }
|
|
23
|
+
: unknown;
|
|
24
|
+
|
|
25
|
+
function replaceBodyInput(
|
|
26
|
+
documentId: string,
|
|
27
|
+
input: unknown,
|
|
28
|
+
): { content: unknown; expected: string } {
|
|
29
|
+
const { content, expected } = (input ?? {}) as {
|
|
30
|
+
content?: unknown;
|
|
31
|
+
expected?: unknown;
|
|
32
|
+
};
|
|
33
|
+
if (typeof expected !== "string") {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`A server writer's replace of page ${documentId} must name the body it was built from as \`expected\`.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
// The worker gets a structured clone, which drops a getter's or a
|
|
39
|
+
// non-enumerable property's value, so send the values checked here.
|
|
40
|
+
return { content, expected };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Applies a named edit to a switched page's live copy for a server writer.
|
|
45
|
+
* A changed edit commits through the binding's persist; an unchanged one
|
|
46
|
+
* still locks the page in Content's order and records the cause's intent,
|
|
47
|
+
* which core's binding persist never sees. `persist` runs after either, in
|
|
48
|
+
* the same transaction, with the page locked.
|
|
49
|
+
*
|
|
50
|
+
* Throws `LIVE_BODY_WRITE_PATH_CHANGED` when the page left its live copy
|
|
51
|
+
* after the writer chose it; nothing commits, so the writer can take its
|
|
52
|
+
* legacy path. An edit a peer's commit overtook is retried against the
|
|
53
|
+
* peer's body, so a replace whose `expected` body changed is refused
|
|
54
|
+
* `base_changed`.
|
|
55
|
+
*/
|
|
56
|
+
export async function editContentLiveCopy<Edit extends string>(
|
|
57
|
+
documentId: string,
|
|
58
|
+
options: {
|
|
59
|
+
cause: ContentBodyWriteCause;
|
|
60
|
+
edit: Edit;
|
|
61
|
+
input: ContentLiveCopyEditInput<Edit>;
|
|
62
|
+
requestSource?: string;
|
|
63
|
+
persist?: (tx: DbExec, change: LiveCopyCallChange) => Promise<void>;
|
|
64
|
+
},
|
|
65
|
+
): Promise<LiveCopyMutationResult> {
|
|
66
|
+
const input =
|
|
67
|
+
options.edit === CONTENT_LIVE_COPY_EDITS.replaceBody
|
|
68
|
+
? replaceBodyInput(documentId, options.input)
|
|
69
|
+
: options.input;
|
|
70
|
+
return withContentBodyWriteCause(options.cause, () =>
|
|
71
|
+
mutateLiveCopy(contentLiveCopy, documentId, {
|
|
72
|
+
edit: options.edit,
|
|
73
|
+
input,
|
|
74
|
+
...(options.requestSource
|
|
75
|
+
? { requestSource: options.requestSource }
|
|
76
|
+
: {}),
|
|
77
|
+
persist: async (tx, change) => {
|
|
78
|
+
if (!change.changed) {
|
|
79
|
+
await persistUnchangedContentBodyEdit(tx, documentId, options.cause);
|
|
80
|
+
}
|
|
81
|
+
await options.persist?.(tx, change);
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineNitroPlugin,
|
|
3
|
+
registerRecurringSweepHandler,
|
|
4
|
+
} from "@agent-native/core/server";
|
|
5
|
+
|
|
6
|
+
import { drainBuilderPrivatePayloadCleanup } from "../../actions/_builder-cms-blob-custody.js";
|
|
7
|
+
|
|
8
|
+
export const BUILDER_PRIVATE_PAYLOAD_CLEANUP_SWEEP_ID =
|
|
9
|
+
"content-builder-private-payload-cleanup";
|
|
10
|
+
|
|
11
|
+
export function registerBuilderPrivatePayloadCleanupSweep() {
|
|
12
|
+
return registerRecurringSweepHandler(
|
|
13
|
+
BUILDER_PRIVATE_PAYLOAD_CLEANUP_SWEEP_ID,
|
|
14
|
+
async ({ deadlineAt, signal }) => {
|
|
15
|
+
await drainBuilderPrivatePayloadCleanup({ deadlineAt, signal });
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default defineNitroPlugin(() => {
|
|
21
|
+
registerBuilderPrivatePayloadCleanupSweep();
|
|
22
|
+
});
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { createCollabPlugin } from "@agent-native/core/server";
|
|
2
2
|
|
|
3
|
+
import { contentLiveCopy } from "../live-copy/binding.js";
|
|
4
|
+
|
|
3
5
|
export default createCollabPlugin({
|
|
4
6
|
table: "documents",
|
|
5
7
|
contentColumn: "content",
|
|
6
8
|
idColumn: "id",
|
|
7
9
|
autoSeed: false, // Seeding happens via edit-document action, not on startup
|
|
8
10
|
access: { mode: "resource", resourceType: "document" },
|
|
11
|
+
liveCopy: contentLiveCopy,
|
|
9
12
|
});
|
|
@@ -1479,6 +1479,34 @@ export const runContentMigrations = runMigrations(
|
|
|
1479
1479
|
name: "content-live-body-switch",
|
|
1480
1480
|
sql: `ALTER TABLE documents ADD COLUMN IF NOT EXISTS live_body_switched_at TEXT`,
|
|
1481
1481
|
},
|
|
1482
|
+
{
|
|
1483
|
+
version: 125,
|
|
1484
|
+
name: "content-live-copy-version",
|
|
1485
|
+
sql: `ALTER TABLE documents ADD COLUMN IF NOT EXISTS live_copy_version INTEGER`,
|
|
1486
|
+
},
|
|
1487
|
+
{
|
|
1488
|
+
version: 126,
|
|
1489
|
+
name: "builder-private-payload-cleanup-queue",
|
|
1490
|
+
sql: `ALTER TABLE document_comments ADD COLUMN IF NOT EXISTS property_id TEXT;
|
|
1491
|
+
ALTER TABLE document_comments ADD COLUMN IF NOT EXISTS property_name TEXT;
|
|
1492
|
+
ALTER TABLE document_comments ADD COLUMN IF NOT EXISTS property_value TEXT;
|
|
1493
|
+
CREATE INDEX IF NOT EXISTS document_comments_document_idx
|
|
1494
|
+
ON document_comments (document_id, owner_email);
|
|
1495
|
+
CREATE TABLE IF NOT EXISTS content_database_builder_blob_cleanup (
|
|
1496
|
+
blob_reference TEXT PRIMARY KEY,
|
|
1497
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
1498
|
+
)`,
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
version: 127,
|
|
1502
|
+
name: "builder-private-payload-cleanup-retries",
|
|
1503
|
+
sql: `ALTER TABLE content_database_builder_blob_cleanup
|
|
1504
|
+
ADD COLUMN IF NOT EXISTS attempt_count INTEGER NOT NULL DEFAULT 0;
|
|
1505
|
+
ALTER TABLE content_database_builder_blob_cleanup
|
|
1506
|
+
ADD COLUMN IF NOT EXISTS next_attempt_at TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
|
|
1507
|
+
CREATE INDEX IF NOT EXISTS content_database_builder_blob_cleanup_due_idx
|
|
1508
|
+
ON content_database_builder_blob_cleanup (next_attempt_at, created_at)`,
|
|
1509
|
+
},
|
|
1482
1510
|
],
|
|
1483
1511
|
{ table: "content_migrations" },
|
|
1484
1512
|
);
|
|
@@ -161,9 +161,23 @@ export interface DocumentUpdateRequest {
|
|
|
161
161
|
loadedContentWasEmpty?: boolean;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* The body part of a browser save on a page whose body is its live copy. The
|
|
166
|
+
* body was not written: the editor's updates reach the live copy over collab.
|
|
167
|
+
* `matchesLiveCopy` says whether every submitted, authored and snapshot body
|
|
168
|
+
* equals the body derived from the live copy at `collabVersion`.
|
|
169
|
+
*/
|
|
170
|
+
export interface LiveCopyBodySave {
|
|
171
|
+
status: "live_copy";
|
|
172
|
+
matchesLiveCopy: boolean;
|
|
173
|
+
bodyRevision: number;
|
|
174
|
+
collabVersion: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
164
177
|
export interface DocumentUpdateResponse extends Document {
|
|
165
178
|
urlPath: string;
|
|
166
179
|
softDeletedDatabaseIds: string[];
|
|
180
|
+
bodySave?: LiveCopyBodySave;
|
|
167
181
|
}
|
|
168
182
|
|
|
169
183
|
export interface DocumentMoveRequest {
|
|
@@ -461,12 +475,19 @@ export type ContentDatabaseItemLeadingKind =
|
|
|
461
475
|
| "checkbox"
|
|
462
476
|
| "status";
|
|
463
477
|
export type ContentDatabaseItemTitleWeight = "regular" | "medium" | "bold";
|
|
478
|
+
/** What line 2 shows: a property, or the first lines of each row's page. */
|
|
479
|
+
export type ContentDatabaseItemLine2Source = "property" | "page";
|
|
464
480
|
|
|
465
481
|
/** Which property fills each part of a list row. */
|
|
466
482
|
export interface ContentDatabaseItemLayout {
|
|
467
483
|
leading: { kind: ContentDatabaseItemLeadingKind; propertyId: string | null };
|
|
468
484
|
titleWeight: ContentDatabaseItemTitleWeight;
|
|
469
|
-
|
|
485
|
+
/** A saved layout leaves `source` out when line 2 shows a property. */
|
|
486
|
+
line2: {
|
|
487
|
+
source?: ContentDatabaseItemLine2Source;
|
|
488
|
+
propertyId: string | null;
|
|
489
|
+
lines: 1 | 2 | 3;
|
|
490
|
+
};
|
|
470
491
|
line3: string[];
|
|
471
492
|
trailing: string[];
|
|
472
493
|
}
|
|
@@ -612,6 +633,11 @@ export interface ContentDatabaseItem {
|
|
|
612
633
|
properties: DocumentProperty[];
|
|
613
634
|
workspaceFilesDatabaseId?: string | null;
|
|
614
635
|
bodyHydration?: ContentDatabaseBodyHydration;
|
|
636
|
+
/**
|
|
637
|
+
* The first lines of the row's page as plain text. List reads include it
|
|
638
|
+
* only when a List view of the collection shows page text in line 2.
|
|
639
|
+
*/
|
|
640
|
+
excerpt?: string;
|
|
615
641
|
sourceRecord?: ContentDatabaseSourceRow;
|
|
616
642
|
canonicalKey?: string | null;
|
|
617
643
|
sourceOverlays?: ContentDatabaseSourceOverlay[];
|
|
@@ -891,6 +917,7 @@ export interface ContentDatabaseSourceExecution {
|
|
|
891
917
|
idempotencyKey: string;
|
|
892
918
|
summary: string;
|
|
893
919
|
payload: Record<string, unknown>;
|
|
920
|
+
payloadUnavailable?: true;
|
|
894
921
|
lastError: string | null;
|
|
895
922
|
createdAt: string;
|
|
896
923
|
updatedAt: string;
|
|
@@ -977,6 +1004,7 @@ export interface ContentDatabaseSource {
|
|
|
977
1004
|
readMode?: "fixture" | "builder-api" | (string & {}) | null;
|
|
978
1005
|
connectionId?: string | null;
|
|
979
1006
|
connectionLabel?: string | null;
|
|
1007
|
+
builderSpacePublicKey?: string | null;
|
|
980
1008
|
truthPolicy?: ContentDatabaseSourceTruthPolicy;
|
|
981
1009
|
syncPolicy?: "manual" | "keep_in_sync";
|
|
982
1010
|
liveBridgeEnabled?: boolean;
|
|
@@ -986,6 +1014,7 @@ export interface ContentDatabaseSource {
|
|
|
986
1014
|
lastReadMatchedRowCount?: number;
|
|
987
1015
|
lastReadLimit?: number;
|
|
988
1016
|
lastReadFetchedEntryCount?: number;
|
|
1017
|
+
lastReadCollectionCount?: number;
|
|
989
1018
|
lastReadPartial?: boolean;
|
|
990
1019
|
lastReadHasMore?: boolean;
|
|
991
1020
|
lastReadNextOffset?: number;
|