@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
|
@@ -89,6 +89,10 @@ import {
|
|
|
89
89
|
type CommentTextAnchor,
|
|
90
90
|
} from "./comment-anchors";
|
|
91
91
|
import { buildContentSelectionPayload } from "./content-selection";
|
|
92
|
+
import {
|
|
93
|
+
isEditorDraftSaveAccepted,
|
|
94
|
+
type EditorDraftSaveResult,
|
|
95
|
+
} from "./editor-draft-save";
|
|
92
96
|
import { AudioNode } from "./extensions/AudioNode";
|
|
93
97
|
import { CodeBlock } from "./extensions/CodeBlockNode";
|
|
94
98
|
import {
|
|
@@ -1339,6 +1343,13 @@ interface VisualEditorProps {
|
|
|
1339
1343
|
contentUpdatedAt?: string | null;
|
|
1340
1344
|
/** Opaque body revision used for base-aware external-edit reconciliation. */
|
|
1341
1345
|
contentRevision?: string | null;
|
|
1346
|
+
/** Latest server-confirmed body snapshot written by this editor. */
|
|
1347
|
+
acknowledgedLocalSnapshot?: {
|
|
1348
|
+
value: string;
|
|
1349
|
+
revision: string;
|
|
1350
|
+
updatedAt: string;
|
|
1351
|
+
sequence: number;
|
|
1352
|
+
} | null;
|
|
1342
1353
|
collabContentRevision?: string | null;
|
|
1343
1354
|
requestCollabSync?: () => Promise<{
|
|
1344
1355
|
status: "synced" | "failed" | "unavailable";
|
|
@@ -1351,7 +1362,9 @@ interface VisualEditorProps {
|
|
|
1351
1362
|
serverRevision: string;
|
|
1352
1363
|
}) => void;
|
|
1353
1364
|
onChange: (markdown: string) => void;
|
|
1354
|
-
onSaveContent?: (
|
|
1365
|
+
onSaveContent?: (
|
|
1366
|
+
markdown: string,
|
|
1367
|
+
) => EditorDraftSaveResult | Promise<EditorDraftSaveResult>;
|
|
1355
1368
|
onEscape?: () => void;
|
|
1356
1369
|
/** Yjs document for collaborative editing. */
|
|
1357
1370
|
ydoc?: YDoc | null;
|
|
@@ -2838,6 +2851,7 @@ export function VisualEditor({
|
|
|
2838
2851
|
content,
|
|
2839
2852
|
contentUpdatedAt,
|
|
2840
2853
|
contentRevision,
|
|
2854
|
+
acknowledgedLocalSnapshot,
|
|
2841
2855
|
collabContentRevision,
|
|
2842
2856
|
requestCollabSync,
|
|
2843
2857
|
onBaseAwareReconcile,
|
|
@@ -3089,12 +3103,16 @@ export function VisualEditor({
|
|
|
3089
3103
|
},
|
|
3090
3104
|
) => {
|
|
3091
3105
|
const guards = guardsRef.current;
|
|
3092
|
-
if (!guards) return
|
|
3106
|
+
if (!guards) return "failed" as const;
|
|
3093
3107
|
try {
|
|
3094
3108
|
const serialized = serializeEditorDraftForPersistence(editorToPersist);
|
|
3095
|
-
if (serialized === null)
|
|
3109
|
+
if (serialized === null)
|
|
3110
|
+
return options?.strict === true
|
|
3111
|
+
? ("failed" as const)
|
|
3112
|
+
: ("unchanged" as const);
|
|
3096
3113
|
const normalized = options?.markdown ?? serialized;
|
|
3097
|
-
if (localFileMode && normalized === content)
|
|
3114
|
+
if (localFileMode && normalized === content)
|
|
3115
|
+
return "unchanged" as const;
|
|
3098
3116
|
// TipTap/Yjs can emit a local-looking empty-paragraph transaction while
|
|
3099
3117
|
// an editor is mounting or reconciling. Content serializes that filler
|
|
3100
3118
|
// as `<empty-block/>`, so the generic whitespace-only collab guard does
|
|
@@ -3107,24 +3125,24 @@ export function VisualEditor({
|
|
|
3107
3125
|
userInitiated: options?.userInitiated === true,
|
|
3108
3126
|
})
|
|
3109
3127
|
) {
|
|
3110
|
-
return
|
|
3128
|
+
return "unchanged" as const;
|
|
3111
3129
|
}
|
|
3112
3130
|
if (options?.immediate && onSaveContentRef.current) {
|
|
3113
3131
|
return onSaveContentRef.current(normalized);
|
|
3114
3132
|
}
|
|
3115
|
-
if (options?.immediate) return
|
|
3133
|
+
if (options?.immediate) return "failed" as const;
|
|
3116
3134
|
// Don't persist an empty doc before Collaboration has seeded (would
|
|
3117
3135
|
// clobber DB content with an empty string). `registerEmitted` records
|
|
3118
3136
|
// this as the last-emitted value and returns false to skip the save.
|
|
3119
|
-
if (!guards.registerEmitted(normalized)) return
|
|
3137
|
+
if (!guards.registerEmitted(normalized)) return "unchanged" as const;
|
|
3120
3138
|
setTimeout(() => onChangeRef.current(normalized), 0);
|
|
3121
|
-
return
|
|
3139
|
+
return "scheduled" as const;
|
|
3122
3140
|
} catch (err: any) {
|
|
3123
3141
|
toast.error(
|
|
3124
3142
|
t("editor.markdownSerializationError", { message: err.message }),
|
|
3125
3143
|
);
|
|
3126
3144
|
console.error("Markdown serialization error:", err);
|
|
3127
|
-
return
|
|
3145
|
+
return "failed" as const;
|
|
3128
3146
|
}
|
|
3129
3147
|
},
|
|
3130
3148
|
[content, localFileMode, t],
|
|
@@ -3134,11 +3152,13 @@ export function VisualEditor({
|
|
|
3134
3152
|
const guards = guardsRef.current;
|
|
3135
3153
|
if (!guards || guards.shouldIgnoreUpdate(transaction)) return;
|
|
3136
3154
|
try {
|
|
3137
|
-
const
|
|
3155
|
+
const saveResult = await persistEditorContent(editorToPersist, {
|
|
3138
3156
|
immediate: true,
|
|
3139
3157
|
userInitiated: true,
|
|
3140
3158
|
});
|
|
3141
|
-
if (!
|
|
3159
|
+
if (!isEditorDraftSaveAccepted(saveResult)) {
|
|
3160
|
+
throw new Error(t("empty.genericError"));
|
|
3161
|
+
}
|
|
3142
3162
|
} catch (error) {
|
|
3143
3163
|
// The ordinary onUpdate path still queues its debounced retry. Keep the
|
|
3144
3164
|
// immediate durability attempt from becoming an unhandled rejection,
|
|
@@ -3417,13 +3437,14 @@ export function VisualEditor({
|
|
|
3417
3437
|
) {
|
|
3418
3438
|
return true;
|
|
3419
3439
|
}
|
|
3420
|
-
|
|
3440
|
+
const result = await Promise.resolve(
|
|
3421
3441
|
persistEditorContent(editor, {
|
|
3422
3442
|
immediate: true,
|
|
3423
3443
|
userInitiated: true,
|
|
3424
3444
|
strict: true,
|
|
3425
3445
|
}),
|
|
3426
3446
|
);
|
|
3447
|
+
return isEditorDraftSaveAccepted(result);
|
|
3427
3448
|
},
|
|
3428
3449
|
});
|
|
3429
3450
|
return () => onPersistenceControllerChange?.(null);
|
|
@@ -3536,10 +3557,11 @@ export function VisualEditor({
|
|
|
3536
3557
|
persistCommittedImage: async () => {
|
|
3537
3558
|
if (!committed || editor.isDestroyed || suggestingRef.current)
|
|
3538
3559
|
return false;
|
|
3539
|
-
|
|
3560
|
+
const result = await persistEditorContent(editor, {
|
|
3540
3561
|
immediate: true,
|
|
3541
3562
|
userInitiated: true,
|
|
3542
3563
|
});
|
|
3564
|
+
return isEditorDraftSaveAccepted(result);
|
|
3543
3565
|
},
|
|
3544
3566
|
});
|
|
3545
3567
|
if (!committed) throw new Error(t("empty.genericError"));
|
|
@@ -3626,6 +3648,7 @@ export function VisualEditor({
|
|
|
3626
3648
|
contentRevision: propsPredateAcknowledgedRestore
|
|
3627
3649
|
? acknowledgedRestore!.contentRevision
|
|
3628
3650
|
: contentRevision,
|
|
3651
|
+
acknowledgedLocalSnapshot,
|
|
3629
3652
|
collabContentRevision: propsPredateAcknowledgedRestore
|
|
3630
3653
|
? null
|
|
3631
3654
|
: collabContentRevision,
|
|
@@ -4084,14 +4107,18 @@ export function VisualEditor({
|
|
|
4084
4107
|
suggesting={suggesting}
|
|
4085
4108
|
notionPageId={notionPageId}
|
|
4086
4109
|
onDraftCommitted={() =>
|
|
4087
|
-
|
|
4110
|
+
Promise.resolve(
|
|
4111
|
+
persistEditorContent(editor, { userInitiated: true }),
|
|
4112
|
+
).then(isEditorDraftSaveAccepted)
|
|
4088
4113
|
}
|
|
4089
4114
|
onDraftPersisted={(markdown) =>
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4115
|
+
Promise.resolve(
|
|
4116
|
+
persistEditorContent(editor, {
|
|
4117
|
+
markdown,
|
|
4118
|
+
immediate: true,
|
|
4119
|
+
userInitiated: true,
|
|
4120
|
+
}),
|
|
4121
|
+
).then(isEditorDraftSaveAccepted)
|
|
4095
4122
|
}
|
|
4096
4123
|
/>
|
|
4097
4124
|
) : null}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type EditorDraftSaveResult =
|
|
2
|
+
| boolean
|
|
3
|
+
| "persisted"
|
|
4
|
+
| "retained"
|
|
5
|
+
| "scheduled"
|
|
6
|
+
| "unchanged"
|
|
7
|
+
| "failed";
|
|
8
|
+
|
|
9
|
+
export function isEditorDraftSaveAccepted(
|
|
10
|
+
result: EditorDraftSaveResult,
|
|
11
|
+
): boolean {
|
|
12
|
+
return result !== false && result !== "failed";
|
|
13
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type MutableRefObject,
|
|
7
|
+
} from "react";
|
|
8
|
+
|
|
9
|
+
export interface ReconcileSaveBase {
|
|
10
|
+
title: string;
|
|
11
|
+
content: string;
|
|
12
|
+
updatedAt: string | null;
|
|
13
|
+
revision?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DocumentReconcileRecoveryState {
|
|
17
|
+
reason: "conflict" | "failed" | "save-failed";
|
|
18
|
+
localDraft: string;
|
|
19
|
+
localTitle: string;
|
|
20
|
+
saving: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ReconcileRecoveryDraft {
|
|
24
|
+
localDraft: string;
|
|
25
|
+
localTitle: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useDocumentReconcileRecovery({
|
|
29
|
+
save,
|
|
30
|
+
retain,
|
|
31
|
+
getDraft,
|
|
32
|
+
getTitle,
|
|
33
|
+
getSaveIdentity = getDraft,
|
|
34
|
+
stateRef,
|
|
35
|
+
}: {
|
|
36
|
+
save: (
|
|
37
|
+
draft: ReconcileRecoveryDraft,
|
|
38
|
+
base?: ReconcileSaveBase,
|
|
39
|
+
) => Promise<boolean>;
|
|
40
|
+
retain?: (draft: ReconcileRecoveryDraft) => Promise<void>;
|
|
41
|
+
getDraft: () => string;
|
|
42
|
+
getTitle?: () => string;
|
|
43
|
+
getSaveIdentity?: () => string;
|
|
44
|
+
stateRef?: MutableRefObject<DocumentReconcileRecoveryState | null>;
|
|
45
|
+
}) {
|
|
46
|
+
const [state, setState] = useState<DocumentReconcileRecoveryState | null>(
|
|
47
|
+
null,
|
|
48
|
+
);
|
|
49
|
+
const internalStateRef = useRef(state);
|
|
50
|
+
const current = stateRef ?? internalStateRef;
|
|
51
|
+
const generation = useRef(0);
|
|
52
|
+
const inFlight = useRef(false);
|
|
53
|
+
const callbacks = useRef({
|
|
54
|
+
save,
|
|
55
|
+
retain,
|
|
56
|
+
getDraft,
|
|
57
|
+
getTitle,
|
|
58
|
+
getSaveIdentity,
|
|
59
|
+
});
|
|
60
|
+
callbacks.current = { save, retain, getDraft, getTitle, getSaveIdentity };
|
|
61
|
+
|
|
62
|
+
const latestDraft = useCallback(
|
|
63
|
+
(): ReconcileRecoveryDraft => ({
|
|
64
|
+
localDraft: callbacks.current.getDraft(),
|
|
65
|
+
localTitle: callbacks.current.getTitle?.() ?? "",
|
|
66
|
+
}),
|
|
67
|
+
[],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const retainLatest = useCallback(async () => {
|
|
71
|
+
if (callbacks.current.retain) await callbacks.current.retain(latestDraft());
|
|
72
|
+
}, [latestDraft]);
|
|
73
|
+
|
|
74
|
+
useEffect(
|
|
75
|
+
() => () => {
|
|
76
|
+
generation.current += 1;
|
|
77
|
+
current.current = null;
|
|
78
|
+
},
|
|
79
|
+
[],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const publish = useCallback(
|
|
83
|
+
(next: DocumentReconcileRecoveryState | null) => {
|
|
84
|
+
current.current = next;
|
|
85
|
+
setState(next);
|
|
86
|
+
},
|
|
87
|
+
[current],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const report = useCallback(
|
|
91
|
+
(reason: "conflict" | "failed", localDraft: string) => {
|
|
92
|
+
generation.current += 1;
|
|
93
|
+
publish({
|
|
94
|
+
reason,
|
|
95
|
+
localDraft,
|
|
96
|
+
localTitle: callbacks.current.getTitle?.() ?? "",
|
|
97
|
+
saving: inFlight.current,
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
[publish],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const updateDraft = useCallback(
|
|
104
|
+
(localDraft: string, localTitle = callbacks.current.getTitle?.() ?? "") => {
|
|
105
|
+
if (!current.current) return false;
|
|
106
|
+
publish({ ...current.current, localDraft, localTitle });
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
[current, publish],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const reportRetentionFailure = useCallback(() => {
|
|
113
|
+
const latest = current.current;
|
|
114
|
+
if (!latest) return;
|
|
115
|
+
publish({ ...latest, reason: "save-failed", saving: false });
|
|
116
|
+
}, [current, publish]);
|
|
117
|
+
|
|
118
|
+
const resolve = useCallback(
|
|
119
|
+
async (base: ReconcileSaveBase): Promise<boolean> => {
|
|
120
|
+
if (!current.current || inFlight.current) return false;
|
|
121
|
+
inFlight.current = true;
|
|
122
|
+
const started = generation.current;
|
|
123
|
+
publish({ ...current.current, saving: true });
|
|
124
|
+
let saveBase: ReconcileSaveBase | undefined = base;
|
|
125
|
+
try {
|
|
126
|
+
while (generation.current === started) {
|
|
127
|
+
const identity = callbacks.current.getSaveIdentity();
|
|
128
|
+
const draft = latestDraft();
|
|
129
|
+
const persisted = await callbacks.current.save(draft, saveBase);
|
|
130
|
+
if (generation.current !== started) {
|
|
131
|
+
await retainLatest();
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (!persisted) {
|
|
135
|
+
await retainLatest();
|
|
136
|
+
publish({
|
|
137
|
+
reason: "conflict",
|
|
138
|
+
localDraft: callbacks.current.getDraft(),
|
|
139
|
+
localTitle: callbacks.current.getTitle?.() ?? "",
|
|
140
|
+
saving: false,
|
|
141
|
+
});
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (callbacks.current.getSaveIdentity() === identity) {
|
|
145
|
+
publish(null);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
saveBase = undefined;
|
|
149
|
+
}
|
|
150
|
+
await retainLatest();
|
|
151
|
+
return false;
|
|
152
|
+
} catch {
|
|
153
|
+
if (generation.current === started) {
|
|
154
|
+
try {
|
|
155
|
+
await retainLatest();
|
|
156
|
+
} catch {
|
|
157
|
+
// The visible recovery state remains the final fallback.
|
|
158
|
+
}
|
|
159
|
+
publish({
|
|
160
|
+
reason: "save-failed",
|
|
161
|
+
localDraft: callbacks.current.getDraft(),
|
|
162
|
+
localTitle: callbacks.current.getTitle?.() ?? "",
|
|
163
|
+
saving: false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
} finally {
|
|
168
|
+
inFlight.current = false;
|
|
169
|
+
if (current.current?.saving)
|
|
170
|
+
publish({ ...current.current, saving: false });
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
[current, latestDraft, publish, retainLatest],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const resolveChoice = useCallback(
|
|
177
|
+
async (
|
|
178
|
+
base: ReconcileSaveBase,
|
|
179
|
+
action: (
|
|
180
|
+
draft: ReconcileRecoveryDraft,
|
|
181
|
+
base: ReconcileSaveBase,
|
|
182
|
+
) => Promise<boolean>,
|
|
183
|
+
): Promise<boolean> => {
|
|
184
|
+
const snapshot = current.current;
|
|
185
|
+
if (!snapshot || inFlight.current) return false;
|
|
186
|
+
inFlight.current = true;
|
|
187
|
+
const started = generation.current;
|
|
188
|
+
publish({ ...snapshot, saving: true });
|
|
189
|
+
try {
|
|
190
|
+
const resolved = await action(
|
|
191
|
+
{
|
|
192
|
+
localDraft: snapshot.localDraft,
|
|
193
|
+
localTitle: snapshot.localTitle,
|
|
194
|
+
},
|
|
195
|
+
base,
|
|
196
|
+
);
|
|
197
|
+
if (!resolved || generation.current !== started) {
|
|
198
|
+
await retainLatest();
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
const latest = current.current;
|
|
202
|
+
if (
|
|
203
|
+
!latest ||
|
|
204
|
+
latest.localDraft !== snapshot.localDraft ||
|
|
205
|
+
latest.localTitle !== snapshot.localTitle
|
|
206
|
+
) {
|
|
207
|
+
await retainLatest();
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
publish(null);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
if (generation.current === started) {
|
|
214
|
+
try {
|
|
215
|
+
await retainLatest();
|
|
216
|
+
} catch {
|
|
217
|
+
// The visible recovery state remains the final fallback.
|
|
218
|
+
}
|
|
219
|
+
const latest = current.current;
|
|
220
|
+
if (latest)
|
|
221
|
+
publish({ ...latest, reason: "save-failed", saving: false });
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
} finally {
|
|
225
|
+
inFlight.current = false;
|
|
226
|
+
if (current.current?.saving)
|
|
227
|
+
publish({ ...current.current, saving: false });
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
[current, publish, retainLatest],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const release = useCallback(() => {
|
|
234
|
+
generation.current += 1;
|
|
235
|
+
publish(null);
|
|
236
|
+
}, [publish]);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
state,
|
|
240
|
+
report,
|
|
241
|
+
updateDraft,
|
|
242
|
+
reportRetentionFailure,
|
|
243
|
+
resolve,
|
|
244
|
+
resolveChoice,
|
|
245
|
+
release,
|
|
246
|
+
current,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from "@agent-native/core/client/api-path";
|
|
5
5
|
import { writeClipboardText } from "@agent-native/core/client/clipboard";
|
|
6
6
|
import { useT } from "@agent-native/core/client/i18n";
|
|
7
|
+
import { openOAuthPopup } from "@agent-native/core/client/oauth-popup";
|
|
7
8
|
import {
|
|
8
9
|
IconExternalLink,
|
|
9
10
|
IconCheck,
|
|
@@ -143,7 +144,7 @@ export function NotionButton() {
|
|
|
143
144
|
toast.error(t("sidebar.notionOAuthNotConfigured"));
|
|
144
145
|
return;
|
|
145
146
|
}
|
|
146
|
-
const popup =
|
|
147
|
+
const popup = openOAuthPopup();
|
|
147
148
|
if (!popup) {
|
|
148
149
|
toast.error(t("sidebar.notionOAuthNotConfigured"));
|
|
149
150
|
return;
|
|
@@ -1154,6 +1154,16 @@ const enUS = {
|
|
|
1154
1154
|
registryBlockUnreadable: "Persisted block source is unreadable.",
|
|
1155
1155
|
blocksFieldRevisionConflict:
|
|
1156
1156
|
"This Blocks field changed elsewhere. Your edit wasn't saved; the latest version is now shown.",
|
|
1157
|
+
reconcileConflict:
|
|
1158
|
+
"This page changed elsewhere. Your edits aren’t saved yet.",
|
|
1159
|
+
reconcileFailed:
|
|
1160
|
+
"The latest changes couldn’t be combined. Your edits aren’t saved yet.",
|
|
1161
|
+
reconcileSaveFailed: "Your edits couldn’t be saved. Review and try again.",
|
|
1162
|
+
reconcileSaving: "Saving your edits…",
|
|
1163
|
+
reconcileReview: "Review changes",
|
|
1164
|
+
reconcileReviewStale:
|
|
1165
|
+
"The saved version changed again. Review the latest text before saving.",
|
|
1166
|
+
reconcileRefresh: "Review latest version",
|
|
1157
1167
|
couldNotReadLocalSourceFile: "Could not read local source file",
|
|
1158
1168
|
couldNotSaveLocalFile: "Could not save local file",
|
|
1159
1169
|
localFileChangedWithUnsavedEdits:
|
|
@@ -1174,6 +1184,9 @@ const enUS = {
|
|
|
1174
1184
|
previewDraftSavedToHistory: "Your edits were saved to Version History",
|
|
1175
1185
|
previewDraftSavedSeparately: "Your edits were saved as a separate page",
|
|
1176
1186
|
previewDraftOpenSavedPage: "Open page",
|
|
1187
|
+
previewDraftMoreOptions: "More options",
|
|
1188
|
+
previewDraftViewFullVersions: "View full versions",
|
|
1189
|
+
previewDraftShowChanges: "Show changes",
|
|
1177
1190
|
restorePreviewDraft: "Restore draft",
|
|
1178
1191
|
previewDraftConflict:
|
|
1179
1192
|
"This draft conflicts with a newer page version. It was not restored.",
|
package/corpus/templates/content/docs/solutions/2026-09-02-content-suggested-edits-parity-shape.md
CHANGED
|
@@ -3,14 +3,96 @@ title: "Content Suggested Edits parity shape"
|
|
|
3
3
|
date: 2026-09-02
|
|
4
4
|
status: shape-complete
|
|
5
5
|
authoritySchemaVersion: 3
|
|
6
|
-
ledgerRevision: content-suggested-edits-shape-
|
|
7
|
-
governingArtifactRevision: content-suggested-edits-shape-
|
|
6
|
+
ledgerRevision: content-suggested-edits-shape-r7
|
|
7
|
+
governingArtifactRevision: content-suggested-edits-shape-r7
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Content Suggested Edits parity
|
|
11
11
|
|
|
12
12
|
## September 12 beta repair plan
|
|
13
13
|
|
|
14
|
+
### September 14 reconciliation: follow-up recovery repair
|
|
15
|
+
|
|
16
|
+
PR #4911 merged on September 14. Its durable persisted-draft resolution,
|
|
17
|
+
body-revision and title guards, history retention, source restrictions and
|
|
18
|
+
suggested-edit behavior remain the base for one focused follow-up PR from current
|
|
19
|
+
`main`. The follow-up ports only the live-editor acknowledgement and recovery
|
|
20
|
+
lifecycle protections that remain absent, then gives live and page-load recovery
|
|
21
|
+
one shared two-choice presentation. It does not merge, deploy, diagnose the
|
|
22
|
+
reported production incidents, add automatic merging or selective acceptance,
|
|
23
|
+
or promote the related product records beyond their current status.
|
|
24
|
+
|
|
25
|
+
The two recovery entry points retain separate lifecycles. Live-editor recovery
|
|
26
|
+
allows continued typing and returns focus to the editor when review closes.
|
|
27
|
+
Persisted-draft recovery settles only the exact private draft version reviewed at
|
|
28
|
+
page load. Both initially show equally weighted **Keep my edits** and **Use saved
|
|
29
|
+
version** actions. **Save a separate copy** and **Copy my edits** live in an
|
|
30
|
+
accessible **More options** menu. Neither version is preselected or recommended.
|
|
31
|
+
|
|
32
|
+
The comparison shows changed passages with nearby context and collapses long
|
|
33
|
+
unchanged runs behind **View full versions**. Full versions use the ordinary
|
|
34
|
+
read-only renderer, with faithful source as the fallback. Wide editor panes align
|
|
35
|
+
the two versions; narrow panes stack based on available pane width. The actual
|
|
36
|
+
editor route must fit at desktop, 390px and 320px, including 200% text zoom,
|
|
37
|
+
without horizontal overflow or obscured actions.
|
|
38
|
+
|
|
39
|
+
Implementation sequence:
|
|
40
|
+
|
|
41
|
+
1. Wire explicit successful-save acknowledgements into live reconciliation so an
|
|
42
|
+
older confirmed save advances the base without replaying its bytes over newer
|
|
43
|
+
typing. An old acknowledgement cannot supersede a newer observed revision, and
|
|
44
|
+
an identical-byte external revision still reconciles normally.
|
|
45
|
+
2. Guard live recovery by generation and exact reviewed base, block duplicate
|
|
46
|
+
submission, distinguish overlap/reconcile/save failures, and repeat persistence
|
|
47
|
+
when title or body changes during an in-flight recovery. Optimistic cache state
|
|
48
|
+
is never a save acknowledgement.
|
|
49
|
+
3. Reuse the existing durable draft, history and resolution actions through small
|
|
50
|
+
lifecycle adapters. A stale completion cannot clear newer recovery; the
|
|
51
|
+
unchosen version remains recoverable; separate copy creates one discoverable
|
|
52
|
+
page without changing the original.
|
|
53
|
+
4. Prove the combined final revision with focused tests, independent concurrency
|
|
54
|
+
and retention review, and real-interface QA through both entry points using
|
|
55
|
+
private task-owned pages and real actions. Capture representative desktop,
|
|
56
|
+
narrow, expanded, menu, pending/error and refreshed-conflict states, then clean
|
|
57
|
+
up every fixture.
|
|
58
|
+
|
|
59
|
+
Additional acceptance (cumulative with C01–C08):
|
|
60
|
+
|
|
61
|
+
- **D01:** Initially exactly two visible, equally weighted resolution buttons;
|
|
62
|
+
More options exposes separate-copy and exact-copy actions with correct keyboard
|
|
63
|
+
and focus behavior.
|
|
64
|
+
- **D02:** The actual editor route fits desktop, narrow desktop panes, 390px and
|
|
65
|
+
320px at normal and 200% text zoom, with readable text, modest gutters, safe-area
|
|
66
|
+
actions and no page-level horizontal overflow.
|
|
67
|
+
- **D03:** Title-only, body-only, deletion, disjoint and long formatted changes
|
|
68
|
+
show faithful contextual differences; expansion reveals every passage through
|
|
69
|
+
the renderer or an explicit source fallback.
|
|
70
|
+
- **D04:** Both main choices survive reload and leave the unchosen version
|
|
71
|
+
access-scoped and recoverable. An unseen update refreshes comparison and
|
|
72
|
+
requires another choice.
|
|
73
|
+
- **D05:** Slow, offline, failed and repeated actions acknowledge promptly, retain
|
|
74
|
+
both versions, prevent duplicate writes and retry honestly. Copy reports success
|
|
75
|
+
or failure, and separate copy opens the one created page.
|
|
76
|
+
- **D06:** Human QA uses the production component and real actions in the actual
|
|
77
|
+
editor route. Replicas and historical screenshots are not acceptance evidence.
|
|
78
|
+
- **E01:** Delayed confirmed own saves advance the base without false conflict or
|
|
79
|
+
lost newer typing; old acknowledgements and identical-byte external revisions
|
|
80
|
+
preserve revision ordering.
|
|
81
|
+
- **E02:** Title-only and body edits during recovery saving survive completion,
|
|
82
|
+
any required follow-up save and reload. Navigation, unmount or a newer conflict
|
|
83
|
+
cannot be cleared by an older completion.
|
|
84
|
+
- **E03:** Both entry points share the two-choice comparison and secondary menu
|
|
85
|
+
while retaining their own exact lifecycle. Closing live review restores editing
|
|
86
|
+
focus without releasing recovery.
|
|
87
|
+
- **E04:** Overlap, reconcile failure and save failure remain distinct and
|
|
88
|
+
retryable; interface and action read-back confirm title, body and recoverability
|
|
89
|
+
for both entry paths.
|
|
90
|
+
|
|
91
|
+
Destination: one ready-for-review follow-up PR against current `main`. Do not
|
|
92
|
+
merge or deploy it. The product lane remains `contract_repair` for
|
|
93
|
+
`content.version.field-history` and `content.history.queryable`; neither record
|
|
94
|
+
becomes fully verified through this bounded repair.
|
|
95
|
+
|
|
14
96
|
### September 13 amendment: ordinary-save reliability and recovery choices
|
|
15
97
|
|
|
16
98
|
Destination: extend the existing PR #4911 on `codex/content-suggestion-beta-repairs`.
|
|
@@ -1840,8 +1840,8 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
1840
1840
|
}
|
|
1841
1841
|
return !(isOverlayElement(el) || el.closest("[data-agent-native-edit-overlay]"));
|
|
1842
1842
|
}
|
|
1843
|
-
function serializeRuntimeLayerSnapshot() {
|
|
1844
|
-
if (!document.body) return
|
|
1843
|
+
function serializeRuntimeLayerSnapshot(excludedRoot) {
|
|
1844
|
+
if (!document.body) return { ok: false, reason: "snapshot-unavailable" };
|
|
1845
1845
|
var snapshotComputedProperties = [
|
|
1846
1846
|
"box-sizing",
|
|
1847
1847
|
"display",
|
|
@@ -1931,7 +1931,7 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
1931
1931
|
for (var index = 0; index < sourceNodes.length && index < cloneNodes.length; index += 1) {
|
|
1932
1932
|
var sourceNode = sourceNodes[index];
|
|
1933
1933
|
var cloneNode = cloneNodes[index];
|
|
1934
|
-
if (!isRuntimeLayerVisualNode(sourceNode)) {
|
|
1934
|
+
if (excludedRoot?.contains(sourceNode) || !isRuntimeLayerVisualNode(sourceNode)) {
|
|
1935
1935
|
cloneNode.setAttribute("data-an-runtime-layer-remove", "true");
|
|
1936
1936
|
continue;
|
|
1937
1937
|
}
|
|
@@ -2026,8 +2026,10 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
2026
2026
|
inlineSnapshotComputedStyle(document.body, cloneBody);
|
|
2027
2027
|
cloneBody.setAttribute("data-an-runtime-layer-snapshot", "true");
|
|
2028
2028
|
var html = "<!doctype html><html>" + cloneBody.outerHTML + "</html>";
|
|
2029
|
-
if (html.length > 2e6)
|
|
2029
|
+
if (html.length > 2e6)
|
|
2030
|
+
return { ok: false, reason: "snapshot-too-large" };
|
|
2030
2031
|
return {
|
|
2032
|
+
ok: true,
|
|
2031
2033
|
html,
|
|
2032
2034
|
nodeCount,
|
|
2033
2035
|
documentId: runtimeDocumentId
|
|
@@ -2043,7 +2045,17 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
2043
2045
|
runtimeLayerSnapshotTimer = null;
|
|
2044
2046
|
runtimeLayerSnapshotMaxTimer = null;
|
|
2045
2047
|
var snapshot = serializeRuntimeLayerSnapshot();
|
|
2046
|
-
if (!snapshot
|
|
2048
|
+
if (!snapshot.ok) {
|
|
2049
|
+
window.parent.postMessage(
|
|
2050
|
+
{
|
|
2051
|
+
type: "agent-native:runtime-layer-snapshot-error",
|
|
2052
|
+
payload: snapshot
|
|
2053
|
+
},
|
|
2054
|
+
"*"
|
|
2055
|
+
);
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (snapshot.html === lastRuntimeLayerSnapshotHtml) return;
|
|
2047
2059
|
lastRuntimeLayerSnapshotHtml = snapshot.html;
|
|
2048
2060
|
window.parent.postMessage(
|
|
2049
2061
|
{
|
|
@@ -9739,7 +9751,7 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
9739
9751
|
correctAbsoluteMemberClientPosition(el, desiredDropPoint);
|
|
9740
9752
|
publishSourceDocumentProvenance(void 0, true);
|
|
9741
9753
|
}
|
|
9742
|
-
function postVisualStructureChange(el, target, origin, insertedHtml, replaced) {
|
|
9754
|
+
function postVisualStructureChange(el, target, origin, insertedHtml, replaced, replacementSnapshotHtml) {
|
|
9743
9755
|
if (!el || !target || !target.anchor) return;
|
|
9744
9756
|
dndLog("post:structure-change", {
|
|
9745
9757
|
el: getSelector(el),
|
|
@@ -9770,6 +9782,7 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
9770
9782
|
// element the source file has never contained.
|
|
9771
9783
|
insertedHtml: typeof insertedHtml === "string" ? insertedHtml : void 0,
|
|
9772
9784
|
replaced: replaced === true ? true : void 0,
|
|
9785
|
+
replacementSnapshotHtml,
|
|
9773
9786
|
sourceRect: rectInfoForElement(el),
|
|
9774
9787
|
anchorRect: rectInfoForElement(target.anchor),
|
|
9775
9788
|
payload: getElementInfo(el),
|
|
@@ -14241,6 +14254,12 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
14241
14254
|
}
|
|
14242
14255
|
var replaceNextSibling = insertAnchor.nextSibling;
|
|
14243
14256
|
replaceParent.insertBefore(parsedInsertEl, insertAnchor);
|
|
14257
|
+
var replacementSnapshot = serializeRuntimeLayerSnapshot(insertAnchor);
|
|
14258
|
+
if (!replacementSnapshot.ok) {
|
|
14259
|
+
parsedInsertEl.remove();
|
|
14260
|
+
rejectInsert("replacement-" + replacementSnapshot.reason);
|
|
14261
|
+
return;
|
|
14262
|
+
}
|
|
14244
14263
|
selectedEl = parsedInsertEl;
|
|
14245
14264
|
positionOverlay(selectionOverlay, selectedEl);
|
|
14246
14265
|
refreshOverlays();
|
|
@@ -14254,7 +14273,8 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
14254
14273
|
prevNextSibling: replaceNextSibling
|
|
14255
14274
|
},
|
|
14256
14275
|
parsedInsertEl.outerHTML,
|
|
14257
|
-
true
|
|
14276
|
+
true,
|
|
14277
|
+
replacementSnapshot.html
|
|
14258
14278
|
);
|
|
14259
14279
|
replaceParent.removeChild(insertAnchor);
|
|
14260
14280
|
refreshOverlays();
|