codex-overleaf-link 2.4.1 → 2.5.1

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 (397) hide show
  1. package/README.md +275 -305
  2. package/README.zh-CN.md +290 -320
  3. package/extension/bootstrap/background.js +99 -19
  4. package/extension/bootstrap/manifest.template.json +9 -4
  5. package/extension/bootstrap/popup.html +11 -16
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/update.js +25 -2
  8. package/extension/bootstrap/updateStatus.js +1 -0
  9. package/extension/runtime-manifest.json +3 -1
  10. package/extension/src/background.js +234 -56
  11. package/extension/src/backgroundBootstrapMigration.js +181 -0
  12. package/extension/src/backgroundSharedSessions.js +440 -0
  13. package/extension/src/backgroundUpdateCoordinator.js +541 -381
  14. package/extension/src/backgroundUpdateSafety.js +173 -0
  15. package/extension/src/content/activeTurnControl.js +16 -2
  16. package/extension/src/content/applyResultFormatters.js +46 -73
  17. package/extension/src/content/assetTransferBroker.js +244 -12
  18. package/extension/src/content/changeHistoryView.js +175 -0
  19. package/extension/src/content/composerAttachments.js +117 -67
  20. package/extension/src/content/composerPanel.js +41 -8
  21. package/extension/src/content/contentRuntime.js +1084 -1099
  22. package/extension/src/content/diagnosticsController.js +122 -97
  23. package/extension/src/content/diagnosticsExport.js +236 -0
  24. package/extension/src/content/diagnosticsPanel.js +78 -19
  25. package/extension/src/content/diffReviewPanel.js +8 -5
  26. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  27. package/extension/src/content/generated/content.bundle.js +14680 -3606
  28. package/extension/src/content/generated/content.bundle.meta.json +30 -3
  29. package/extension/src/content/globalPreferencesController.js +1 -1
  30. package/extension/src/content/legacyGlobalRegistry.js +25 -1
  31. package/extension/src/content/localSkillsPanel.js +141 -16
  32. package/extension/src/content/markdownDomRenderer.js +9 -4
  33. package/extension/src/content/markdownText.js +5 -6
  34. package/extension/src/content/modelPicker.js +8 -5
  35. package/extension/src/content/nativeCompatibilityController.js +45 -20
  36. package/extension/src/content/otWarmMirror.js +4 -5
  37. package/extension/src/content/otWarmMirrorController.js +17 -20
  38. package/extension/src/content/pageBridgeClient.js +33 -5
  39. package/extension/src/content/panelMaintenance.js +87 -132
  40. package/extension/src/content/panelRenderer.js +10 -0
  41. package/extension/src/content/pendingInputView.js +109 -76
  42. package/extension/src/content/projectProviderSelection.js +13 -4
  43. package/extension/src/content/projectSessionCleanup.js +109 -62
  44. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  45. package/extension/src/content/providerSettingsCoordinator.js +63 -14
  46. package/extension/src/content/providerSettingsDialog.js +724 -113
  47. package/extension/src/content/recentProjects.js +21 -6
  48. package/extension/src/content/runActivitySummary.js +665 -0
  49. package/extension/src/content/runCompletionMeta.js +344 -0
  50. package/extension/src/content/runController.js +40 -15
  51. package/extension/src/content/runFailureNotice.js +34 -12
  52. package/extension/src/content/runGuidanceController.js +76 -30
  53. package/extension/src/content/runGuidanceView.js +59 -10
  54. package/extension/src/content/runPresence.js +306 -0
  55. package/extension/src/content/runQueueScheduler.js +150 -43
  56. package/extension/src/content/runRecoveryActions.js +146 -0
  57. package/extension/src/content/runResultActions.js +11 -8
  58. package/extension/src/content/runReviewController.js +574 -0
  59. package/extension/src/content/runScrollLayout.js +21 -2
  60. package/extension/src/content/runTimelineView.js +315 -290
  61. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  62. package/extension/src/content/selectionContextView.js +300 -0
  63. package/extension/src/content/sensitivePreflight.js +140 -0
  64. package/extension/src/content/sessionForkController.js +80 -17
  65. package/extension/src/content/sessionManager.js +1 -1
  66. package/extension/src/content/sessionPersistence.js +6 -1
  67. package/extension/src/content/settingsPanel.js +84 -55
  68. package/extension/src/content/settingsWorkbench.js +379 -0
  69. package/extension/src/content/sharedSessionViewSync.js +109 -0
  70. package/extension/src/content/subagentActivityView.js +195 -0
  71. package/extension/src/content/trackedChangeCaptureController.js +7 -3
  72. package/extension/src/content/updateIdle.js +4 -1
  73. package/extension/src/content/updateNotice.js +179 -56
  74. package/extension/src/content/updateSafetyReceiver.js +58 -0
  75. package/extension/src/content/writebackController.js +308 -2
  76. package/extension/src/content/writebackOrchestrator.js +213 -199
  77. package/extension/src/content/writebackPlan.js +231 -0
  78. package/extension/src/content/writebackRecovery.js +122 -0
  79. package/extension/src/content/writingStyleSettings.js +600 -0
  80. package/extension/src/content/writtenChangesView.js +152 -0
  81. package/extension/src/page/binaryAssetUploader.js +223 -23
  82. package/extension/src/page/fileMover.js +256 -0
  83. package/extension/src/page/overleafProjectSnapshot.js +1 -0
  84. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  85. package/extension/src/page/referenceProjects.js +149 -0
  86. package/extension/src/page/reviewContext.js +113 -0
  87. package/extension/src/page/saveState.js +20 -6
  88. package/extension/src/page/snapshotRouter.js +124 -48
  89. package/extension/src/page/textCoordinates.js +102 -0
  90. package/extension/src/page/textFileCreator.js +442 -71
  91. package/extension/src/page/trackedChangeCapture.js +53 -57
  92. package/extension/src/page/trackedChangeOwnership.js +216 -0
  93. package/extension/src/page/trackedChangeReplay.js +184 -0
  94. package/extension/src/page/trackedChangesLifecycle.js +343 -339
  95. package/extension/src/page/treeOperations.js +33 -12
  96. package/extension/src/page/updateSafetyProbe.js +41 -0
  97. package/extension/src/page/writebackReceiptJournal.js +114 -0
  98. package/extension/src/page/writebackRouter.js +242 -201
  99. package/extension/src/pageBridge.js +73 -102
  100. package/extension/src/popup.js +261 -65
  101. package/extension/src/shared/agentTranscript.js +41 -4
  102. package/extension/src/shared/auditRecords.js +73 -0
  103. package/extension/src/shared/changeHistoryModel.js +94 -0
  104. package/extension/src/shared/compatibility.js +22 -6
  105. package/extension/src/shared/compileAdapter.js +101 -37
  106. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  107. package/extension/src/shared/directoryCleanupProof.js +43 -0
  108. package/extension/src/shared/failureReasons.js +329 -3
  109. package/extension/src/shared/globalPreferences.js +31 -14
  110. package/extension/src/shared/governanceRules.js +4 -37
  111. package/extension/src/shared/i18n.js +389 -25
  112. package/extension/src/shared/managedUpdateProjection.js +8 -5
  113. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  114. package/extension/src/shared/pageRpcContract.js +9 -2
  115. package/extension/src/shared/projectFiles.js +21 -44
  116. package/extension/src/shared/providerProfiles.js +114 -3
  117. package/extension/src/shared/runActivityModel.js +361 -0
  118. package/extension/src/shared/runExecutionSnapshot.js +8 -2
  119. package/extension/src/shared/runExecutionSnapshotCodec.js +16 -4
  120. package/extension/src/shared/runInputQueue.js +48 -5
  121. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +6 -3
  122. package/extension/src/shared/selectionContext.js +37 -0
  123. package/extension/src/shared/sessionState.js +166 -30
  124. package/extension/src/shared/settlementFacts.js +10 -2
  125. package/extension/src/shared/sharedSessionBridge.js +311 -0
  126. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  127. package/extension/src/shared/staleGuard.js +118 -13
  128. package/extension/src/shared/storageDb.js +77 -78
  129. package/extension/src/shared/storageMigration.js +74 -111
  130. package/extension/src/shared/storageRunActions.js +211 -22
  131. package/extension/src/shared/storageValuePruning.js +40 -0
  132. package/extension/src/shared/subagentActivityModel.js +174 -0
  133. package/extension/src/shared/undoOperations.js +216 -6
  134. package/extension/src/shared/updateConsent.js +42 -11
  135. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  136. package/extension/src/shared/writebackIntent.js +78 -0
  137. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  138. package/extension/src/shared/writebackSettlement.js +67 -48
  139. package/extension/src/shared/writingStyle.js +33 -0
  140. package/extension/styles/panel.css +1987 -385
  141. package/extension/styles/popup.css +110 -0
  142. package/native-host/src/anthropicBridgeRequest.js +6 -3
  143. package/native-host/src/anthropicBridgeResponse.js +6 -2
  144. package/native-host/src/anthropicMessagesBridge.js +24 -16
  145. package/native-host/src/bootstrapMigration.js +296 -0
  146. package/native-host/src/bootstrapMigrationContract.js +101 -0
  147. package/native-host/src/bootstrapUpdate.js +233 -0
  148. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  149. package/native-host/src/chatBridgeResponse.js +10 -0
  150. package/native-host/src/chatCompletionsBridge.js +21 -15
  151. package/native-host/src/codexConfigPaths.js +106 -0
  152. package/native-host/src/codexHome.js +66 -73
  153. package/native-host/src/codexHomeScope.js +124 -0
  154. package/native-host/src/codexInheritedAuth.js +90 -0
  155. package/native-host/src/codexLocalSkills.js +301 -0
  156. package/native-host/src/codexModelRecovery.js +107 -0
  157. package/native-host/src/codexPromptAssembly.js +33 -12
  158. package/native-host/src/codexProviderLaunch.js +3 -3
  159. package/native-host/src/codexProviderTest.js +24 -26
  160. package/native-host/src/codexRuntimeIdentity.js +1 -0
  161. package/native-host/src/codexSessionRunner.js +211 -318
  162. package/native-host/src/codexSessionTiming.js +21 -8
  163. package/native-host/src/codexSkillIsolation.js +170 -0
  164. package/native-host/src/codexThreadFork.js +19 -3
  165. package/native-host/src/index.js +19 -7
  166. package/native-host/src/managedInstall.js +115 -39
  167. package/native-host/src/managedInstallRecovery.js +140 -0
  168. package/native-host/src/managedRuntimeEvidence.js +57 -0
  169. package/native-host/src/mirrorWorkspace.js +92 -22
  170. package/native-host/src/nativeDoctor.js +15 -0
  171. package/native-host/src/nativeEnvironment.js +70 -0
  172. package/native-host/src/nativeHelperLifecycle.js +21 -0
  173. package/native-host/src/nativeHostRegistration.js +33 -0
  174. package/native-host/src/nativeQuotas.js +15 -8
  175. package/native-host/src/nativeRunLifecycle.js +71 -0
  176. package/native-host/src/overleafWorkspaceRules.js +16 -0
  177. package/native-host/src/processStopBarrier.js +115 -0
  178. package/native-host/src/projectPathPlatform.js +30 -0
  179. package/native-host/src/providerHttp.js +80 -0
  180. package/native-host/src/providerModelCatalog.js +4 -3
  181. package/native-host/src/providerProfile.js +17 -3
  182. package/native-host/src/providerReasoning.js +10 -11
  183. package/native-host/src/providerRuntime.js +1 -0
  184. package/native-host/src/providerStore.js +76 -14
  185. package/native-host/src/releaseTransport.js +14 -5
  186. package/native-host/src/responsesPassthroughBridge.js +266 -52
  187. package/native-host/src/runtimeInstaller.js +21 -1
  188. package/native-host/src/selectionScope.js +86 -0
  189. package/native-host/src/skills/parallel-subagents/SKILL.md +31 -3
  190. package/native-host/src/skills/writing-style-builder/SKILL.md +146 -0
  191. package/native-host/src/subagentAdmission.js +26 -0
  192. package/native-host/src/subagentBroker.js +176 -159
  193. package/native-host/src/subagentTelemetry.js +325 -0
  194. package/native-host/src/subagentWorkspace.js +103 -0
  195. package/native-host/src/subagentWorkspacePath.js +34 -0
  196. package/native-host/src/taskRunnerRuntime.js +130 -57
  197. package/native-host/src/tomlStatements.js +140 -0
  198. package/native-host/src/turnAttachments.js +113 -0
  199. package/native-host/src/updateManager.js +406 -38
  200. package/native-host/src/updateNetworkControl.js +75 -0
  201. package/native-host/src/updateTrust.js +21 -2
  202. package/native-host/src/vendor/pdfjs/LICENSE +177 -0
  203. package/native-host/src/vendor/pdfjs/SOURCE.txt +3 -0
  204. package/native-host/src/vendor/pdfjs/cmaps/78-EUC-H.bcmap +0 -0
  205. package/native-host/src/vendor/pdfjs/cmaps/78-EUC-V.bcmap +0 -0
  206. package/native-host/src/vendor/pdfjs/cmaps/78-H.bcmap +0 -0
  207. package/native-host/src/vendor/pdfjs/cmaps/78-RKSJ-H.bcmap +0 -0
  208. package/native-host/src/vendor/pdfjs/cmaps/78-RKSJ-V.bcmap +0 -0
  209. package/native-host/src/vendor/pdfjs/cmaps/78-V.bcmap +0 -0
  210. package/native-host/src/vendor/pdfjs/cmaps/78ms-RKSJ-H.bcmap +0 -0
  211. package/native-host/src/vendor/pdfjs/cmaps/78ms-RKSJ-V.bcmap +0 -0
  212. package/native-host/src/vendor/pdfjs/cmaps/83pv-RKSJ-H.bcmap +0 -0
  213. package/native-host/src/vendor/pdfjs/cmaps/90ms-RKSJ-H.bcmap +0 -0
  214. package/native-host/src/vendor/pdfjs/cmaps/90ms-RKSJ-V.bcmap +0 -0
  215. package/native-host/src/vendor/pdfjs/cmaps/90msp-RKSJ-H.bcmap +0 -0
  216. package/native-host/src/vendor/pdfjs/cmaps/90msp-RKSJ-V.bcmap +0 -0
  217. package/native-host/src/vendor/pdfjs/cmaps/90pv-RKSJ-H.bcmap +0 -0
  218. package/native-host/src/vendor/pdfjs/cmaps/90pv-RKSJ-V.bcmap +0 -0
  219. package/native-host/src/vendor/pdfjs/cmaps/Add-H.bcmap +0 -0
  220. package/native-host/src/vendor/pdfjs/cmaps/Add-RKSJ-H.bcmap +0 -0
  221. package/native-host/src/vendor/pdfjs/cmaps/Add-RKSJ-V.bcmap +0 -0
  222. package/native-host/src/vendor/pdfjs/cmaps/Add-V.bcmap +0 -0
  223. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-0.bcmap +0 -0
  224. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-1.bcmap +0 -0
  225. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-2.bcmap +0 -0
  226. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-3.bcmap +0 -0
  227. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-4.bcmap +0 -0
  228. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-5.bcmap +0 -0
  229. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-6.bcmap +0 -0
  230. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
  231. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-0.bcmap +0 -0
  232. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-1.bcmap +0 -0
  233. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-2.bcmap +0 -0
  234. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-3.bcmap +0 -0
  235. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-4.bcmap +0 -0
  236. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-5.bcmap +0 -0
  237. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
  238. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-0.bcmap +0 -0
  239. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-1.bcmap +0 -0
  240. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-2.bcmap +0 -0
  241. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-3.bcmap +0 -0
  242. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-4.bcmap +0 -0
  243. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-5.bcmap +0 -0
  244. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-6.bcmap +0 -0
  245. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
  246. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Korea1-0.bcmap +0 -0
  247. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Korea1-1.bcmap +0 -0
  248. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Korea1-2.bcmap +0 -0
  249. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
  250. package/native-host/src/vendor/pdfjs/cmaps/B5-H.bcmap +0 -0
  251. package/native-host/src/vendor/pdfjs/cmaps/B5-V.bcmap +0 -0
  252. package/native-host/src/vendor/pdfjs/cmaps/B5pc-H.bcmap +0 -0
  253. package/native-host/src/vendor/pdfjs/cmaps/B5pc-V.bcmap +0 -0
  254. package/native-host/src/vendor/pdfjs/cmaps/CNS-EUC-H.bcmap +0 -0
  255. package/native-host/src/vendor/pdfjs/cmaps/CNS-EUC-V.bcmap +0 -0
  256. package/native-host/src/vendor/pdfjs/cmaps/CNS1-H.bcmap +0 -0
  257. package/native-host/src/vendor/pdfjs/cmaps/CNS1-V.bcmap +0 -0
  258. package/native-host/src/vendor/pdfjs/cmaps/CNS2-H.bcmap +0 -0
  259. package/native-host/src/vendor/pdfjs/cmaps/CNS2-V.bcmap +3 -0
  260. package/native-host/src/vendor/pdfjs/cmaps/ETHK-B5-H.bcmap +0 -0
  261. package/native-host/src/vendor/pdfjs/cmaps/ETHK-B5-V.bcmap +0 -0
  262. package/native-host/src/vendor/pdfjs/cmaps/ETen-B5-H.bcmap +0 -0
  263. package/native-host/src/vendor/pdfjs/cmaps/ETen-B5-V.bcmap +0 -0
  264. package/native-host/src/vendor/pdfjs/cmaps/ETenms-B5-H.bcmap +3 -0
  265. package/native-host/src/vendor/pdfjs/cmaps/ETenms-B5-V.bcmap +0 -0
  266. package/native-host/src/vendor/pdfjs/cmaps/EUC-H.bcmap +0 -0
  267. package/native-host/src/vendor/pdfjs/cmaps/EUC-V.bcmap +0 -0
  268. package/native-host/src/vendor/pdfjs/cmaps/Ext-H.bcmap +0 -0
  269. package/native-host/src/vendor/pdfjs/cmaps/Ext-RKSJ-H.bcmap +0 -0
  270. package/native-host/src/vendor/pdfjs/cmaps/Ext-RKSJ-V.bcmap +0 -0
  271. package/native-host/src/vendor/pdfjs/cmaps/Ext-V.bcmap +0 -0
  272. package/native-host/src/vendor/pdfjs/cmaps/GB-EUC-H.bcmap +0 -0
  273. package/native-host/src/vendor/pdfjs/cmaps/GB-EUC-V.bcmap +0 -0
  274. package/native-host/src/vendor/pdfjs/cmaps/GB-H.bcmap +4 -0
  275. package/native-host/src/vendor/pdfjs/cmaps/GB-V.bcmap +0 -0
  276. package/native-host/src/vendor/pdfjs/cmaps/GBK-EUC-H.bcmap +0 -0
  277. package/native-host/src/vendor/pdfjs/cmaps/GBK-EUC-V.bcmap +0 -0
  278. package/native-host/src/vendor/pdfjs/cmaps/GBK2K-H.bcmap +0 -0
  279. package/native-host/src/vendor/pdfjs/cmaps/GBK2K-V.bcmap +0 -0
  280. package/native-host/src/vendor/pdfjs/cmaps/GBKp-EUC-H.bcmap +0 -0
  281. package/native-host/src/vendor/pdfjs/cmaps/GBKp-EUC-V.bcmap +0 -0
  282. package/native-host/src/vendor/pdfjs/cmaps/GBT-EUC-H.bcmap +0 -0
  283. package/native-host/src/vendor/pdfjs/cmaps/GBT-EUC-V.bcmap +0 -0
  284. package/native-host/src/vendor/pdfjs/cmaps/GBT-H.bcmap +0 -0
  285. package/native-host/src/vendor/pdfjs/cmaps/GBT-V.bcmap +0 -0
  286. package/native-host/src/vendor/pdfjs/cmaps/GBTpc-EUC-H.bcmap +0 -0
  287. package/native-host/src/vendor/pdfjs/cmaps/GBTpc-EUC-V.bcmap +0 -0
  288. package/native-host/src/vendor/pdfjs/cmaps/GBpc-EUC-H.bcmap +0 -0
  289. package/native-host/src/vendor/pdfjs/cmaps/GBpc-EUC-V.bcmap +0 -0
  290. package/native-host/src/vendor/pdfjs/cmaps/H.bcmap +0 -0
  291. package/native-host/src/vendor/pdfjs/cmaps/HKdla-B5-H.bcmap +0 -0
  292. package/native-host/src/vendor/pdfjs/cmaps/HKdla-B5-V.bcmap +0 -0
  293. package/native-host/src/vendor/pdfjs/cmaps/HKdlb-B5-H.bcmap +0 -0
  294. package/native-host/src/vendor/pdfjs/cmaps/HKdlb-B5-V.bcmap +0 -0
  295. package/native-host/src/vendor/pdfjs/cmaps/HKgccs-B5-H.bcmap +0 -0
  296. package/native-host/src/vendor/pdfjs/cmaps/HKgccs-B5-V.bcmap +0 -0
  297. package/native-host/src/vendor/pdfjs/cmaps/HKm314-B5-H.bcmap +0 -0
  298. package/native-host/src/vendor/pdfjs/cmaps/HKm314-B5-V.bcmap +0 -0
  299. package/native-host/src/vendor/pdfjs/cmaps/HKm471-B5-H.bcmap +0 -0
  300. package/native-host/src/vendor/pdfjs/cmaps/HKm471-B5-V.bcmap +0 -0
  301. package/native-host/src/vendor/pdfjs/cmaps/HKscs-B5-H.bcmap +0 -0
  302. package/native-host/src/vendor/pdfjs/cmaps/HKscs-B5-V.bcmap +0 -0
  303. package/native-host/src/vendor/pdfjs/cmaps/Hankaku.bcmap +0 -0
  304. package/native-host/src/vendor/pdfjs/cmaps/Hiragana.bcmap +0 -0
  305. package/native-host/src/vendor/pdfjs/cmaps/KSC-EUC-H.bcmap +0 -0
  306. package/native-host/src/vendor/pdfjs/cmaps/KSC-EUC-V.bcmap +0 -0
  307. package/native-host/src/vendor/pdfjs/cmaps/KSC-H.bcmap +0 -0
  308. package/native-host/src/vendor/pdfjs/cmaps/KSC-Johab-H.bcmap +0 -0
  309. package/native-host/src/vendor/pdfjs/cmaps/KSC-Johab-V.bcmap +0 -0
  310. package/native-host/src/vendor/pdfjs/cmaps/KSC-V.bcmap +0 -0
  311. package/native-host/src/vendor/pdfjs/cmaps/KSCms-UHC-H.bcmap +0 -0
  312. package/native-host/src/vendor/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
  313. package/native-host/src/vendor/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
  314. package/native-host/src/vendor/pdfjs/cmaps/KSCms-UHC-V.bcmap +0 -0
  315. package/native-host/src/vendor/pdfjs/cmaps/KSCpc-EUC-H.bcmap +0 -0
  316. package/native-host/src/vendor/pdfjs/cmaps/KSCpc-EUC-V.bcmap +0 -0
  317. package/native-host/src/vendor/pdfjs/cmaps/Katakana.bcmap +0 -0
  318. package/native-host/src/vendor/pdfjs/cmaps/LICENSE +36 -0
  319. package/native-host/src/vendor/pdfjs/cmaps/NWP-H.bcmap +0 -0
  320. package/native-host/src/vendor/pdfjs/cmaps/NWP-V.bcmap +0 -0
  321. package/native-host/src/vendor/pdfjs/cmaps/RKSJ-H.bcmap +0 -0
  322. package/native-host/src/vendor/pdfjs/cmaps/RKSJ-V.bcmap +0 -0
  323. package/native-host/src/vendor/pdfjs/cmaps/Roman.bcmap +0 -0
  324. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UCS2-H.bcmap +0 -0
  325. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UCS2-V.bcmap +0 -0
  326. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF16-H.bcmap +0 -0
  327. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF16-V.bcmap +0 -0
  328. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF32-H.bcmap +0 -0
  329. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF32-V.bcmap +0 -0
  330. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF8-H.bcmap +0 -0
  331. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF8-V.bcmap +0 -0
  332. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UCS2-H.bcmap +0 -0
  333. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UCS2-V.bcmap +0 -0
  334. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF16-H.bcmap +0 -0
  335. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF16-V.bcmap +0 -0
  336. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF32-H.bcmap +0 -0
  337. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF32-V.bcmap +0 -0
  338. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF8-H.bcmap +0 -0
  339. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF8-V.bcmap +0 -0
  340. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UCS2-H.bcmap +0 -0
  341. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
  342. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
  343. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UCS2-V.bcmap +0 -0
  344. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF16-H.bcmap +0 -0
  345. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF16-V.bcmap +0 -0
  346. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF32-H.bcmap +0 -0
  347. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF32-V.bcmap +0 -0
  348. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF8-H.bcmap +0 -0
  349. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF8-V.bcmap +0 -0
  350. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
  351. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
  352. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
  353. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
  354. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
  355. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
  356. package/native-host/src/vendor/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
  357. package/native-host/src/vendor/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
  358. package/native-host/src/vendor/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
  359. package/native-host/src/vendor/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
  360. package/native-host/src/vendor/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
  361. package/native-host/src/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
  362. package/native-host/src/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
  363. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UCS2-H.bcmap +0 -0
  364. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UCS2-V.bcmap +0 -0
  365. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF16-H.bcmap +0 -0
  366. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF16-V.bcmap +0 -0
  367. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF32-H.bcmap +0 -0
  368. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF32-V.bcmap +0 -0
  369. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF8-H.bcmap +0 -0
  370. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF8-V.bcmap +0 -0
  371. package/native-host/src/vendor/pdfjs/cmaps/V.bcmap +0 -0
  372. package/native-host/src/vendor/pdfjs/cmaps/WP-Symbol.bcmap +0 -0
  373. package/native-host/src/vendor/pdfjs/pdf.mjs +26740 -0
  374. package/native-host/src/vendor/pdfjs/pdf.worker.mjs +62201 -0
  375. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitDingbats.pfb +0 -0
  376. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitFixed.pfb +0 -0
  377. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitFixedBold.pfb +0 -0
  378. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb +0 -0
  379. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitFixedItalic.pfb +0 -0
  380. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitSerif.pfb +0 -0
  381. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitSerifBold.pfb +0 -0
  382. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb +0 -0
  383. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitSerifItalic.pfb +0 -0
  384. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitSymbol.pfb +0 -0
  385. package/native-host/src/vendor/pdfjs/standard_fonts/LICENSE_FOXIT +27 -0
  386. package/native-host/src/vendor/pdfjs/standard_fonts/LICENSE_LIBERATION +102 -0
  387. package/native-host/src/vendor/pdfjs/standard_fonts/LiberationSans-Bold.ttf +0 -0
  388. package/native-host/src/vendor/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf +0 -0
  389. package/native-host/src/vendor/pdfjs/standard_fonts/LiberationSans-Italic.ttf +0 -0
  390. package/native-host/src/vendor/pdfjs/standard_fonts/LiberationSans-Regular.ttf +0 -0
  391. package/native-host/src/writingStyleEvidence.js +102 -0
  392. package/native-host/src/writingStylePdfWorker.js +35 -0
  393. package/native-host/src/writingStyleProfile.js +165 -0
  394. package/native-host/src/writingStyleRuntime.js +475 -0
  395. package/native-host/src/writingStyleStore.js +73 -0
  396. package/package.json +1 -1
  397. package/scripts/install-managed.mjs +1 -0
@@ -1,20 +1,18 @@
1
1
  (function initCodexOverleafTrackedChangesLifecycle(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory(require('./trackedChangeReplay'), require('./reviewContext'));
4
4
  } else {
5
- root.CodexOverleafTrackedChangesLifecycle = factory();
5
+ root.CodexOverleafTrackedChangesLifecycle = factory(root.CodexOverleafTrackedChangeReplay, root.CodexOverleafReviewContext);
6
6
  }
7
- })(typeof window !== 'undefined' ? window : globalThis, function trackedChangesLifecycleFactory() {
7
+ })(typeof window !== 'undefined' ? window : globalThis, function trackedChangesLifecycleFactory(Replay, ReviewContext) {
8
8
  'use strict';
9
-
10
- // Tracked-changes lifecycle — carved out of writebackRouter.js in v1.8.0
11
- // (structural-debt phase 7, #117): the accept/reject flows, the
12
- // accept-replay + editor-undo recovery machinery, the reviewing/editing
13
- // blocked-result builders, and the DOM collectors for Overleaf's
14
- // tracked-change widgets. Code moved verbatim (original indentation kept);
15
- // page-side collaborators are factory-injected by writebackRouter, which
16
- // remains the only consumer and the owner of applyOperations itself.
17
9
  function create(deps = {}) {
10
+ const base = createCore(deps);
11
+ const run = (method, params) => ReviewContext.create(deps, params).run((scoped, input) => createCore(scoped)[method](input));
12
+ return { ...base, rejectTrackedChanges: params => run('rejectTrackedChanges', params),
13
+ acceptTrackedChanges: params => run('acceptTrackedChanges', params) };
14
+ }
15
+ function createCore(deps = {}) {
18
16
  const {
19
17
  window,
20
18
  checkWritebackRunProjectId,
@@ -41,7 +39,7 @@
41
39
  normalizeOperationPaths,
42
40
  normalizeReviewingSignalText,
43
41
  normalizeSafeProjectPath,
44
- openFileByPath,
42
+ openFileByPath: openReviewFileByPath,
45
43
  readActiveEditorText,
46
44
  readNodeSignalText,
47
45
  setReviewingEnabled,
@@ -49,10 +47,19 @@
49
47
  verifyActiveEditorText,
50
48
  collectElements,
51
49
  } = deps;
50
+ const { buildAcceptReplayOperations, rebaseCheckpointPair, prepareSnapshotUndoCheckpoints, buildSnapshotUndoResult } = Replay.create({
51
+ normalizeSafeProjectPath, applyTextPatches
52
+ });
53
+ function openFileByPath(path, ...options) {
54
+ if (path !== getActiveFilePath()) rememberTrackedChangeNodeContext(getActiveFilePath());
55
+ return openReviewFileByPath(path, ...options);
56
+ }
52
57
 
53
58
  function nativeReviewBlocked(path, code, applied = [], requestStarted = false) {
54
59
  const reason = requestStarted
55
60
  ? 'The native review request could not be fully verified; some target changes may already be accepted. No text replay was attempted. Check Overleaf Review before retrying.'
61
+ : code === 'native_untracked_undo_pending_changes'
62
+ ? 'This file has unresolved tracked changes. Undo without track changes was stopped to preserve them; no document changes were made.'
56
63
  : 'The native tracked-change scope could not be safely isolated. No review mutation was attempted. Use Overleaf Review to handle the changes individually.';
57
64
  return { ok: false, applied, skipped: [{ trackedChange: { path }, result: {
58
65
  ok: false, code, reason, failure: {
@@ -65,14 +72,15 @@
65
72
  } }] };
66
73
  }
67
74
 
68
- async function waitForNativeReview(path, refs, untracked = false) {
75
+ async function waitForNativeReview(path, refs, untracked = false, checkpointReady = () => true) {
69
76
  const prepare = () => untracked ? prepareUntrackedUndo(path) : prepareTrackedChangeReview(path, refs);
70
77
  for (let attempt = 0; attempt < 16; attempt++) {
71
78
  const scope = prepare();
72
- if (scope.ok) return scope;
79
+ if (scope.ok && checkpointReady(scope)) return scope;
73
80
  await delay(150);
74
81
  }
75
- return prepare();
82
+ const scope = prepare();
83
+ return scope.ok && !checkpointReady(scope) ? { ...scope, ok: false } : scope;
76
84
  }
77
85
 
78
86
  async function prepareNativeReview(params, allowUntrackedUndo = false) {
@@ -96,7 +104,7 @@
96
104
  ...checkpoints.map(file => normalizeSafeProjectPath(file?.path))])];
97
105
  if ((!refs.length && !untracked) || refs.some(ref => ref.invalidProjectPath || !ref.path) || paths.some(path => !path))
98
106
  return nativeReviewBlocked('', 'native_review_identity_unavailable');
99
- const files = [], deadline = Date.now() + 90000;
107
+ const files = [], deadline = Math.min(Date.now() + 90000, deps.reviewDeadlineAt ?? Infinity);
100
108
  for (const path of paths) {
101
109
  if (Date.now() >= deadline || checkWritebackRunProjectId(params))
102
110
  return nativeReviewBlocked(path, 'native_review_context_changed');
@@ -104,13 +112,121 @@
104
112
  return nativeReviewBlocked(path, 'native_review_file_unavailable');
105
113
  const scope = await waitForNativeReview(path, refs, untracked);
106
114
  if (!scope.ok) return nativeReviewBlocked(path, scope.reason || 'native_review_unavailable');
115
+ if (scope.merged) {
116
+ const before = (params.expectedFiles || []).filter(file => file.path === path);
117
+ const post = (params.postFiles || []).filter(file => file.path === path);
118
+ if (scope.merged.runProjectId !== params.runProjectId || before.length !== 1 || post.length !== 1
119
+ || before[0].content !== scope.merged.preContent || post[0].content !== scope.merged.postContent)
120
+ return nativeReviewBlocked(path, 'native_merged_review_checkpoint_mismatch');
121
+ }
107
122
  files.push({ path, nativeDocId: scope.nativeDocId, scope });
108
123
  }
109
124
  return { ok: true, files, refs, deadline };
110
125
  }
111
126
 
127
+ function mergedReviewContextMatches(params, file, content) {
128
+ const projectId = typeof deps.getProjectId === 'function' ? deps.getProjectId()
129
+ : /\/project\/([^/?#]+)/.exec(window?.location?.pathname || '')?.[1];
130
+ return Boolean(params.runProjectId && projectId === params.runProjectId
131
+ && !checkWritebackRunProjectId(params) && getActiveFilePath() === file.path
132
+ && readActiveEditorText() === content);
133
+ }
134
+
135
+ async function reviewMergedInsertion(params, file, action) {
136
+ const path = file.path, accepting = action === 'accept';
137
+ let scope = prepareTrackedChangeReview(path, file.scope.targets);
138
+ if (!scope.ok || !scope.merged || scope.nativeDocId !== file.nativeDocId
139
+ || !mergedReviewContextMatches(params, file, scope.merged.postContent))
140
+ return nativeReviewBlocked(path, 'native_merged_review_scope_changed');
141
+ const before = (params.expectedFiles || []).filter(item => item.path === path);
142
+ const post = (params.postFiles || []).filter(item => item.path === path);
143
+ if (before.length !== 1 || post.length !== 1
144
+ || before[0].content !== scope.merged.preContent || post[0].content !== scope.merged.postContent)
145
+ return nativeReviewBlocked(path, 'native_merged_review_checkpoint_mismatch');
146
+ const view = deps.getCodeMirrorEditorView?.();
147
+ if (typeof view?.dispatch !== 'function' || view.state?.doc?.toString() !== post[0].content)
148
+ return nativeReviewBlocked(path, 'native_merged_review_editor_unavailable');
149
+
150
+ const editing = await forceEditingForAcceptReplay();
151
+ if (!editing.ok) return nativeReviewBlocked(path, 'native_merged_review_editing_unavailable');
152
+ let attempted = false;
153
+ try {
154
+ const stable = await waitForStableEditingForAcceptReplay({ waitMs: 1400, intervalMs: 160 });
155
+ scope = prepareTrackedChangeReview(path, file.scope.targets);
156
+ if (!stable.ok || !scope.ok || !scope.merged || scope.nativeDocId !== file.nativeDocId
157
+ || !mergedReviewContextMatches(params, file, post[0].content)
158
+ || deps.getCodeMirrorEditorView?.() !== view || view.state.doc.toString() !== post[0].content)
159
+ return nativeReviewBlocked(path, 'native_merged_review_scope_changed');
160
+
161
+ // A same-text replacement is deliberate: plain Editing removes tracking
162
+ // only from these exact inserted slices, without a transient text rollback.
163
+ // Never send a merged parent's ID to the whole-change Accept endpoint.
164
+ const changes = scope.merged.ranges.map(range => ({
165
+ from: range.start, to: range.end,
166
+ insert: accepting ? post[0].content.slice(range.start, range.end) : ''
167
+ }));
168
+ const expectedText = accepting ? post[0].content : before[0].content;
169
+ const expectedKeys = accepting ? scope.merged.preservedKeys : scope.merged.beforeKeys;
170
+ if (deps.reviewGuard?.()) return nativeReviewBlocked(path, 'native_review_context_changed');
171
+ attempted = true;
172
+ view.dispatch({ changes });
173
+ compileBridge.markSourceEdited();
174
+
175
+ const until = Math.min(file.deadline || Date.now() + 8000, Date.now() + 8000);
176
+ let matchingSince = null;
177
+ while (Date.now() <= until) {
178
+ if (!mergedReviewContextMatches(params, file, expectedText))
179
+ return nativeReviewBlocked(path, 'native_merged_review_content_or_context_changed', [], true);
180
+ const after = prepareTrackedChangeCapture(path);
181
+ const matches = after.ready && after.source === 'native' && after.nativeDocId === file.nativeDocId
182
+ && JSON.stringify(after.refs.map(ref => ref.key).sort()) === JSON.stringify(expectedKeys);
183
+ if (matches) {
184
+ if (matchingSince === null) matchingSince = Date.now();
185
+ if (Date.now() - matchingSince >= 500) {
186
+ return { ok: true, verified: true, applied: scope.targets.map(trackedChange => ({
187
+ trackedChange, result: { ok: true, verified: true, verifiedContent: expectedText,
188
+ method: accepting ? 'overleaf-accept-untracked-replay' : 'overleaf-merged-insertion-undo',
189
+ preservedTrackedChanges: true }
190
+ })), skipped: [] };
191
+ }
192
+ } else matchingSince = null;
193
+ await delay(150);
194
+ }
195
+ return nativeReviewBlocked(path, 'native_merged_review_not_verified', [], true);
196
+ } catch (_error) {
197
+ return nativeReviewBlocked(path, 'native_merged_review_interrupted', [], attempted);
198
+ } finally {
199
+ // Match the existing no-trace Accept policy: do not re-enable tracking
200
+ // while an untracked operation can still be awaiting its server flush.
201
+ if (!attempted && editing.activated) await setReviewingEnabled(true, { waitMs: 1800 });
202
+ }
203
+ }
204
+
205
+ async function rejectPlanWithMergedInsertions(params, plan) {
206
+ const mixed = plan.files.find(file => !file.scope.merged && file.scope.unrelated.length);
207
+ if (mixed) return nativeReviewBlocked(mixed.path, 'native_reject_mixed_changes');
208
+ const applied = deps.createReviewApplied?.() || [];
209
+ for (const file of plan.files) {
210
+ if (Date.now() >= plan.deadline || checkWritebackRunProjectId(params))
211
+ return nativeReviewBlocked(file.path, 'native_review_context_changed', applied, applied.length > 0);
212
+ if (getActiveFilePath() !== file.path && !(await openFileByPath(file.path)).ok)
213
+ return nativeReviewBlocked(file.path, 'native_review_file_unavailable', applied, applied.length > 0);
214
+ const scoped = { ...params,
215
+ trackedChanges: plan.refs.filter(ref => ref.path === file.path),
216
+ expectedFiles: (params.expectedFiles || []).filter(item => item.path === file.path),
217
+ postFiles: (params.postFiles || []).filter(item => item.path === file.path)
218
+ };
219
+ const result = file.scope.merged
220
+ ? await reviewMergedInsertion(scoped, { ...file, deadline: plan.deadline }, 'reject')
221
+ : await rejectTrackedChanges(scoped);
222
+ applied.push(...(result.applied || []));
223
+ if (!result.ok) return { ...result, applied };
224
+ }
225
+ return { ok: true, applied, skipped: [] };
226
+ }
227
+
112
228
  async function acceptNativeReview(params, plan) {
113
- const applied = [];
229
+ const applied = deps.createReviewApplied?.() || [];
114
230
  let token = window?.document?.querySelector?.('meta[name="ol-csrfToken"]')?.getAttribute('content');
115
231
  try { token = JSON.parse(token); } catch (_error) { /* Older pages expose plain text. */ }
116
232
  if (typeof token !== 'string' || !token || token.length > 4096
@@ -126,6 +242,12 @@
126
242
  return nativeReviewBlocked(path, 'native_review_context_changed', applied, applied.length > 0);
127
243
  if (getActiveFilePath() !== path && !(await openFileByPath(path)).ok)
128
244
  return nativeReviewBlocked(path, 'native_review_file_unavailable', applied, applied.length > 0);
245
+ if (file.scope.merged) {
246
+ const result = await reviewMergedInsertion(params, { ...file, deadline: plan.deadline }, 'accept');
247
+ applied.push(...(result.applied || []));
248
+ if (!result.ok) return { ...result, applied };
249
+ continue;
250
+ }
129
251
  await waitForNativeReview(path, plan.refs);
130
252
  const scope = prepareTrackedChangeReview(path, plan.refs), text = readActiveEditorText();
131
253
  if (!scope.ok || !scope.acceptByIdSupported || scope.nativeDocId !== file.nativeDocId
@@ -135,6 +257,7 @@
135
257
  const controller = new window.AbortController();
136
258
  const timer = window.setTimeout(() => controller.abort(), Math.min(12000, Math.max(1, plan.deadline - Date.now())));
137
259
  try {
260
+ if (deps.reviewGuard?.()) return nativeReviewBlocked(path, 'native_review_context_changed', applied, applied.length > 0);
138
261
  // Same ID-scoped endpoint and CSRF header used by Overleaf's RangesProvider.
139
262
  const response = await window.fetch('/project/' + encodeURIComponent(params.runProjectId)
140
263
  + '/doc/' + encodeURIComponent(scope.nativeDocId) + '/changes/accept', {
@@ -142,6 +265,7 @@
142
265
  headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-Csrf-Token': token },
143
266
  body: JSON.stringify({ change_ids: scope.ids })
144
267
  });
268
+ if (deps.reviewGuard?.()) return nativeReviewBlocked(path, 'native_review_context_changed', applied, true);
145
269
  if (!response.ok) return nativeReviewBlocked(path, 'native_accept_http_' + response.status, applied, true);
146
270
  } catch (_error) {
147
271
  return nativeReviewBlocked(path, 'native_accept_request_unconfirmed', applied, true);
@@ -166,19 +290,15 @@
166
290
  return { ok: true, verified: true, applied, skipped: [] };
167
291
  }
168
292
 
293
+ let nativeRejectPlan = null;
169
294
  async function rejectTrackedChanges(params = {}) {
170
- // Welcome-panel + write-guard: defense-in-depth.
171
- // The pageBridge wrapper already runs the runProjectId guard, but a
172
- // future caller could reach the router directly. A missing or empty
173
- // `runProjectId` blocks the reject with the same shape the page-side
174
- // guard uses. Existing tests that target individual reject behaviors set
175
- // `runProjectId` on params; tests that intentionally omit it land here.
295
+ // Direct router callers also require the original run project identity.
176
296
  const writeGuardBlock = checkWritebackRunProjectId(params);
177
297
  if (writeGuardBlock) return writeGuardBlock;
178
298
  const trackedChanges = normalizeTrackedChangeRefs(params.trackedChanges || []);
179
299
  const expectedFiles = Array.isArray(params.expectedFiles) ? params.expectedFiles : [];
180
300
  const postFiles = Array.isArray(params.postFiles) ? params.postFiles : [];
181
- const applied = [];
301
+ const applied = deps.createReviewApplied?.() || [];
182
302
  const skipped = [];
183
303
  const appliedPaths = new Set();
184
304
  const invalidTrackedChange = trackedChanges.find(trackedChange => trackedChange.invalidProjectPath);
@@ -194,9 +314,26 @@
194
314
  }
195
315
 
196
316
  const nativeReview = await prepareNativeReview(params, true);
317
+ nativeRejectPlan = nativeReview;
197
318
  if (nativeReview && !nativeReview.ok) return nativeReview;
319
+ if (nativeReview?.files.some(file => file.scope.merged))
320
+ return rejectPlanWithMergedInsertions(params, nativeReview);
198
321
  const mixedFile = nativeReview?.files.find(file => file.scope.unrelated.length);
199
322
  if (mixedFile) return nativeReviewBlocked(mixedFile.path, 'native_reject_mixed_changes');
323
+ if (params.untrackedUndo === true && !trackedChanges.length) {
324
+ if (!nativeReview) return nativeReviewBlocked('', 'native_review_unavailable');
325
+ // An ordinary write has no owned editor-history entry. Restore its
326
+ // checkpoint directly; global Undo may belong to an earlier user edit.
327
+ const restored = await restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles, {
328
+ runProjectId: params.runProjectId, untrackedPlan: nativeReview
329
+ });
330
+ if (restored.applied.some(entry => entry.result?.changedDocument !== false)) compileBridge.markSourceEdited();
331
+ return { ...restored, ok: restored.attempted && restored.ok,
332
+ skipped: restored.attempted ? restored.skipped : [{ trackedChange: { path: restored.file || '' }, result: {
333
+ ok: false, file: restored.file || '', code: restored.code || 'snapshot_undo_unavailable',
334
+ reason: restored.reason || 'The saved checkpoint is unavailable for this undo.'
335
+ } }] };
336
+ }
200
337
  const editorUndo = await rejectTrackedChangesViaEditorUndo(expectedFiles, postFiles, applied);
201
338
  if (editorUndo.ok) {
202
339
  if (applied.length > 0) {
@@ -295,7 +432,7 @@
295
432
  continue;
296
433
  }
297
434
 
298
- const rejectControl = findRejectControlForTrackedChangeNode(node);
435
+ const rejectControl = isTrackedChangeNodeInNativeScope(node, trackedChange.path || getActiveFilePath()) ? findRejectControlForTrackedChangeNode(node) : null;
299
436
  if (!rejectControl) {
300
437
  if (expectedFiles.length || trackedChange.path) {
301
438
  continue;
@@ -345,7 +482,7 @@
345
482
  const completion = await rejectRemainingTrackedChangesForExpectedFiles(expectedFiles, applied);
346
483
  if (!completion.ok) {
347
484
  skipped.push({
348
- trackedChange: null,
485
+ trackedChange: completion.file ? { path: completion.file } : null,
349
486
  result: completion
350
487
  });
351
488
  }
@@ -353,7 +490,7 @@
353
490
  const completion = await rejectRemainingTrackedChangesForTrackedPaths(trackedChanges, applied);
354
491
  if (!completion.ok) {
355
492
  skipped.push({
356
- trackedChange: null,
493
+ trackedChange: completion.file ? { path: completion.file } : null,
357
494
  result: completion
358
495
  });
359
496
  }
@@ -373,18 +510,15 @@
373
510
  // Native-ledger browsers accept exact IDs without changing document text.
374
511
  // The original undo/replay flow remains only for legacy adapter callers.
375
512
  async function acceptTrackedChanges(params = {}) {
376
- // Welcome-panel + write-guard: defense-in-depth.
377
- // The pageBridge wrapper already runs the runProjectId guard, but a
378
- // future caller could reach the router directly. A missing or empty
379
- // `runProjectId` blocks the accept with the same shape the page-side
380
- // guard uses.
513
+ // Direct router callers also require the original run project identity.
381
514
  const writeGuardBlock = checkWritebackRunProjectId(params);
382
515
  if (writeGuardBlock) return writeGuardBlock;
516
+ if (params.verifyManualReview === true) return nativeReviewBlocked('', 'native_accept_protocol_unsupported');
383
517
  const nativeReview = await prepareNativeReview(params);
384
518
  if (nativeReview) return nativeReview.ok ? acceptNativeReview(params, nativeReview) : nativeReview;
385
519
  const expectedFiles = Array.isArray(params.expectedFiles) ? params.expectedFiles : [];
386
520
  const postFiles = Array.isArray(params.postFiles) ? params.postFiles : [];
387
- const applied = [];
521
+ const applied = deps.createReviewApplied?.() || [];
388
522
  const skipped = [];
389
523
  // diagnostics: a per-step trace returned to the caller so the run card can
390
524
  // surface exactly what happened on each Accept All step. Each entry is
@@ -427,12 +561,8 @@
427
561
  const editorUndo = await rejectTrackedChangesViaEditorUndo(expectedFiles, postFiles, applied);
428
562
  let undoReady = editorUndo.ok === true;
429
563
  let snapshotUndo = null;
430
- // A page refresh preserves the run checkpoint but clears CodeMirror's
431
- // in-memory undo history. When the native editor undo made no progress,
432
- // reuse Undo's guarded snapshot path: it first verifies that every file is
433
- // still exactly at this run's post-write content, then restores the
434
- // persisted pre-write content with Track Changes disabled. The normal
435
- // Accept replay below can then apply the post-write patch untracked.
564
+ // After a refresh clears native history, guarded snapshot rollback can
565
+ // restore exact checkpoints in Editing before the legacy Accept replay.
436
566
  if (!undoReady && applied.length === appliedBeforeUndo) {
437
567
  snapshotUndo = await restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles, {
438
568
  runProjectId: params.runProjectId
@@ -498,10 +628,8 @@
498
628
  reason: editingSwitch.reason || ''
499
629
  });
500
630
  if (!editingSwitch.ok) {
501
- // The undo already reverted the writeback; without a *confirmed* Editing
502
- // mode the replay would itself be tracked. Bail rather than re-introduce
503
- // tracked changes. The structured failure from forceEditingForAcceptReplay
504
- // (editing_not_confirmed, changedDocument:true) is preserved verbatim.
631
+ // Undo already changed the document. Preserve that evidence and stop
632
+ // when Editing cannot be confirmed for an untracked replay.
505
633
  return {
506
634
  ok: false,
507
635
  applied: [],
@@ -544,12 +672,8 @@
544
672
  };
545
673
  }
546
674
 
547
- // Step 3: re-apply each file's changed fragments as a plain edit. With
548
- // tracking off these land as permanent, untracked text. The replay must
549
- // write only the minimal changed fragments via the `patches` path of
550
- // applyOperationsCore — never a whole-file replaceAll, which would clobber
551
- // any unrelated content and produce one giant tracked change if anything
552
- // about the mode switch were imperfect.
675
+ // Replay only the changed fragments in Editing; whole-file replacement
676
+ // would overwrite unrelated text or create one large tracked change.
553
677
  const operationsResult = buildAcceptReplayOperations(
554
678
  paths,
555
679
  expectedByPath,
@@ -571,18 +695,9 @@
571
695
  };
572
696
  }
573
697
  const operations = operationsResult.operations;
574
- // Per-op replay loop with sticky-Editing re-confirm and a short stable
575
- // window before every actual CodeMirror write.
576
- //
577
- // Bug C: even after forceEditingForAcceptReplay confirmed Editing once,
578
- // Overleaf has been observed flipping back to Reviewing between the
579
- // confirm and the next CodeMirror write (Overleaf-side override, per-user
580
- // "Track Changes for me" setting, or a positive-confirm false-positive
581
- // window). The fix: re-verify Editing immediately BEFORE every single
582
- // operation, and if Reviewing has slipped back on, force the toggle off
583
- // again and positively re-confirm before writing. If Editing still cannot
584
- // be positively confirmed after the re-toggle, bail the rest of the loop
585
- // so we never land a tracked write.
698
+ // Overleaf may return to Reviewing between confirmation and a write.
699
+ // Reconfirm stable Editing before every operation, force it off again
700
+ // if needed, and stop the tail whenever confirmation fails.
586
701
  let bailedReason = null;
587
702
  for (const operation of operations) {
588
703
  const opPath = operation?.path || '';
@@ -736,12 +851,8 @@
736
851
  }
737
852
  }
738
853
 
739
- // Step 4 intentionally does NOT restore Reviewing. Accept replay is a
740
- // no-trace write transaction: restoring Track Changes immediately after the
741
- // CodeMirror dispatch can race with Overleaf's collaboration/reviewing
742
- // settlement and cause the replay itself to land as fresh tracked changes.
743
- // Leave the editor in Editing after a successful replay; the user can turn
744
- // Reviewing back on manually after Overleaf has saved the accepted text.
854
+ // Leave Editing active: restoring Reviewing before collaboration settles
855
+ // can turn the untracked Accept replay into fresh tracked changes.
745
856
  if (weToggledOff) {
746
857
  pushDiagnostic('restoreReviewing', {
747
858
  ok: true,
@@ -763,12 +874,8 @@
763
874
  };
764
875
  }
765
876
 
766
- // Summarizes a reviewing state for the per-step diagnostics. Surfaces
767
- // exactly the bits the user needs to diagnose a sticky-Editing slip: the
768
- // reviewing detector's verdict (ok/status/source), a compact controls
769
- // summary, and the two strict gates (isReviewingPositivelyOn /
770
- // isEditingPositivelyConfirmed). Self-contained — the run card can render
771
- // this verbatim without re-reading any page state.
877
+ // Capture the detector signals and strict gates for self-contained
878
+ // diagnostics; rendering must not reread mutable page state.
772
879
  function summarizeReviewingStateForDiagnostics(state = {}) {
773
880
  const reviewing = state.reviewing || {};
774
881
  const controls = (state.signals?.controls || []).slice(0, 6).map(control => {
@@ -794,10 +901,8 @@
794
901
  };
795
902
  }
796
903
 
797
- // Track Changes is positively confirmed OFF only when Editing is positively
798
- // detected AND Reviewing is NOT positively confirmed on. This is the strict
799
- // gate the Accept replay needs: it must never replay while tracked, so
800
- // anything short of a positive "Editing on / Reviewing off" is rejected.
904
+ // Accept replay requires positively detected Editing and no positively
905
+ // confirmed Reviewing; every weaker observation must stop the replay.
801
906
  function isEditingPositivelyConfirmed(state = {}) {
802
907
  return isEditingConfirmedForNoTraceUndo(state) && !isReviewingConfirmedForWrite(state);
803
908
  }
@@ -856,42 +961,20 @@
856
961
  };
857
962
  }
858
963
 
859
- // Robustly switch Overleaf to Editing (Track Changes OFF) for the Accept
860
- // replay.
861
- //
862
- // Bug B root cause: the accept flow used ensureEditing, which short-circuits
863
- // on isEditingConfirmedForNoTraceUndo. That detector is negation-based — it
864
- // returns true whenever Reviewing is not *positively* confirmed-for-write
865
- // (which requires an active aria attribute the real Overleaf reviewing
866
- // control does not always carry) AND a control loosely matches an Editing
867
- // pattern. So while Track Changes was actually ON, it false-positived
868
- // "already Editing", ensureEditing returned activated:false without toggling,
869
- // and the replay landed as tracked changes.
870
- //
871
- // The fix here does NOT trust that lenient short-circuit: it reads the
872
- // reviewing state explicitly and treats Editing as confirmed only when it is
873
- // *positively* confirmed (Editing detected AND Reviewing not confirmed on).
874
- // Whenever that strict gate is not met it forces setReviewingEnabled(false),
875
- // then positively re-confirms Editing before returning ok; if it still cannot
876
- // confirm Editing it returns not-ok so the caller bails instead of replaying
877
- // while tracked.
964
+ // Accept replay requires positively confirmed Editing. The legacy detector
965
+ // can misclassify Reviewing controls without aria state; force the toggle
966
+ // and recheck before replaying untracked text.
878
967
  async function forceEditingForAcceptReplay() {
879
968
  const initial = getReviewingState({});
880
969
  if (isEditingPositivelyConfirmed(initial)) {
881
- // Positively confirmed already in Editing — no toggle needed.
882
970
  return { ok: true, activated: false };
883
971
  }
884
972
 
885
- // Reviewing is on, or Editing is not positively confirmed off. Force the
886
- // toggle rather than trusting ensureEditing's lenient detection.
973
+ // Force the toggle whenever Editing is not positively confirmed.
887
974
  const switched = await setReviewingEnabled(false, { waitMs: 1800 });
888
975
  if (!switched.ok) {
889
- // §9.4 editing_not_confirmed: the document is at pre-write state after
890
- // the editor-undo succeeded; the mode toggle failed. The Accept replay
891
- // was rolled back so the user-visible doc state has moved from the
892
- // post-write tracked state back to pre-write — set changedDocument:true
893
- // per the design spec's "is there text the user might want to inspect"
894
- // contract for accept-side failures after a rollback.
976
+ // Native Undo already changed the document; a failed mode switch must
977
+ // preserve changedDocument evidence even though replay never started.
895
978
  return {
896
979
  ok: false,
897
980
  code: switched.code || 'accept_editing_not_confirmed',
@@ -933,126 +1016,6 @@
933
1016
  return { ok: true, activated: true };
934
1017
  }
935
1018
 
936
- // Builds the minimal replay operations for Accept All. The replay must write
937
- // only the changed fragments, never a whole-file replaceAll.
938
- //
939
- // Preferred source: the run's own original forward writeback operations
940
- // (carrying their `patches`). After the editor-undo the document is back at
941
- // the pre-write content, so those patches' `expected` slices match and
942
- // re-apply cleanly.
943
- //
944
- // Fallback: when a path has no usable original patch operation, compute a
945
- // minimal pre->post diff (trim the common prefix/suffix to a single targeted
946
- // {from,to,insert} patch).
947
- function buildAcceptReplayOperations(paths, expectedByPath, postByPath, appliedOperations) {
948
- const patchesByPath = collectAppliedEditPatchesByPath(appliedOperations);
949
- const operations = [];
950
- for (const path of paths) {
951
- const preContent = expectedByPath.get(path);
952
- const postContent = postByPath.get(path);
953
- if (typeof preContent !== 'string' || typeof postContent !== 'string') {
954
- return {
955
- ok: false,
956
- code: 'accept_missing_run_content',
957
- reason: `${path} 缺少写入前/写入后内容;Codex 没有重放本轮改动。`
958
- };
959
- }
960
-
961
- // Preferred: re-apply the run's original forward patches verbatim.
962
- const originalPatches = patchesByPath.get(path);
963
- if (originalPatches && originalPatches.length && patchesApplyCleanly(preContent, originalPatches)) {
964
- operations.push({
965
- type: 'edit',
966
- path,
967
- patches: originalPatches,
968
- reason: 'Accept tracked edit (replay untracked)'
969
- });
970
- continue;
971
- }
972
-
973
- // Fallback: minimal pre->post diff trimmed to a single targeted patch.
974
- const diffPatch = buildMinimalDiffPatch(preContent, postContent);
975
- if (!diffPatch) {
976
- // pre === post: nothing to replay for this file.
977
- continue;
978
- }
979
- operations.push({
980
- type: 'edit',
981
- path,
982
- patches: [diffPatch],
983
- reason: 'Accept tracked edit (replay untracked)'
984
- });
985
- }
986
- return { ok: true, operations };
987
- }
988
-
989
- // Collects the per-path `patches` arrays from the run's applied edit
990
- // operations. Only edit operations that actually carried `patches` are
991
- // usable; whole-file replaceAll / verifiedContent operations are skipped so
992
- // the replay never falls back to a whole-file write.
993
- function collectAppliedEditPatchesByPath(appliedOperations) {
994
- const patchesByPath = new Map();
995
- for (const rawOperation of Array.isArray(appliedOperations) ? appliedOperations : []) {
996
- if (!rawOperation || rawOperation.type !== 'edit') {
997
- continue;
998
- }
999
- const path = typeof rawOperation.path === 'string'
1000
- ? normalizeSafeProjectPath(rawOperation.path)
1001
- : '';
1002
- if (!path || !Array.isArray(rawOperation.patches) || !rawOperation.patches.length) {
1003
- continue;
1004
- }
1005
- const normalized = rawOperation.patches.map(patch => ({
1006
- from: Number(patch?.from),
1007
- to: Number(patch?.to),
1008
- expected: String(patch?.expected ?? ''),
1009
- insert: String(patch?.insert ?? '')
1010
- }));
1011
- const existing = patchesByPath.get(path) || [];
1012
- patchesByPath.set(path, existing.concat(normalized));
1013
- }
1014
- return patchesByPath;
1015
- }
1016
-
1017
- // Confirms a patch set re-applies cleanly against the given (pre-write) text:
1018
- // every patch range is valid and its `expected` slice matches.
1019
- function patchesApplyCleanly(text, patches) {
1020
- const applied = applyTextPatches(text, patches);
1021
- return applied.ok === true;
1022
- }
1023
-
1024
- // Computes a single targeted {from,to,insert} patch describing the pre->post
1025
- // change by trimming the common prefix and suffix. Returns null when the two
1026
- // strings are identical.
1027
- function buildMinimalDiffPatch(preContent, postContent) {
1028
- if (preContent === postContent) {
1029
- return null;
1030
- }
1031
- const preLen = preContent.length;
1032
- const postLen = postContent.length;
1033
- let prefix = 0;
1034
- const maxPrefix = Math.min(preLen, postLen);
1035
- while (prefix < maxPrefix && preContent[prefix] === postContent[prefix]) {
1036
- prefix += 1;
1037
- }
1038
- let suffix = 0;
1039
- const maxSuffix = Math.min(preLen, postLen) - prefix;
1040
- while (
1041
- suffix < maxSuffix
1042
- && preContent[preLen - 1 - suffix] === postContent[postLen - 1 - suffix]
1043
- ) {
1044
- suffix += 1;
1045
- }
1046
- const from = prefix;
1047
- const to = preLen - suffix;
1048
- return {
1049
- from,
1050
- to,
1051
- expected: preContent.slice(from, to),
1052
- insert: postContent.slice(from, postLen - suffix)
1053
- };
1054
- }
1055
-
1056
1019
  async function rejectTrackedChangesViaEditorUndo(expectedFiles, postFiles, applied) {
1057
1020
  const expectedByPath = new Map((expectedFiles || [])
1058
1021
  .filter(file => file?.path && typeof file.content === 'string')
@@ -1071,10 +1034,8 @@
1071
1034
  const postContent = postByPath.get(path);
1072
1035
  const activePathMatches = getActiveFilePath() === path;
1073
1036
  const activeContentMatches = readActiveEditorText() === postContent;
1074
- // Overleaf can update the selected tree path before CodeMirror swaps its
1075
- // document. In that state getActiveFilePath() already names `path`, while
1076
- // readActiveEditorText() still belongs to the previously viewed file.
1077
- // Force-reopen the target whenever either identity signal disagrees.
1037
+ // Tree selection can precede the editor swap. Reopen when either
1038
+ // the target path or its exact postimage is not yet active.
1078
1039
  if (path && (!activePathMatches || !activeContentMatches)) {
1079
1040
  const opened = await openFileByPath(path, { force: true });
1080
1041
  if (!opened.ok) {
@@ -1125,18 +1086,23 @@
1125
1086
  }
1126
1087
 
1127
1088
  async function restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles, options = {}) {
1128
- const expectedByPath = new Map((expectedFiles || [])
1129
- .filter(file => file?.path && typeof file.content === 'string')
1130
- .map(file => [normalizeSafeProjectPath(file.path), file.content])
1131
- .filter(([path]) => path));
1132
- const postByPath = new Map((postFiles || [])
1133
- .filter(file => file?.path && typeof file.content === 'string')
1134
- .map(file => [normalizeSafeProjectPath(file.path), file.content])
1135
- .filter(([path]) => path));
1136
- const paths = Array.from(expectedByPath.keys()).filter(path => postByPath.has(path));
1089
+ const { expectedByPath, postByPath, paths } = prepareSnapshotUndoCheckpoints(expectedFiles, postFiles);
1137
1090
  const rebasedExpectedByPath = new Map();
1138
1091
  const rebasedPostByPath = new Map();
1139
1092
  const undoRebaseProofs = new Map();
1093
+ // Only the ordinary, native-ledger-checked Undo route may recognize a
1094
+ // lost receipt's exact pre-image. This consumes an existing checkpoint;
1095
+ // it grants no native review, file creation or move ownership.
1096
+ const untrackedByPath = new Map((options.untrackedPlan?.files || []).map(file => [file.path, file]));
1097
+ const alreadyRestored = new Set();
1098
+ const mismatch = path => ({ ok: false, file: path, changedDocument: false, code: 'snapshot_undo_current_mismatch',
1099
+ reason: `${path} 当前内容或文档身份与本轮撤销检查点不一致;Codex 保留恢复材料并停止撤销。` });
1100
+ async function verifyAlreadyRestored(path) {
1101
+ if (getActiveFilePath() !== path && !(await openFileByPath(path)).ok) return false;
1102
+ return (await waitForNativeReview(path, [], true, scope => Boolean(scope.nativeDocId)
1103
+ && scope.nativeDocId === untrackedByPath.get(path)?.nativeDocId
1104
+ && getActiveFilePath() === path && readActiveEditorText() === expectedByPath.get(path))).ok === true;
1105
+ }
1140
1106
  if (!paths.length) {
1141
1107
  return {
1142
1108
  ok: false,
@@ -1157,7 +1123,7 @@
1157
1123
  attempted: false,
1158
1124
  applied: [],
1159
1125
  skipped: [],
1160
- code: 'snapshot_undo_open_failed',
1126
+ code: 'snapshot_undo_open_failed', file: path,
1161
1127
  reason: `无法打开 ${path} 来执行快照撤销。`
1162
1128
  };
1163
1129
  }
@@ -1166,7 +1132,8 @@
1166
1132
  path,
1167
1133
  expectedByPath.get(path),
1168
1134
  postByPath.get(path),
1169
- 1500
1135
+ 1500,
1136
+ untrackedByPath.has(path)
1170
1137
  );
1171
1138
  if (!ready.ok) {
1172
1139
  return {
@@ -1176,10 +1143,14 @@
1176
1143
  skipped: [],
1177
1144
  rebasedExpectedFiles: [],
1178
1145
  rebasedPostFiles: [],
1179
- code: 'snapshot_undo_current_mismatch',
1146
+ code: 'snapshot_undo_current_mismatch', file: path,
1180
1147
  reason: `${path} 当前内容已经不是本轮写入后的内容;为避免覆盖你的后续修改,Codex 不执行快照撤销。`
1181
1148
  };
1182
1149
  }
1150
+ if (ready.alreadyRestored) {
1151
+ if (!(await verifyAlreadyRestored(path))) return { ...mismatch(path), attempted: false, applied: [], skipped: [] };
1152
+ alreadyRestored.add(path);
1153
+ }
1183
1154
  rebasedExpectedByPath.set(path, ready.expectedContent);
1184
1155
  rebasedPostByPath.set(path, ready.postContent);
1185
1156
  if (ready.prefixLength || ready.suffixLength) {
@@ -1187,55 +1158,49 @@
1187
1158
  }
1188
1159
  }
1189
1160
 
1190
- const operations = paths.map(path => ({
1161
+ // Re-check observations after other preflight awaits before starting the
1162
+ // unfinished tail. They are checked again after writes before settlement.
1163
+ for (const path of alreadyRestored) {
1164
+ if (!(await verifyAlreadyRestored(path))) return { ...mismatch(path), attempted: false, applied: [], skipped: [] };
1165
+ }
1166
+ const operations = paths.filter(path => !alreadyRestored.has(path)).map(path => ({
1191
1167
  type: 'edit',
1192
1168
  path,
1193
1169
  replaceAll: rebasedExpectedByPath.get(path),
1194
1170
  reason: 'Undo tracked edit'
1195
1171
  }));
1196
- const result = await applyOperationsWithNoTraceUndo(operations, {
1172
+ const result = operations.length ? await applyOperationsWithNoTraceUndo(operations, {
1197
1173
  baseFiles: paths.map(path => ({
1198
1174
  path,
1199
1175
  content: rebasedPostByPath.get(path)
1200
1176
  })),
1201
1177
  runProjectId: typeof options.runProjectId === 'string' ? options.runProjectId : ''
1202
- });
1203
- const toTrackedResult = item => ({
1204
- trackedChange: {
1205
- key: `snapshot-undo:${item.operation?.path || ''}`,
1206
- id: '',
1207
- path: item.operation?.path || '',
1208
- label: 'No-trace snapshot undo'
1209
- },
1210
- result: item.result?.ok !== false && item.result?.verified === true
1211
- && undoRebaseProofs.has(item.operation?.path)
1212
- && item.result.verifiedContent === rebasedExpectedByPath.get(item.operation?.path)
1213
- ? { ...item.result, undoRebaseProof: undoRebaseProofs.get(item.operation.path) }
1214
- : item.result
1215
- });
1216
- return {
1217
- ok: !(result.skipped || []).length,
1218
- attempted: true,
1219
- applied: (result.applied || []).map(toTrackedResult),
1220
- skipped: (result.skipped || []).map(toTrackedResult),
1221
- rebasedExpectedFiles: paths.map(path => ({
1222
- path,
1223
- content: rebasedExpectedByPath.get(path)
1224
- })),
1225
- rebasedPostFiles: paths.map(path => ({
1226
- path,
1227
- content: rebasedPostByPath.get(path)
1228
- }))
1229
- };
1178
+ }) : { applied: [], skipped: [] };
1179
+ const converted = buildSnapshotUndoResult(result, rebasedExpectedByPath, rebasedPostByPath, undoRebaseProofs);
1180
+ const applied = deps.createReviewApplied?.() || [];
1181
+ applied.push(...converted.applied);
1182
+ const skipped = converted.skipped;
1183
+ for (const path of alreadyRestored) {
1184
+ const verified = await verifyAlreadyRestored(path);
1185
+ const entry = { trackedChange: { key: `snapshot-undo:${path}`, id: '', path,
1186
+ label: 'Already restored checkpoint' }, result: verified
1187
+ ? { ok: true, verified: true, verifiedContent: expectedByPath.get(path), changedDocument: false, idempotent: true }
1188
+ : mismatch(path) };
1189
+ (verified ? applied : skipped).push(entry);
1190
+ }
1191
+ return { ...converted, ok: !skipped.length, applied, skipped };
1230
1192
  }
1231
1193
 
1232
- async function waitForActiveEditorCheckpoint(filePath, expectedContent, postContent, timeoutMs) {
1194
+ async function waitForActiveEditorCheckpoint(filePath, expectedContent, postContent, timeoutMs, allowAlreadyRestored = false) {
1233
1195
  const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
1234
1196
  let actual = readActiveEditorText();
1235
1197
  while (Date.now() < deadline) {
1236
1198
  const activeFileMatches = !filePath || getActiveFilePath() === filePath;
1237
1199
  actual = readActiveEditorText();
1238
1200
  if (activeFileMatches) {
1201
+ if (allowAlreadyRestored && actual === expectedContent) {
1202
+ return { ok: true, alreadyRestored: true, expectedContent, postContent, prefixLength: 0, suffixLength: 0 };
1203
+ }
1239
1204
  const rebased = rebaseCheckpointPair(actual, expectedContent, postContent);
1240
1205
  if (rebased.ok) {
1241
1206
  return rebased;
@@ -1249,37 +1214,6 @@
1249
1214
  };
1250
1215
  }
1251
1216
 
1252
- function rebaseCheckpointPair(actualContent, expectedContent, postContent) {
1253
- const actual = String(actualContent ?? '');
1254
- const expected = String(expectedContent ?? '');
1255
- const post = String(postContent ?? '');
1256
- if (actual === post) {
1257
- return {
1258
- ok: true,
1259
- expectedContent: expected,
1260
- postContent: post,
1261
- prefixLength: 0,
1262
- suffixLength: 0
1263
- };
1264
- }
1265
- if (!post) {
1266
- return { ok: false };
1267
- }
1268
- const matchIndex = actual.indexOf(post);
1269
- if (matchIndex < 0 || actual.indexOf(post, matchIndex + 1) >= 0) {
1270
- return { ok: false };
1271
- }
1272
- const prefix = actual.slice(0, matchIndex);
1273
- const suffix = actual.slice(matchIndex + post.length);
1274
- return {
1275
- ok: true,
1276
- expectedContent: prefix + expected + suffix,
1277
- postContent: actual,
1278
- prefixLength: prefix.length,
1279
- suffixLength: suffix.length
1280
- };
1281
- }
1282
-
1283
1217
  async function waitForActiveEditorExpectedText(filePath, expectedContent, timeoutMs) {
1284
1218
  const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
1285
1219
  let actual = readActiveEditorText();
@@ -1306,34 +1240,36 @@
1306
1240
  const maxClicks = 200;
1307
1241
  for (let clicks = 0; clicks <= maxClicks; clicks += 1) {
1308
1242
  if (readActiveEditorText() === expectedContent) {
1309
- return {
1310
- ok: true,
1311
- attempted: clicks > 0,
1312
- clicks
1313
- };
1243
+ return { ok: true, attempted: clicks > 0, clicks };
1314
1244
  }
1315
1245
  if (clicks === maxClicks) {
1316
1246
  break;
1317
1247
  }
1318
1248
 
1319
- const undoControl = findEditorUndoControl();
1249
+ const beforeText = readActiveEditorText();
1250
+ let undoControl = findEditorUndoControl();
1320
1251
  if (!undoControl) {
1321
- return {
1322
- ok: false,
1323
- attempted: clicks > 0,
1324
- code: 'editor_undo_control_not_found',
1252
+ // Text and the native ledger can be ready before the toolbar updates.
1253
+ // Wait only before dispatch, keeping this click's scope fixed.
1254
+ const assertStable = await createReviewScopeGuard(path, beforeText), until = Date.now() + 1500;
1255
+ while (!undoControl && Date.now() < until) {
1256
+ assertStable();
1257
+ await delay(Math.min(60, until - Date.now()));
1258
+ assertStable();
1259
+ undoControl = findEditorUndoControl();
1260
+ }
1261
+ assertStable();
1262
+ }
1263
+ if (!undoControl) {
1264
+ return { ok: false, attempted: clicks > 0, code: 'editor_undo_control_not_found',
1325
1265
  reason: `没有找到 Overleaf 编辑器自己的 Undo/撤销按钮,无法一次性撤销 ${path} 的本轮留痕。`
1326
1266
  };
1327
1267
  }
1328
1268
 
1329
- const beforeText = readActiveEditorText();
1330
1269
  clickNode(undoControl);
1331
1270
  await waitForEditorTextProgress(beforeText, expectedContent, 1000);
1332
1271
  if (readActiveEditorText() === beforeText) {
1333
- return {
1334
- ok: false,
1335
- attempted: clicks > 0,
1336
- code: 'editor_undo_no_progress',
1272
+ return { ok: false, attempted: clicks > 0, code: 'editor_undo_no_progress',
1337
1273
  reason: `Codex 点击了 Overleaf Undo/撤销,但 ${path} 内容没有变化。`
1338
1274
  };
1339
1275
  }
@@ -1366,7 +1302,9 @@
1366
1302
  }
1367
1303
  const result = await rejectRemainingTrackedChangesForExpectedFile(file, applied);
1368
1304
  if (!result.ok) {
1369
- return result;
1305
+ // The sweep knows its target even when no DOM review node remains.
1306
+ // Keep that scope so independently verified files can leave recovery.
1307
+ return { ...result, file: file.path };
1370
1308
  }
1371
1309
  }
1372
1310
  return { ok: true };
@@ -1387,7 +1325,7 @@
1387
1325
  for (const path of paths) {
1388
1326
  const result = await rejectRemainingTrackedChangesForPath(path, applied);
1389
1327
  if (!result.ok) {
1390
- return result;
1328
+ return { ...result, file: path };
1391
1329
  }
1392
1330
  }
1393
1331
  return { ok: true };
@@ -1408,7 +1346,9 @@
1408
1346
  const appliedCountBefore = applied.length;
1409
1347
  const maxAttempts = 200;
1410
1348
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
1411
- const node = findLastTrackedChangeNodeForPath(path);
1349
+ const selection = await waitForRejectableTrackedChange(path);
1350
+ selection.assertStable();
1351
+ const node = selection.node;
1412
1352
  if (!node) {
1413
1353
  if (applied.length > appliedCountBefore) {
1414
1354
  return { ok: true };
@@ -1420,7 +1360,7 @@
1420
1360
  };
1421
1361
  }
1422
1362
 
1423
- const rejectControl = findRejectControlForTrackedChangeNode(node);
1363
+ const rejectControl = isTrackedChangeNodeInNativeScope(node, path) ? findRejectControlForTrackedChangeNode(node) : null;
1424
1364
  if (!rejectControl) {
1425
1365
  return {
1426
1366
  ok: false,
@@ -1431,6 +1371,7 @@
1431
1371
 
1432
1372
  const trackedChange = trackedChangeRefFromNode(node, path);
1433
1373
  const beforeText = readActiveEditorText();
1374
+ selection.assertStable();
1434
1375
  clickNode(rejectControl);
1435
1376
  await waitForTrackedChangeRejectProgress(trackedChange, path, beforeText, 1200);
1436
1377
  applied.push({
@@ -1461,6 +1402,17 @@
1461
1402
  };
1462
1403
  }
1463
1404
 
1405
+ async function verifyReviewFileContent(file, applied) {
1406
+ const verified = await verifyActiveEditorText(file.content, file.path, 800);
1407
+ if (getActiveFilePath() !== file.path) verified.ok = false;
1408
+ if (verified.ok) for (const entry of applied) {
1409
+ if (entry.trackedChange?.path === file.path && entry.result?.ok === true
1410
+ && ['overleaf-review-reject', 'overleaf-review-reject-sweep'].includes(entry.result.method))
1411
+ Object.assign(entry.result, { verified: true, verifiedContent: file.content });
1412
+ }
1413
+ return verified;
1414
+ }
1415
+
1464
1416
  async function rejectRemainingTrackedChangesForExpectedFile(file, applied) {
1465
1417
  const opened = await openFileByPath(file.path);
1466
1418
  if (!opened.ok) {
@@ -1474,12 +1426,16 @@
1474
1426
  const appliedCountBefore = applied.length;
1475
1427
  const maxAttempts = 200;
1476
1428
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
1477
- const node = findLastTrackedChangeNodeForPath(file.path);
1429
+ if (applied.some(entry => entry.trackedChange?.path === file.path
1430
+ && ['overleaf-review-reject', 'overleaf-review-reject-sweep'].includes(entry.result?.method))
1431
+ && getActiveFilePath() === file.path && readActiveEditorText() === file.content
1432
+ && (await verifyReviewFileContent(file, applied)).ok) return { ok: true };
1433
+ const selection = await waitForRejectableTrackedChange(file.path);
1434
+ selection.assertStable();
1435
+ const node = selection.node;
1478
1436
  if (!node) {
1479
- const verified = await verifyActiveEditorText(file.content, file.path, 800);
1480
- if (verified.ok) {
1481
- return { ok: true };
1482
- }
1437
+ const verified = await verifyReviewFileContent(file, applied);
1438
+ if (verified.ok) return { ok: true };
1483
1439
  const rejectedAnyForFile = applied.length > appliedCountBefore;
1484
1440
  return {
1485
1441
  ...verified,
@@ -1490,7 +1446,7 @@
1490
1446
  };
1491
1447
  }
1492
1448
 
1493
- const rejectControl = findRejectControlForTrackedChangeNode(node);
1449
+ const rejectControl = isTrackedChangeNodeInNativeScope(node, file.path) ? findRejectControlForTrackedChangeNode(node) : null;
1494
1450
  if (!rejectControl) {
1495
1451
  return {
1496
1452
  ok: false,
@@ -1501,6 +1457,7 @@
1501
1457
 
1502
1458
  const trackedChange = trackedChangeRefFromNode(node, file.path);
1503
1459
  const beforeText = readActiveEditorText();
1460
+ selection.assertStable();
1504
1461
  clickNode(rejectControl);
1505
1462
  await waitForTrackedChangeRejectProgress(trackedChange, file.path, beforeText, 1200);
1506
1463
  applied.push({
@@ -1546,13 +1503,10 @@
1546
1503
  return false;
1547
1504
  }
1548
1505
 
1549
-
1550
-
1551
-
1552
1506
  const captureModule = window?.CodexOverleafTrackedChangeCapture
1553
1507
  || (typeof module === 'object' && module.exports ? require('./trackedChangeCapture.js') : null);
1554
1508
  const { collectTrackedChangeNodes, trackedChangeRefFromNode, collectTrackedChangeRefsForPaths,
1555
- waitForTrackedChangeDiff, prepareTrackedChangeCapture, prepareTrackedChangeReview, prepareUntrackedUndo, getTrackedChangeCaptureStatus,
1509
+ waitForTrackedChangeDiff, prepareTrackedChangeCapture, prepareTrackedChangeReview, prepareUntrackedUndo, getTrackedChangeCaptureStatus, isTrackedChangeNodeInNativeScope, rememberTrackedChangeNodeContext,
1556
1510
  captureTrackedWrite, reconcileTrackedChangeCapture } = captureModule.create(deps);
1557
1511
 
1558
1512
  function orderTrackedChangesForReviewAction(refs = []) {
@@ -1575,9 +1529,56 @@
1575
1529
  return nodes[0] || null;
1576
1530
  }
1577
1531
 
1578
- function findLastTrackedChangeNodeForPath(path) {
1579
- const nodes = findTrackedChangeNodesForPath(path);
1580
- return nodes[nodes.length - 1] || null;
1532
+ async function createReviewScopeGuard(path, expectedText) {
1533
+ const owner = nativeRejectPlan?.files?.find(file => file.path === path);
1534
+ const changed = () => {
1535
+ const failure = nativeReviewBlocked(path, 'native_review_scope_changed').skipped[0].result;
1536
+ throw Object.assign(new Error(failure.reason), { reviewFailure: failure });
1537
+ };
1538
+ let snapshot = prepareTrackedChangeCapture(path);
1539
+ const until = Date.now() + 1500;
1540
+ while ((owner || snapshot.source === 'native') && (snapshot.source !== 'native' || !snapshot.ready) && Date.now() < until) {
1541
+ if (getActiveFilePath() !== path || (expectedText !== undefined && readActiveEditorText() !== expectedText)) changed();
1542
+ await delay(Math.min(60, until - Date.now()));
1543
+ snapshot = prepareTrackedChangeCapture(path);
1544
+ }
1545
+ if (owner || snapshot.source === 'native') {
1546
+ const owned = (owner?.scope.refs || []).map(ref => JSON.stringify([ref.id, ref.kind, ref.textHash]));
1547
+ if (!snapshot.ready || !snapshot.refs.length || !owner || snapshot.nativeDocId !== owner.nativeDocId || snapshot.refs.some(ref => {
1548
+ const index = owned.indexOf(JSON.stringify([ref.id, ref.kind, ref.textHash]));
1549
+ if (index < 0) return true;
1550
+ owned.splice(index, 1); return false;
1551
+ })) changed();
1552
+ }
1553
+ const beforeText = expectedText ?? readActiveEditorText();
1554
+ const key = value => value.source !== 'native' ? '' : !value.ready ? null
1555
+ : JSON.stringify([value.nativeDocId, value.refs.map(ref => ref.key).sort()]);
1556
+ const beforeScope = key(snapshot);
1557
+ return () => {
1558
+ if (getActiveFilePath() !== path || readActiveEditorText() !== beforeText
1559
+ || key(prepareTrackedChangeCapture(path)) !== beforeScope) changed();
1560
+ };
1561
+ }
1562
+ async function waitForRejectableTrackedChange(path) {
1563
+ const until = Date.now() + 1500;
1564
+ const select = () => {
1565
+ const nodes = findTrackedChangeNodesForPath(path).reverse();
1566
+ for (const node of nodes) {
1567
+ if (!isTrackedChangeNodeInNativeScope(node, path)) continue;
1568
+ const control = findRejectControlForTrackedChangeNode(node);
1569
+ if (control) return { node, control };
1570
+ }
1571
+ return { node: nodes[0] || null, control: null };
1572
+ };
1573
+ let selection = select();
1574
+ if (!selection.node) return { ...selection, assertStable() {} };
1575
+ const assertStable = await createReviewScopeGuard(path);
1576
+ selection = select();
1577
+ while (!selection.control && Date.now() < until) {
1578
+ assertStable(); await delay(Math.min(60, until - Date.now())); assertStable();
1579
+ selection = select();
1580
+ }
1581
+ return { ...selection, assertStable };
1581
1582
  }
1582
1583
 
1583
1584
  function findTrackedChangeNodesForPath(path) {
@@ -1590,6 +1591,7 @@
1590
1591
  }
1591
1592
 
1592
1593
  function findRejectControlForTrackedChangeNode(node) {
1594
+ if (!node || node.isConnected === false) return null;
1593
1595
  const scopes = [];
1594
1596
  let current = node;
1595
1597
  for (let index = 0; current && index < 6; index += 1) {
@@ -1616,17 +1618,19 @@
1616
1618
  }
1617
1619
 
1618
1620
  function isRejectTrackedChangeControl(node) {
1619
- if (!node || node.disabled) {
1621
+ if (!node || node.disabled || node.isConnected === false) {
1620
1622
  return false;
1621
1623
  }
1622
1624
  if (/^(true|disabled)$/i.test(node.getAttribute?.('aria-disabled') || '')) {
1623
1625
  return false;
1624
1626
  }
1625
- const signal = normalizeReviewingSignalText(readNodeSignalText(node));
1626
- if (/\b(?:accept|approve|apply|resolve|接受|批准|应用)\b/i.test(signal)) {
1627
+ const signal = normalizeReviewingSignalText([readNodeSignalText(node),
1628
+ ...Array.from(node.querySelectorAll?.('.visually-hidden') || [])
1629
+ .map(label => label.textContent || '')].join(' '));
1630
+ if (/\b(?:accept|approve|apply|resolve)\b|接受|批准|应用/i.test(signal)) {
1627
1631
  return false;
1628
1632
  }
1629
- return /\b(?:reject|decline|discard|revert|拒绝|丢弃|还原)\b/i.test(signal);
1633
+ return /\b(?:reject|decline|discard|revert)\b|拒绝|丢弃|还原/i.test(signal);
1630
1634
  }
1631
1635
  return {
1632
1636
  acceptTrackedChanges,