@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.
Files changed (58) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/components/player/create-account-dialog.tsx +2 -1
  3. package/corpus/templates/clips/changelog/2026-09-15-google-sign-up-now-opens-reliably-in-embedded-browsers.md +6 -0
  4. package/corpus/templates/content/actions/resolve-preview-document-draft.ts +320 -60
  5. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +3 -1
  6. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +349 -104
  7. package/corpus/templates/content/app/components/editor/DocumentReconcileRecovery.tsx +109 -0
  8. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +31 -153
  9. package/corpus/templates/content/app/components/editor/RecoveryComparison.tsx +230 -0
  10. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +46 -19
  11. package/corpus/templates/content/app/components/editor/editor-draft-save.ts +13 -0
  12. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +248 -0
  13. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +2 -1
  14. package/corpus/templates/content/app/i18n-data.ts +13 -0
  15. package/corpus/templates/content/changelog/2026-09-15-oauth-connections-now-open-reliably-in-embedded-browsers.md +6 -0
  16. package/corpus/templates/content/docs/solutions/2026-09-02-content-suggested-edits-parity-shape.md +84 -2
  17. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +27 -7
  18. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +45 -15
  19. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +35 -8
  20. package/corpus/templates/design/app/components/design/design-canvas/element-payload.ts +14 -0
  21. package/corpus/templates/design/app/pages/DesignEditor.tsx +18 -5
  22. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +9 -4
  23. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +3 -0
  24. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +7 -2
  25. package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +2 -0
  26. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +4 -0
  27. package/corpus/templates/design/app/pages/design-editor/pending-structure-verification.ts +197 -147
  28. package/corpus/templates/design/changelog/2026-09-16-design-editor-agent-chat-uses-shared-conversation-tabs-and-s.md +6 -0
  29. package/corpus/templates/design/changelog/2026-09-16-visual-edits-stay-applied-after-replacements.md +5 -0
  30. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -1
  31. package/corpus/templates/mail/changelog/2026-09-15-google-connections-now-open-reliably-in-embedded-browsers.md +6 -0
  32. package/dist/client/AgentPanel.d.ts +1 -1
  33. package/dist/client/AgentPanel.js +4 -4
  34. package/dist/client/auth/AuthPage.js +6 -1
  35. package/dist/client/oauth-popup.d.ts +7 -0
  36. package/dist/client/oauth-popup.js +14 -0
  37. package/dist/client/ocean/OceanBackground.d.ts +3 -1
  38. package/dist/client/ocean/OceanBackground.js +4 -3
  39. package/dist/client/resources/mcp-integration-catalog.js +7 -3
  40. package/dist/client/settings/useBuilderStatus.js +4 -3
  41. package/dist/collab/routes.d.ts +1 -1
  42. package/dist/collab/struct-routes.d.ts +1 -1
  43. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  44. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  45. package/dist/notifications/routes.d.ts +3 -3
  46. package/dist/observability/routes.d.ts +3 -3
  47. package/dist/org/app-roles.d.ts +8 -0
  48. package/dist/org/app-roles.js +7 -0
  49. package/dist/org/index.d.ts +1 -1
  50. package/dist/org/index.js +1 -1
  51. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  52. package/dist/secrets/routes.d.ts +6 -6
  53. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  54. package/dist/server/auth.js +3 -0
  55. package/dist/server/core-routes-plugin.d.ts +3 -0
  56. package/dist/server/core-routes-plugin.js +17 -0
  57. package/dist/server/realtime-token.d.ts +1 -1
  58. 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?: (markdown: string) => boolean | Promise<boolean>;
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 false;
3106
+ if (!guards) return "failed" as const;
3093
3107
  try {
3094
3108
  const serialized = serializeEditorDraftForPersistence(editorToPersist);
3095
- if (serialized === null) return options?.strict !== true;
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) return true;
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 true;
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 false;
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 true;
3137
+ if (!guards.registerEmitted(normalized)) return "unchanged" as const;
3120
3138
  setTimeout(() => onChangeRef.current(normalized), 0);
3121
- return true;
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 false;
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 persisted = await persistEditorContent(editorToPersist, {
3155
+ const saveResult = await persistEditorContent(editorToPersist, {
3138
3156
  immediate: true,
3139
3157
  userInitiated: true,
3140
3158
  });
3141
- if (!persisted) throw new Error(t("empty.genericError"));
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
- return await Promise.resolve(
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
- return await persistEditorContent(editor, {
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
- persistEditorContent(editor, { userInitiated: true })
4110
+ Promise.resolve(
4111
+ persistEditorContent(editor, { userInitiated: true }),
4112
+ ).then(isEditorDraftSaveAccepted)
4088
4113
  }
4089
4114
  onDraftPersisted={(markdown) =>
4090
- persistEditorContent(editor, {
4091
- markdown,
4092
- immediate: true,
4093
- userInitiated: true,
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 = window.open("about:blank", "_blank");
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.",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-15
4
+ ---
5
+
6
+ Google sign-in and Notion connections now open reliably in embedded browsers
@@ -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-r5
7
- governingArtifactRevision: content-suggested-edits-shape-r5
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 null;
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) return null;
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 || snapshot.html === lastRuntimeLayerSnapshotHtml) return;
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();