@agent-native/core 0.181.0 → 0.181.1-nightly-20260916144018
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/clips/app/components/player/create-account-dialog.tsx +2 -1
- package/corpus/templates/clips/changelog/2026-09-15-google-sign-up-now-opens-reliably-in-embedded-browsers.md +6 -0
- package/corpus/templates/content/actions/resolve-preview-document-draft.ts +320 -60
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +3 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +349 -104
- package/corpus/templates/content/app/components/editor/DocumentReconcileRecovery.tsx +109 -0
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +31 -153
- package/corpus/templates/content/app/components/editor/RecoveryComparison.tsx +230 -0
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +46 -19
- package/corpus/templates/content/app/components/editor/editor-draft-save.ts +13 -0
- package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +248 -0
- package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +2 -1
- package/corpus/templates/content/app/i18n-data.ts +13 -0
- package/corpus/templates/content/changelog/2026-09-15-oauth-connections-now-open-reliably-in-embedded-browsers.md +6 -0
- package/corpus/templates/content/docs/solutions/2026-09-02-content-suggested-edits-parity-shape.md +84 -2
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +27 -7
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +45 -15
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +35 -8
- package/corpus/templates/design/app/components/design/design-canvas/element-payload.ts +14 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +18 -5
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +9 -4
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +3 -0
- package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +7 -2
- package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +2 -0
- package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +4 -0
- package/corpus/templates/design/app/pages/design-editor/pending-structure-verification.ts +197 -147
- package/corpus/templates/design/changelog/2026-09-16-design-editor-agent-chat-uses-shared-conversation-tabs-and-s.md +6 -0
- package/corpus/templates/design/changelog/2026-09-16-visual-edits-stay-applied-after-replacements.md +5 -0
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -1
- package/corpus/templates/mail/changelog/2026-09-15-google-connections-now-open-reliably-in-embedded-browsers.md +6 -0
- package/dist/client/AgentPanel.d.ts +1 -1
- package/dist/client/AgentPanel.js +4 -4
- package/dist/client/auth/AuthPage.js +6 -1
- package/dist/client/oauth-popup.d.ts +7 -0
- package/dist/client/oauth-popup.js +14 -0
- package/dist/client/ocean/OceanBackground.d.ts +3 -1
- package/dist/client/ocean/OceanBackground.js +4 -3
- package/dist/client/resources/mcp-integration-catalog.js +7 -3
- package/dist/client/settings/useBuilderStatus.js +4 -3
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/app-roles.d.ts +8 -0
- package/dist/org/app-roles.js +7 -0
- package/dist/org/index.d.ts +1 -1
- package/dist/org/index.js +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +3 -0
- package/dist/server/core-routes-plugin.d.ts +3 -0
- package/dist/server/core-routes-plugin.js +17 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +5 -4
|
@@ -96,6 +96,7 @@ import {
|
|
|
96
96
|
useDocument,
|
|
97
97
|
useDeleteDocument,
|
|
98
98
|
useDocuments,
|
|
99
|
+
useResolvePreviewDocumentDraft,
|
|
99
100
|
useUpdatePreviewDocumentDraft,
|
|
100
101
|
useUpdateDocument,
|
|
101
102
|
} from "@/hooks/use-documents";
|
|
@@ -163,7 +164,9 @@ import { DocumentDatabase } from "./DocumentDatabase";
|
|
|
163
164
|
import { DocumentEditorSkeleton } from "./DocumentEditorSkeleton";
|
|
164
165
|
import { DocumentInfoPanel } from "./DocumentInfoPanel";
|
|
165
166
|
import { DocumentProperties } from "./DocumentProperties";
|
|
167
|
+
import { DocumentReconcileRecovery } from "./DocumentReconcileRecovery";
|
|
166
168
|
import { DocumentToolbar, type ToolbarBreadcrumbItem } from "./DocumentToolbar";
|
|
169
|
+
import type { EditorDraftSaveResult } from "./editor-draft-save";
|
|
167
170
|
import { EmojiPicker } from "./EmojiPicker";
|
|
168
171
|
import { LinkedLocalDocumentAgentBridge } from "./LinkedLocalDocumentAgentBridge";
|
|
169
172
|
import {
|
|
@@ -200,6 +203,12 @@ import {
|
|
|
200
203
|
normalizeTitleText,
|
|
201
204
|
stripMarkdownHeadingPrefixFromTitlePaste,
|
|
202
205
|
} from "./title-text";
|
|
206
|
+
import {
|
|
207
|
+
useDocumentReconcileRecovery,
|
|
208
|
+
type DocumentReconcileRecoveryState,
|
|
209
|
+
type ReconcileRecoveryDraft,
|
|
210
|
+
type ReconcileSaveBase,
|
|
211
|
+
} from "./useDocumentReconcileRecovery";
|
|
203
212
|
import { VisualEditor } from "./VisualEditor";
|
|
204
213
|
import type {
|
|
205
214
|
NotionPageLink,
|
|
@@ -1043,8 +1052,8 @@ type DocumentSaveOptions = {
|
|
|
1043
1052
|
historySessionId?: string;
|
|
1044
1053
|
allowQueuedSave?: boolean;
|
|
1045
1054
|
expectedLocalSourceRevision?: string | null;
|
|
1046
|
-
adoptCurrentServerBase?: boolean;
|
|
1047
1055
|
contentBase?: DocumentContentBase;
|
|
1056
|
+
titleBase?: string;
|
|
1048
1057
|
contentEditVersion?: number;
|
|
1049
1058
|
};
|
|
1050
1059
|
|
|
@@ -1492,6 +1501,7 @@ function PageEditorSessionBody({
|
|
|
1492
1501
|
});
|
|
1493
1502
|
}, [documentId, host, t]);
|
|
1494
1503
|
const updateDocument = useUpdateDocument();
|
|
1504
|
+
const resolvePreviewDocumentDraft = useResolvePreviewDocumentDraft();
|
|
1495
1505
|
const updatePreviewDocumentDraft = useUpdatePreviewDocumentDraft();
|
|
1496
1506
|
const updatePreviewDocumentDraftRef = useRef(
|
|
1497
1507
|
updatePreviewDocumentDraft.mutateAsync,
|
|
@@ -1701,9 +1711,20 @@ function PageEditorSessionBody({
|
|
|
1701
1711
|
diskRevision?: DesktopContentFileRevision;
|
|
1702
1712
|
unsavedText: string;
|
|
1703
1713
|
} | null>(null);
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1706
|
-
|
|
1714
|
+
const reconcileRecoveryStateRef =
|
|
1715
|
+
useRef<DocumentReconcileRecoveryState | null>(null);
|
|
1716
|
+
const reconcileSaveRef = useRef<
|
|
1717
|
+
(
|
|
1718
|
+
draft: ReconcileRecoveryDraft,
|
|
1719
|
+
base?: ReconcileSaveBase,
|
|
1720
|
+
) => Promise<boolean>
|
|
1721
|
+
>(async () => false);
|
|
1722
|
+
const reconcileRetainRef = useRef<
|
|
1723
|
+
(draft: ReconcileRecoveryDraft) => Promise<void>
|
|
1724
|
+
>(async () => undefined);
|
|
1725
|
+
const reportReconcileRef = useRef<
|
|
1726
|
+
(reason: "conflict" | "failed", localDraft: string) => void
|
|
1727
|
+
>(() => undefined);
|
|
1707
1728
|
const [localSourceMissing, setLocalSourceMissing] = useState(false);
|
|
1708
1729
|
const [localSourceAccess, setLocalSourceAccess] = useState<
|
|
1709
1730
|
"checking" | "available" | "unavailable"
|
|
@@ -1821,6 +1842,36 @@ function PageEditorSessionBody({
|
|
|
1821
1842
|
const localContentRef = useRef(localContent);
|
|
1822
1843
|
const contentEditVersionRef = useRef(0);
|
|
1823
1844
|
localContentRef.current = localContent;
|
|
1845
|
+
const reconcileRecovery = useDocumentReconcileRecovery({
|
|
1846
|
+
save: (draft, base) => reconcileSaveRef.current(draft, base),
|
|
1847
|
+
retain: (draft) => reconcileRetainRef.current(draft),
|
|
1848
|
+
getDraft: () => localContentRef.current,
|
|
1849
|
+
getTitle: () => localTitleRef.current,
|
|
1850
|
+
getSaveIdentity: () =>
|
|
1851
|
+
JSON.stringify([localTitleRef.current, localContentRef.current]),
|
|
1852
|
+
stateRef: reconcileRecoveryStateRef,
|
|
1853
|
+
});
|
|
1854
|
+
const documentReconcileConflict = reconcileRecovery.state;
|
|
1855
|
+
const {
|
|
1856
|
+
report: reportReconcile,
|
|
1857
|
+
resolve: resolveReconcile,
|
|
1858
|
+
resolveChoice: resolveReconcileChoice,
|
|
1859
|
+
updateDraft: updateReconcileDraft,
|
|
1860
|
+
reportRetentionFailure,
|
|
1861
|
+
} = reconcileRecovery;
|
|
1862
|
+
const retainActiveRecoveryDraft = useCallback(
|
|
1863
|
+
(draft: ReconcileRecoveryDraft) => {
|
|
1864
|
+
void reconcileRetainRef.current(draft).catch(reportRetentionFailure);
|
|
1865
|
+
},
|
|
1866
|
+
[reportRetentionFailure],
|
|
1867
|
+
);
|
|
1868
|
+
const [acknowledgedLocalSnapshot, setAcknowledgedLocalSnapshot] = useState<{
|
|
1869
|
+
value: string;
|
|
1870
|
+
revision: string;
|
|
1871
|
+
updatedAt: string;
|
|
1872
|
+
sequence: number;
|
|
1873
|
+
} | null>(null);
|
|
1874
|
+
const acknowledgedLocalSnapshotSequenceRef = useRef(0);
|
|
1824
1875
|
const getLinkedLocalEditorSnapshot = useCallback(
|
|
1825
1876
|
() => ({
|
|
1826
1877
|
title: localTitleRef.current,
|
|
@@ -1835,6 +1886,8 @@ function PageEditorSessionBody({
|
|
|
1835
1886
|
documentUpdatedAtRef.current = document.updatedAt ?? null;
|
|
1836
1887
|
const documentContentRef = useRef(document.content);
|
|
1837
1888
|
documentContentRef.current = document.content;
|
|
1889
|
+
const documentTitleRef = useRef(document.title);
|
|
1890
|
+
documentTitleRef.current = document.title;
|
|
1838
1891
|
const documentRevisionRef = useRef(document.revision);
|
|
1839
1892
|
documentRevisionRef.current = document.revision;
|
|
1840
1893
|
const handleBackgroundSaveError = useCallback(
|
|
@@ -2114,6 +2167,7 @@ function PageEditorSessionBody({
|
|
|
2114
2167
|
useEffect(() => {
|
|
2115
2168
|
if (!document || !isInitializedRef.current) return;
|
|
2116
2169
|
if (isLinkedLocalSourceDocument) return;
|
|
2170
|
+
if (reconcileRecoveryStateRef.current) return;
|
|
2117
2171
|
const serverTitle = document.title;
|
|
2118
2172
|
const lastSaved = lastSavedTitleRef.current;
|
|
2119
2173
|
if (serverTitle === lastSaved.title) {
|
|
@@ -2405,6 +2459,23 @@ function PageEditorSessionBody({
|
|
|
2405
2459
|
persistenceErrorsRef.current.set(field, error);
|
|
2406
2460
|
}
|
|
2407
2461
|
} else {
|
|
2462
|
+
if (
|
|
2463
|
+
updates.content !== undefined &&
|
|
2464
|
+
result.revision &&
|
|
2465
|
+
result.updatedAt
|
|
2466
|
+
) {
|
|
2467
|
+
const snapshot = {
|
|
2468
|
+
value: result.content,
|
|
2469
|
+
revision: result.revision,
|
|
2470
|
+
updatedAt: result.updatedAt,
|
|
2471
|
+
sequence: ++acknowledgedLocalSnapshotSequenceRef.current,
|
|
2472
|
+
};
|
|
2473
|
+
setAcknowledgedLocalSnapshot((current) =>
|
|
2474
|
+
!current || snapshot.sequence > current.sequence
|
|
2475
|
+
? snapshot
|
|
2476
|
+
: current,
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2408
2479
|
if (
|
|
2409
2480
|
result.updatedAt &&
|
|
2410
2481
|
(!documentUpdatedAtRef.current ||
|
|
@@ -2591,13 +2662,6 @@ function PageEditorSessionBody({
|
|
|
2591
2662
|
): Promise<DocumentSaveResult> => {
|
|
2592
2663
|
const contentEditVersion =
|
|
2593
2664
|
options.contentEditVersion ?? contentEditVersionRef.current;
|
|
2594
|
-
if (options.adoptCurrentServerBase) {
|
|
2595
|
-
lastSavedContentRef.current = {
|
|
2596
|
-
content: documentContentRef.current,
|
|
2597
|
-
updatedAt: documentUpdatedAtRef.current,
|
|
2598
|
-
revision: document.revision,
|
|
2599
|
-
};
|
|
2600
|
-
}
|
|
2601
2665
|
lastSavedContentRef.current = refreshUnchangedContentSaveWatermark({
|
|
2602
2666
|
serverContent: documentContentRef.current,
|
|
2603
2667
|
serverUpdatedAt: documentUpdatedAtRef.current,
|
|
@@ -2608,19 +2672,35 @@ function PageEditorSessionBody({
|
|
|
2608
2672
|
// stale — content. Guard per-field using the field's own watermark.
|
|
2609
2673
|
const titleIsStale =
|
|
2610
2674
|
!isLinkedLocalSourceDocument &&
|
|
2675
|
+
options.titleBase === undefined &&
|
|
2611
2676
|
documentUpdatedAtRef.current &&
|
|
2612
2677
|
lastSavedTitleRef.current.updatedAt &&
|
|
2613
2678
|
documentUpdatedAtRef.current > lastSavedTitleRef.current.updatedAt;
|
|
2679
|
+
const contentBase = options.contentBase ?? lastSavedContentRef.current;
|
|
2680
|
+
if (
|
|
2681
|
+
options.titleBase !== undefined &&
|
|
2682
|
+
documentTitleRef.current !== options.titleBase
|
|
2683
|
+
) {
|
|
2684
|
+
return { contentPersisted: false };
|
|
2685
|
+
}
|
|
2686
|
+
if (
|
|
2687
|
+
options.contentBase &&
|
|
2688
|
+
(documentContentRef.current !== options.contentBase.content ||
|
|
2689
|
+
documentUpdatedAtRef.current !== options.contentBase.updatedAt ||
|
|
2690
|
+
documentRevisionRef.current !== options.contentBase.revision)
|
|
2691
|
+
) {
|
|
2692
|
+
return { contentPersisted: false };
|
|
2693
|
+
}
|
|
2614
2694
|
const contentIsStale =
|
|
2615
2695
|
!isLinkedLocalSourceDocument &&
|
|
2616
2696
|
!!documentRevisionRef.current &&
|
|
2617
|
-
!!
|
|
2618
|
-
documentRevisionRef.current !==
|
|
2697
|
+
!!contentBase.revision &&
|
|
2698
|
+
documentRevisionRef.current !== contentBase.revision;
|
|
2619
2699
|
|
|
2620
2700
|
const updates: Record<string, string> = {};
|
|
2621
2701
|
if (title !== lastSavedTitleRef.current.title && !titleIsStale)
|
|
2622
2702
|
updates.title = title;
|
|
2623
|
-
const contentChanged = content !==
|
|
2703
|
+
const contentChanged = content !== contentBase.content;
|
|
2624
2704
|
if (contentChanged && !contentIsStale) updates.content = content;
|
|
2625
2705
|
if (Object.keys(updates).length === 0) {
|
|
2626
2706
|
return { contentPersisted: !contentChanged };
|
|
@@ -2637,7 +2717,7 @@ function PageEditorSessionBody({
|
|
|
2637
2717
|
let result;
|
|
2638
2718
|
try {
|
|
2639
2719
|
result = await saveDocumentWithRebase({
|
|
2640
|
-
base: { ...
|
|
2720
|
+
base: { ...contentBase },
|
|
2641
2721
|
content,
|
|
2642
2722
|
owner: {
|
|
2643
2723
|
version: contentEditVersion,
|
|
@@ -2648,7 +2728,6 @@ function PageEditorSessionBody({
|
|
|
2648
2728
|
confirm: (confirmedContent) => {
|
|
2649
2729
|
localContentRef.current = confirmedContent;
|
|
2650
2730
|
setLocalContent(confirmedContent);
|
|
2651
|
-
setDocumentReconcileConflict(null);
|
|
2652
2731
|
},
|
|
2653
2732
|
},
|
|
2654
2733
|
canRetry: (winner) =>
|
|
@@ -2667,9 +2746,7 @@ function PageEditorSessionBody({
|
|
|
2667
2746
|
activeContentSavesRef.current -= 1;
|
|
2668
2747
|
}
|
|
2669
2748
|
if (result.status === "conflict") {
|
|
2670
|
-
|
|
2671
|
-
localDraft: result.localDraft,
|
|
2672
|
-
});
|
|
2749
|
+
reportReconcileRef.current("conflict", result.localDraft);
|
|
2673
2750
|
return { contentPersisted: false };
|
|
2674
2751
|
}
|
|
2675
2752
|
saved = result.document;
|
|
@@ -2785,6 +2862,21 @@ function PageEditorSessionBody({
|
|
|
2785
2862
|
},
|
|
2786
2863
|
[documentId, t],
|
|
2787
2864
|
);
|
|
2865
|
+
const recoveryDraftRetentionQueueRef = useRef<Promise<void>>(
|
|
2866
|
+
Promise.resolve(),
|
|
2867
|
+
);
|
|
2868
|
+
const queueRecoveryDraftRetention = useCallback(
|
|
2869
|
+
(title: string, content: string, deferredReason: "conflict" | null) => {
|
|
2870
|
+
const retain = () => retainRecoveryDraft(title, content, deferredReason);
|
|
2871
|
+
const queued = recoveryDraftRetentionQueueRef.current.then(
|
|
2872
|
+
retain,
|
|
2873
|
+
retain,
|
|
2874
|
+
);
|
|
2875
|
+
recoveryDraftRetentionQueueRef.current = queued.catch(() => undefined);
|
|
2876
|
+
return queued;
|
|
2877
|
+
},
|
|
2878
|
+
[retainRecoveryDraft],
|
|
2879
|
+
);
|
|
2788
2880
|
const clearRecoveryDraft = useCallback(
|
|
2789
2881
|
async (persistedTitle: string, persistedContent: string) => {
|
|
2790
2882
|
const current = recoveryDraftRef.current;
|
|
@@ -2846,14 +2938,11 @@ function PageEditorSessionBody({
|
|
|
2846
2938
|
)
|
|
2847
2939
|
.then((result) => {
|
|
2848
2940
|
if (!result.contentPersisted) {
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
? pending.content
|
|
2855
|
-
: localContentRef.current,
|
|
2856
|
-
},
|
|
2941
|
+
reportReconcileRef.current(
|
|
2942
|
+
"conflict",
|
|
2943
|
+
contentEditVersionRef.current === pending.contentEditVersion
|
|
2944
|
+
? pending.content
|
|
2945
|
+
: localContentRef.current,
|
|
2857
2946
|
);
|
|
2858
2947
|
}
|
|
2859
2948
|
})
|
|
@@ -2968,6 +3057,7 @@ function PageEditorSessionBody({
|
|
|
2968
3057
|
const debouncedSave = useCallback(
|
|
2969
3058
|
(title: string, content: string) => {
|
|
2970
3059
|
if (!canEditRef.current) return;
|
|
3060
|
+
if (reconcileRecoveryStateRef.current) return;
|
|
2971
3061
|
const expectedLocalSourceRevision = isLinkedLocalSourceDocument
|
|
2972
3062
|
? localSourceRevisionForQueuedEdit(
|
|
2973
3063
|
pendingDocumentSaveRef.current?.expectedLocalSourceRevision,
|
|
@@ -3310,6 +3400,13 @@ function PageEditorSessionBody({
|
|
|
3310
3400
|
return;
|
|
3311
3401
|
localTitleRef.current = newTitle;
|
|
3312
3402
|
setLocalTitle(newTitle);
|
|
3403
|
+
if (updateReconcileDraft(localContentRef.current, newTitle)) {
|
|
3404
|
+
retainActiveRecoveryDraft({
|
|
3405
|
+
localTitle: newTitle,
|
|
3406
|
+
localDraft: localContentRef.current,
|
|
3407
|
+
});
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3313
3410
|
patchDocumentCaches(queryClient, documentId, { title: newTitle });
|
|
3314
3411
|
// Renames must not leave a stale optimistic title for the next landing.
|
|
3315
3412
|
refreshLandingTitleHintCache(queryClient, documentId, newTitle);
|
|
@@ -3319,22 +3416,15 @@ function PageEditorSessionBody({
|
|
|
3319
3416
|
void rememberContentLandingDocument(documentId, newTitle).catch(() => {});
|
|
3320
3417
|
debouncedSave(newTitle, localContentRef.current);
|
|
3321
3418
|
},
|
|
3322
|
-
[
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
if (documentReconcileConflict) {
|
|
3332
|
-
setDocumentReconcileConflict({ localDraft: newContent });
|
|
3333
|
-
return;
|
|
3334
|
-
}
|
|
3335
|
-
debouncedSave(localTitleRef.current, newContent);
|
|
3336
|
-
},
|
|
3337
|
-
[debouncedSave, documentReconcileConflict, editorCanEdit],
|
|
3419
|
+
[
|
|
3420
|
+
debouncedSave,
|
|
3421
|
+
documentId,
|
|
3422
|
+
editorCanEdit,
|
|
3423
|
+
isSuggesting,
|
|
3424
|
+
queryClient,
|
|
3425
|
+
retainActiveRecoveryDraft,
|
|
3426
|
+
updateReconcileDraft,
|
|
3427
|
+
],
|
|
3338
3428
|
);
|
|
3339
3429
|
|
|
3340
3430
|
const savedSuggestions = useMemo(() => {
|
|
@@ -3753,53 +3843,223 @@ function PageEditorSessionBody({
|
|
|
3753
3843
|
]);
|
|
3754
3844
|
|
|
3755
3845
|
const handleContentSaveNow = useCallback(
|
|
3756
|
-
async (
|
|
3846
|
+
async (
|
|
3847
|
+
recovery: ReconcileRecoveryDraft,
|
|
3848
|
+
reconcileBase?: ReconcileSaveBase,
|
|
3849
|
+
) => {
|
|
3757
3850
|
if (!editorCanEdit) return false;
|
|
3758
3851
|
contentEditVersionRef.current += 1;
|
|
3759
|
-
const contentEditVersion = contentEditVersionRef.current;
|
|
3760
3852
|
if (saveTimeoutRef.current) {
|
|
3761
3853
|
clearTimeout(saveTimeoutRef.current);
|
|
3762
3854
|
saveTimeoutRef.current = null;
|
|
3763
3855
|
pendingDocumentSaveRef.current = null;
|
|
3764
3856
|
}
|
|
3765
|
-
|
|
3766
|
-
|
|
3857
|
+
localTitleRef.current = recovery.localTitle;
|
|
3858
|
+
localContentRef.current = recovery.localDraft;
|
|
3859
|
+
setLocalTitle(recovery.localTitle);
|
|
3860
|
+
setLocalContent(recovery.localDraft);
|
|
3767
3861
|
const result = await queueDocumentSave(
|
|
3768
|
-
|
|
3769
|
-
|
|
3862
|
+
recovery.localTitle,
|
|
3863
|
+
recovery.localDraft,
|
|
3770
3864
|
{
|
|
3771
|
-
|
|
3865
|
+
contentBase: reconcileBase
|
|
3866
|
+
? {
|
|
3867
|
+
content: reconcileBase.content,
|
|
3868
|
+
updatedAt: reconcileBase.updatedAt,
|
|
3869
|
+
revision: reconcileBase.revision,
|
|
3870
|
+
}
|
|
3871
|
+
: undefined,
|
|
3872
|
+
titleBase: reconcileBase?.title,
|
|
3772
3873
|
},
|
|
3773
3874
|
);
|
|
3774
|
-
if (!result.contentPersisted) {
|
|
3775
|
-
setDocumentReconcileConflict(
|
|
3776
|
-
(current) =>
|
|
3777
|
-
current ?? {
|
|
3778
|
-
localDraft:
|
|
3779
|
-
contentEditVersionRef.current === contentEditVersion
|
|
3780
|
-
? newContent
|
|
3781
|
-
: localContentRef.current,
|
|
3782
|
-
},
|
|
3783
|
-
);
|
|
3784
|
-
} else if (contentEditVersionRef.current === contentEditVersion) {
|
|
3785
|
-
setDocumentReconcileConflict(null);
|
|
3786
|
-
}
|
|
3787
3875
|
return result.contentPersisted;
|
|
3788
3876
|
},
|
|
3789
3877
|
[editorCanEdit, queueDocumentSave],
|
|
3790
3878
|
);
|
|
3879
|
+
reconcileSaveRef.current = handleContentSaveNow;
|
|
3880
|
+
reconcileRetainRef.current = ({ localTitle, localDraft }) =>
|
|
3881
|
+
queueRecoveryDraftRetention(
|
|
3882
|
+
localTitle,
|
|
3883
|
+
localDraft,
|
|
3884
|
+
reconcileRecoveryStateRef.current?.reason === "conflict"
|
|
3885
|
+
? "conflict"
|
|
3886
|
+
: null,
|
|
3887
|
+
);
|
|
3888
|
+
reportReconcileRef.current = reportReconcile;
|
|
3889
|
+
|
|
3890
|
+
const handleContentChange = useCallback(
|
|
3891
|
+
(newContent: string) => {
|
|
3892
|
+
if (!editorCanEdit) return;
|
|
3893
|
+
contentEditVersionRef.current += 1;
|
|
3894
|
+
localContentRef.current = newContent;
|
|
3895
|
+
setLocalContent(newContent);
|
|
3896
|
+
if (updateReconcileDraft(newContent)) {
|
|
3897
|
+
retainActiveRecoveryDraft({
|
|
3898
|
+
localTitle: localTitleRef.current,
|
|
3899
|
+
localDraft: newContent,
|
|
3900
|
+
});
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
debouncedSave(localTitleRef.current, newContent);
|
|
3904
|
+
},
|
|
3905
|
+
[
|
|
3906
|
+
debouncedSave,
|
|
3907
|
+
editorCanEdit,
|
|
3908
|
+
retainActiveRecoveryDraft,
|
|
3909
|
+
updateReconcileDraft,
|
|
3910
|
+
],
|
|
3911
|
+
);
|
|
3912
|
+
|
|
3913
|
+
const handleImmediateContentChange = useCallback(
|
|
3914
|
+
async (newContent: string): Promise<EditorDraftSaveResult> => {
|
|
3915
|
+
if (!editorCanEdit) return "failed";
|
|
3916
|
+
if (reconcileRecoveryStateRef.current) {
|
|
3917
|
+
handleContentChange(newContent);
|
|
3918
|
+
return "retained";
|
|
3919
|
+
}
|
|
3920
|
+
return (await handleContentSaveNow({
|
|
3921
|
+
localTitle: localTitleRef.current,
|
|
3922
|
+
localDraft: newContent,
|
|
3923
|
+
}))
|
|
3924
|
+
? "persisted"
|
|
3925
|
+
: "failed";
|
|
3926
|
+
},
|
|
3927
|
+
[editorCanEdit, handleContentChange, handleContentSaveNow],
|
|
3928
|
+
);
|
|
3791
3929
|
|
|
3792
3930
|
const handleBaseAwareReconcile = useCallback(
|
|
3793
|
-
(result: {
|
|
3931
|
+
(result: {
|
|
3932
|
+
status: "merged" | "conflict" | "failed";
|
|
3933
|
+
content: string;
|
|
3934
|
+
serverContent: string;
|
|
3935
|
+
serverRevision: string;
|
|
3936
|
+
}) => {
|
|
3937
|
+
if (saveTimeoutRef.current) {
|
|
3938
|
+
clearTimeout(saveTimeoutRef.current);
|
|
3939
|
+
saveTimeoutRef.current = null;
|
|
3940
|
+
pendingDocumentSaveRef.current = null;
|
|
3941
|
+
}
|
|
3942
|
+
localContentRef.current = result.content;
|
|
3943
|
+
setLocalContent(result.content);
|
|
3944
|
+
reportReconcile(
|
|
3945
|
+
result.status === "conflict" ? "conflict" : "failed",
|
|
3946
|
+
result.content,
|
|
3947
|
+
);
|
|
3948
|
+
retainActiveRecoveryDraft({
|
|
3949
|
+
localTitle: localTitleRef.current,
|
|
3950
|
+
localDraft: result.content,
|
|
3951
|
+
});
|
|
3794
3952
|
if (result.status === "merged") {
|
|
3795
|
-
|
|
3796
|
-
|
|
3953
|
+
if (documentContentRef.current === result.serverContent) {
|
|
3954
|
+
void resolveReconcile({
|
|
3955
|
+
title: document.title,
|
|
3956
|
+
content: result.serverContent,
|
|
3957
|
+
updatedAt: documentUpdatedAtRef.current,
|
|
3958
|
+
revision: result.serverRevision,
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3797
3961
|
}
|
|
3798
|
-
setDocumentReconcileConflict({ localDraft: result.content });
|
|
3799
3962
|
},
|
|
3800
|
-
[
|
|
3963
|
+
[reportReconcile, resolveReconcile, retainActiveRecoveryDraft],
|
|
3964
|
+
);
|
|
3965
|
+
|
|
3966
|
+
const handleResolveReconcile = useCallback(
|
|
3967
|
+
async (base: ReconcileSaveBase) => {
|
|
3968
|
+
const saved = await resolveReconcile(base);
|
|
3969
|
+
if (saved)
|
|
3970
|
+
requestAnimationFrame(() => {
|
|
3971
|
+
documentLayoutRef.current
|
|
3972
|
+
?.querySelector<HTMLElement>(".ProseMirror")
|
|
3973
|
+
?.focus({ preventScroll: true });
|
|
3974
|
+
});
|
|
3975
|
+
return saved;
|
|
3976
|
+
},
|
|
3977
|
+
[resolveReconcile],
|
|
3978
|
+
);
|
|
3979
|
+
|
|
3980
|
+
const resolveLiveRecoveryDraft = useCallback(
|
|
3981
|
+
async (
|
|
3982
|
+
choice: "use_saved" | "save_separately",
|
|
3983
|
+
base: ReconcileSaveBase,
|
|
3984
|
+
) => {
|
|
3985
|
+
let resolvedUrlPath: string | undefined;
|
|
3986
|
+
const resolved = await resolveReconcileChoice(
|
|
3987
|
+
base,
|
|
3988
|
+
async (recovery, reviewedBase) => {
|
|
3989
|
+
await retainRecoveryDraft(
|
|
3990
|
+
recovery.localTitle,
|
|
3991
|
+
recovery.localDraft,
|
|
3992
|
+
documentReconcileConflict?.reason === "conflict"
|
|
3993
|
+
? "conflict"
|
|
3994
|
+
: null,
|
|
3995
|
+
);
|
|
3996
|
+
const draft = recoveryDraftRef.current;
|
|
3997
|
+
if (!draft || !reviewedBase.updatedAt) return false;
|
|
3998
|
+
const result = await resolvePreviewDocumentDraft.mutateAsync({
|
|
3999
|
+
choice,
|
|
4000
|
+
documentId,
|
|
4001
|
+
expectedDraftVersion: draft.version,
|
|
4002
|
+
expectedDraftTitle: draft.title,
|
|
4003
|
+
expectedDraftContent: draft.content,
|
|
4004
|
+
expectedDocumentUpdatedAt: reviewedBase.updatedAt,
|
|
4005
|
+
});
|
|
4006
|
+
if (result.status === "document_conflict") {
|
|
4007
|
+
await queryClient.refetchQueries(documentQueryFilter(documentId));
|
|
4008
|
+
return false;
|
|
4009
|
+
}
|
|
4010
|
+
if (
|
|
4011
|
+
recoveryDraftRef.current?.version === draft.version &&
|
|
4012
|
+
recoveryDraftRef.current.title === draft.title &&
|
|
4013
|
+
recoveryDraftRef.current.content === draft.content
|
|
4014
|
+
) {
|
|
4015
|
+
recoveryDraftRef.current = null;
|
|
4016
|
+
}
|
|
4017
|
+
resolvedUrlPath = result.urlPath;
|
|
4018
|
+
return true;
|
|
4019
|
+
},
|
|
4020
|
+
);
|
|
4021
|
+
if (!resolved) return false;
|
|
4022
|
+
if (choice === "save_separately" && resolvedUrlPath) {
|
|
4023
|
+
toast.success(t("editor.previewDraftSavedSeparately"));
|
|
4024
|
+
void navigate(resolvedUrlPath);
|
|
4025
|
+
return true;
|
|
4026
|
+
}
|
|
4027
|
+
if (choice === "use_saved") {
|
|
4028
|
+
localTitleRef.current = base.title ?? document.title;
|
|
4029
|
+
localContentRef.current = base.content;
|
|
4030
|
+
setLocalTitle(base.title ?? document.title);
|
|
4031
|
+
setLocalContent(base.content);
|
|
4032
|
+
await queryClient.refetchQueries(documentQueryFilter(documentId));
|
|
4033
|
+
toast.success(t("editor.previewDraftSavedToHistory"));
|
|
4034
|
+
}
|
|
4035
|
+
return true;
|
|
4036
|
+
},
|
|
4037
|
+
[
|
|
4038
|
+
documentId,
|
|
4039
|
+
documentReconcileConflict?.reason,
|
|
4040
|
+
navigate,
|
|
4041
|
+
queryClient,
|
|
4042
|
+
resolveReconcileChoice,
|
|
4043
|
+
resolvePreviewDocumentDraft,
|
|
4044
|
+
retainRecoveryDraft,
|
|
4045
|
+
t,
|
|
4046
|
+
],
|
|
3801
4047
|
);
|
|
3802
4048
|
|
|
4049
|
+
const copyLiveRecoveryDraft = useCallback(async () => {
|
|
4050
|
+
const copied = await writeClipboardText(localContentRef.current);
|
|
4051
|
+
if (copied) toast.success(t("editor.unsavedTextCopied"));
|
|
4052
|
+
else toast.error(t("editor.toolbar.clipboardAccessUnavailable"));
|
|
4053
|
+
}, [t]);
|
|
4054
|
+
|
|
4055
|
+
const focusEditorAfterRecoveryReview = useCallback(() => {
|
|
4056
|
+
requestAnimationFrame(() => {
|
|
4057
|
+
documentLayoutRef.current
|
|
4058
|
+
?.querySelector<HTMLElement>(".ProseMirror")
|
|
4059
|
+
?.focus({ preventScroll: true });
|
|
4060
|
+
});
|
|
4061
|
+
}, []);
|
|
4062
|
+
|
|
3803
4063
|
const useDiskVersion = useCallback(() => {
|
|
3804
4064
|
if (!localSourceConflict) return;
|
|
3805
4065
|
const next = localSourceConflict.diskDocument;
|
|
@@ -4868,41 +5128,23 @@ function PageEditorSessionBody({
|
|
|
4868
5128
|
) : null}
|
|
4869
5129
|
|
|
4870
5130
|
{documentReconcileConflict ? (
|
|
4871
|
-
<
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
t("editor.toolbar.clipboardAccessUnavailable"),
|
|
4889
|
-
);
|
|
4890
|
-
});
|
|
4891
|
-
}}
|
|
4892
|
-
>
|
|
4893
|
-
{t("editor.copyUnsavedText")}
|
|
4894
|
-
</Button>
|
|
4895
|
-
<Button
|
|
4896
|
-
type="button"
|
|
4897
|
-
size="sm"
|
|
4898
|
-
onClick={() => {
|
|
4899
|
-
const localDraft = documentReconcileConflict.localDraft;
|
|
4900
|
-
void handleContentSaveNow(localDraft, true);
|
|
4901
|
-
}}
|
|
4902
|
-
>
|
|
4903
|
-
{t("editor.keepLocalDraft")}
|
|
4904
|
-
</Button>
|
|
4905
|
-
</div>
|
|
5131
|
+
<DocumentReconcileRecovery
|
|
5132
|
+
state={documentReconcileConflict}
|
|
5133
|
+
server={{
|
|
5134
|
+
title: document.title,
|
|
5135
|
+
content: document.content,
|
|
5136
|
+
updatedAt: document.updatedAt ?? null,
|
|
5137
|
+
revision: document.revision,
|
|
5138
|
+
}}
|
|
5139
|
+
canEdit={editorCanEdit}
|
|
5140
|
+
onKeepMine={handleResolveReconcile}
|
|
5141
|
+
onUseSaved={(base) => resolveLiveRecoveryDraft("use_saved", base)}
|
|
5142
|
+
onSaveSeparately={(base) =>
|
|
5143
|
+
resolveLiveRecoveryDraft("save_separately", base)
|
|
5144
|
+
}
|
|
5145
|
+
onCopy={copyLiveRecoveryDraft}
|
|
5146
|
+
onClose={focusEditorAfterRecoveryReview}
|
|
5147
|
+
/>
|
|
4906
5148
|
) : null}
|
|
4907
5149
|
|
|
4908
5150
|
{isSuggesting &&
|
|
@@ -5280,6 +5522,9 @@ function PageEditorSessionBody({
|
|
|
5280
5522
|
? null
|
|
5281
5523
|
: (document.revision ?? null)
|
|
5282
5524
|
}
|
|
5525
|
+
acknowledgedLocalSnapshot={
|
|
5526
|
+
acknowledgedLocalSnapshot
|
|
5527
|
+
}
|
|
5283
5528
|
onBaseAwareReconcile={handleBaseAwareReconcile}
|
|
5284
5529
|
collabContentRevision={
|
|
5285
5530
|
isLocalFileDocument || isSuggesting
|
|
@@ -5294,7 +5539,7 @@ function PageEditorSessionBody({
|
|
|
5294
5539
|
}
|
|
5295
5540
|
onSaveContent={
|
|
5296
5541
|
suggestionEditorIsolation.persistCanonical
|
|
5297
|
-
?
|
|
5542
|
+
? handleImmediateContentChange
|
|
5298
5543
|
: undefined
|
|
5299
5544
|
}
|
|
5300
5545
|
ydoc={
|