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
@@ -56,7 +56,12 @@
56
56
  'codex.history.clearPlugin',
57
57
  'skills.list',
58
58
  'skills.install',
59
- 'skills.remove'
59
+ 'skills.remove',
60
+ 'writing-style.get',
61
+ 'writing-style.set',
62
+ 'codex.thread.fork',
63
+ 'asset.readChunk',
64
+ 'asset.release'
60
65
  ]);
61
66
  const PageRpcContract = Modules.PageRpcContract;
62
67
  const PANEL_DEFAULT_WIDTH = 380;
@@ -64,7 +69,9 @@
64
69
  const PANEL_MAX_WIDTH = 760;
65
70
  const PAGE_MIN_WIDTH = 520;
66
71
  const CodexOverleafCompatibility = Modules.Compatibility;
67
- const INSTALL_COMMAND = CodexOverleafCompatibility?.buildInstallCommand?.(
72
+ const MANAGED_INSTALLATION = chrome.runtime.getManifest?.()?.background?.service_worker === 'bootstrap/background.js'
73
+ || root.__CODEX_OVERLEAF_RUNTIME_PREFIX__ === 'runtime/';
74
+ const INSTALL_COMMAND = MANAGED_INSTALLATION ? '' : CodexOverleafCompatibility?.buildInstallCommand?.(
68
75
  undefined,
69
76
  undefined,
70
77
  getCurrentExtensionId()
@@ -99,6 +106,7 @@
99
106
  const {
100
107
  buildExpectedFilesAfterOperations,
101
108
  buildSnapshotRestoreUndo,
109
+ buildCreatedFileUndoCheckpoint,
102
110
  buildUndoCheckpoint
103
111
  } = Modules.UndoOperations;
104
112
  const { buildHumanCompletionReport, mapAgentEventToActivity, stripEmptyHtmlCommentPlaceholders,
@@ -206,6 +214,8 @@
206
214
  closeSlashMenu: () => closeSlashMenu(),
207
215
  getCurrentProjectId: () => getCurrentProjectId(),
208
216
  getLocalSkillsPanel: () => localSkillsPanel,
217
+ getCodexLocalSkills: () => getCodexLocalSkillsCatalog(),
218
+ onSkillsChange: () => updateSlashMenuForTaskInput(),
209
219
  getLocale: () => getLocale(),
210
220
  getPanel: () => panel,
211
221
  getPanelRendererInstance: () => panelRendererInstance,
@@ -215,7 +225,7 @@
215
225
  refreshStorageUsageSummary: () => refreshStorageUsageSummary(),
216
226
  renderAuditHistoryPanel: () => renderAuditHistoryPanel(),
217
227
  renderRecentProjectsVariant: (...args) => renderRecentProjectsVariant(...args),
218
- saveStateSoon: () => saveStateSoon(),
228
+ saveStateSoon: delay => saveStateSoon(delay),
219
229
  updateGlobalPreferences: patch => getGlobalPreferences().update(patch),
220
230
  setState: next => { state = next; },
221
231
  tr,
@@ -228,6 +238,9 @@
228
238
  closeCustomInstructionsSettings,
229
239
  closeSkillsView,
230
240
  getCodexOverleafSkillEnabled,
241
+ getCodexLocalSkillEnabled,
242
+ isCodexLocalSkillEnabled,
243
+ setCodexLocalSkillEnabled,
231
244
  getCustomInstructionsForCurrentProject,
232
245
  getGovernanceRulesForCurrentProject,
233
246
  getSkillLoadingSettings,
@@ -334,6 +347,7 @@
334
347
  otWarmMirrorController,
335
348
  getActiveFocusFiles,
336
349
  getCurrentProjectId,
350
+ getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
337
351
  isExperimentalOtEnabled,
338
352
  readOtBridgeStatus,
339
353
  formatOtStatusLabel,
@@ -365,6 +379,9 @@
365
379
  // whose deps reference the recovery handlers.
366
380
  const panelMaintenance = Modules.PanelMaintenance.create({
367
381
  StorageDb: Modules.StorageDb,
382
+ ChangeHistoryView: Modules.ChangeHistoryView,
383
+ getAccountScopeId: () => cachedAccountScopeId || '',
384
+ sanitizeText: sanitizeAssistantVisibleText,
368
385
  tx,
369
386
  showPluginToast: (...args) => showPluginToast(...args),
370
387
  showPluginConfirm: (...args) => showPluginConfirm(...args),
@@ -379,12 +396,17 @@
379
396
  openCustomInstructionsSettings: () => openCustomInstructionsSettings(),
380
397
  onHistoryRowJump: record => jumpToHistoryRun(record),
381
398
  resetProjectNameCache: () => recentProjects.resetProjectNameCache(),
399
+ clearHistoryComposer: () => {
400
+ pendingRetryReplacement = null;
401
+ clearTaskComposer();
402
+ persistComposerAttachmentsToState();
403
+ },
382
404
  getPanel: () => panel,
383
405
  getState: () => state,
384
406
  setState: next => { state = next; },
385
407
  getCurrentRunView: () => currentRunView,
386
408
  getSettingsPanelInstance: () => settingsPanelInstance,
387
- prepareRetryReplacement: run => prepareRetryReplacement(run)
409
+ prepareRetryReplacement: (run, task) => prepareRetryReplacement(run, task)
388
410
  });
389
411
  const {
390
412
  showUndoFileSelection,
@@ -403,8 +425,15 @@
403
425
  SessionState: Modules.SessionState,
404
426
  getState: () => state,
405
427
  setState: next => { state = next; },
406
- sendBackgroundNative,
407
- saveState,
428
+ getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
429
+ sendBackgroundNative, saveState,
430
+ getEnabledCodexLocalSkillIds: () => getEnabledCodexLocalSkillIds(),
431
+ captureLocalSkillIdsForFork: async preferences => {
432
+ if (preferences.loadCodexLocalSkills === false) return [];
433
+ const choices = { ...preferences.codexLocalSkillEnabled };
434
+ const skills = await primeCodexLocalSkillsCatalog({ required: true });
435
+ return skills.filter(skill => Object.hasOwn(choices, skill.id) ? choices[skill.id] !== false : skill.enabled !== false).map(skill => skill.id);
436
+ },
408
437
  applyStateToPanel,
409
438
  showPluginToast,
410
439
  tr
@@ -415,10 +444,22 @@
415
444
  forkRunFromNode: sessionForkController.forkRunFromNode
416
445
  });
417
446
  const runTimelineView = Modules.RunTimelineView.create({
447
+ RunActivitySummary: Modules.RunActivitySummary,
448
+ RunPresence: Modules.RunPresence,
449
+ // recentProjects is composed below; the empty state only reads it after startup.
450
+ getProjectName: () => { try { return recentProjects.readCurrentProjectNameFromDom(); } catch { return ''; } },
451
+ getCurrentProjectId: () => getCurrentProjectId(),
452
+ RunActivityModel: Modules.RunActivityModel,
453
+ SubagentActivityView: Modules.SubagentActivityView,
418
454
  RunGuidanceView: Modules.RunGuidanceView,
455
+ getPendingGuidance: run => runGuidanceController.getPending(run),
419
456
  RunResultActions: runResultActions,
420
457
  RunScrollLayout: Modules.RunScrollLayout,
421
458
  RunFailureNotice: Modules.RunFailureNotice,
459
+ I18n: i18n,
460
+ FailureReasons,
461
+ localizeVisibleReason: value => localizeVisibleReason(value),
462
+ localizeCompileReason: value => Modules.CompileAdapter.localizeCompileReason(value, getLocale()),
422
463
  tr,
423
464
  tx,
424
465
  getLocale,
@@ -428,6 +469,7 @@
428
469
  formatEventDetail,
429
470
  formatEventTime,
430
471
  renderAttachmentPreviewList,
472
+ renderSentSelection: Modules.SelectionContextView.renderSent,
431
473
  formatModeLabel,
432
474
  undoRun,
433
475
  acceptRun,
@@ -443,6 +485,10 @@
443
485
  getState: () => state,
444
486
  getCurrentRunView: () => currentRunView,
445
487
  refillComposerForRetry,
488
+ canEditRun: run => runController.isRetryReplacementEligible(run, WritebackSettlement.projectRunSettlement(run)),
489
+ retryWriteback: retryRunWriteback,
490
+ getCurrentProjectId,
491
+ getAccountScopeId: () => cachedAccountScopeId,
446
492
  showNativeSetupGuidance: () => showNativeUpdateGuidanceModal({}),
447
493
  openProjectFileForFailure,
448
494
  openStorageSettings,
@@ -465,6 +511,8 @@
465
511
  upsertStreamEvent,
466
512
  renderCompletionReport,
467
513
  isTrackedChangeLifecycleRun,
514
+ configureEditButton,
515
+ configureWritebackButton,
468
516
  configureUndoButton,
469
517
  configureAcceptButton
470
518
  } = runTimelineView;
@@ -536,7 +584,88 @@
536
584
  // lives in writebackOrchestrator.js. Every dep below is either a hoisted
537
585
  // function declaration, an already-initialized const above, or a lazy
538
586
  // accessor thunk — safe in the top-of-init() wiring zone.
587
+ const selectionContextView = Modules.SelectionContextView.create({
588
+ getPanel: () => panel,
589
+ getState: () => state,
590
+ getProjectId: getCurrentProjectId,
591
+ openComposer: () => ensurePanelOpen(),
592
+ setSelection: (value, options = {}) => {
593
+ if (options.edit && panel?.querySelector('[data-task]')) readPanelInputs();
594
+ state = updateActiveSession(state, {
595
+ selectionContext: value,
596
+ ...(options.edit ? { mode: 'auto' } : {})
597
+ });
598
+ if (options.edit && panel) {
599
+ const modeInput = panel.querySelector('[data-mode]');
600
+ if (modeInput) modeInput.value = 'auto';
601
+ syncModeControls();
602
+ }
603
+ saveStateSoon();
604
+ },
605
+ callPageBridge, tx, toast: showPluginToast
606
+ });
607
+ selectionContextView.start();
608
+ const writingStyleSettings = Modules.WritingStyleSettings.create({
609
+ tx,
610
+ getScope: () => {
611
+ const projectId = getCurrentProjectId();
612
+ return cachedAccountScopeId && projectId ? { accountScopeId: cachedAccountScopeId, projectId } : null;
613
+ },
614
+ request: sendBackgroundNative,
615
+ getReferenceSnapshot: async (projectId, requestId) => {
616
+ const result = await callPageBridge('getReferenceProjectSnapshot', { projectId, requestId });
617
+ if (!result?.ok) throw Object.assign(new Error(result?.error || result?.reason || tx(
618
+ 'The reference project could not be read.', '参考项目未能读取。')), { code: result?.code });
619
+ return result;
620
+ },
621
+ cancelReferenceRead: async requestId => {
622
+ const result = await callPageBridge('cancelReferenceRead', { requestId });
623
+ if (!result?.ok) throw new Error(result?.error || tx('The reference read could not be cancelled.', '参考资料读取未能取消。'));
624
+ return result;
625
+ },
626
+ isRunning: () => Boolean(currentRunView),
627
+ listProjects: async () => {
628
+ const result = await callPageBridge('listReferenceProjects', {});
629
+ if (!result?.ok) throw new Error(result?.error || tx(
630
+ 'The Overleaf project directory could not be loaded.', 'Overleaf 项目目录未能加载。'));
631
+ return result.projects;
632
+ },
633
+ getModelSettings: async () => {
634
+ await providerSettingsCoordinator.ensureLoaded();
635
+ if (panel?.querySelector('[data-task]')) readPanelInputs();
636
+ return {
637
+ model: state.model,
638
+ reasoningEffort: state.reasoningEffort,
639
+ speedTier: state.speedTier,
640
+ providerSelection: providerSettingsCoordinator.getRunSelection(state.providerId || 'builtin'),
641
+ locale: getLocale()
642
+ };
643
+ },
644
+ preflightProjectReferences: ({ project, assertCurrent }) => {
645
+ assertCurrent();
646
+ return Modules.SensitivePreflight.create({
647
+ SensitiveScan, normalizeGovernanceRules, isTextSnapshotFile, getCurrentProjectId,
648
+ appendRunEvent: () => {}, appendCompletionReport: () => {},
649
+ showPluginConfirm, tr, tx
650
+ }).run({ task: '', project, rules: getGovernanceRulesForCurrentProject(), assertCurrent });
651
+ },
652
+ startBuild: async params => {
653
+ const gate = await nativeCompatibilityController.ensureForMethod('codex.run');
654
+ if (!gate.ok) throw new Error(gate.response?.error?.message || 'Native host update required.');
655
+ const id = crypto.randomUUID();
656
+ const promise = nativeChannel.sendBackgroundNative(nativeCompatibilityController.attachEvidence({
657
+ id, method: 'codex.run', params
658
+ }, gate.compatibility));
659
+ return { id, promise };
660
+ }
661
+ });
539
662
  const assetTransferBroker = Modules.AssetTransferBroker.create({ callPageBridge, sendBackgroundNative });
663
+ const { partitionUnsafeProjectPathOperations, normalizeOperationProjectPaths, getInvalidOperationProjectPath,
664
+ evaluateGovernedOperations, buildGovernanceSkippedApplyResult, filterSyncChangesByOperations, mergeApplyResultSkipped,
665
+ confirmBinaryOperations, buildAuditSummaryFromApply, summarizeOperationForAudit } = Modules.WritebackPlan.create({
666
+ tx, tr, normalizeSafeProjectPath, GovernanceRules, getGovernanceRulesForCurrentProject,
667
+ showPluginConfirm, formatOperationFiles, getAppliedEntries, getSkippedEntries
668
+ });
540
669
  const writebackOrchestrator = Modules.WritebackOrchestrator.create({
541
670
  tr,
542
671
  tx,
@@ -577,6 +706,8 @@
577
706
  filterSyncChangesByOperations,
578
707
  writebackController,
579
708
  assetTransferBroker,
709
+ stageWritebackRecovery,
710
+ settleWritebackRecovery,
580
711
  writebackSettlement: WritebackSettlement,
581
712
  compileAdapter: Modules.CompileAdapter,
582
713
  RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
@@ -674,8 +805,10 @@
674
805
  window,
675
806
  getSettingsPanelInstance: () => settingsPanelInstance,
676
807
  getSelectedModel: () => state?.model || '', getSelectedProviderId: () => state?.providerId || 'builtin',
808
+ getSelectedProviderRevision: () => state?.providerRevision,
677
809
  setSelectedProviderId: projectProviderSelection.commit,
678
810
  confirmProviderSwitch: projectProviderSelection.confirmSwitch,
811
+ assertProviderSwitchAllowed: projectProviderSelection.assertSwitchAllowed,
679
812
  refreshModelOptions: loadModelOptions, persistInputs: persistPanelInputs
680
813
  });
681
814
  // v1.8.1 D3: the dashboard was a one-shot render — another tab finishing a
@@ -757,6 +890,7 @@
757
890
  tr,
758
891
  tx,
759
892
  appendPlainLog,
893
+ onChange: () => getComposerDraftPersistence().changed(),
760
894
  limits: {
761
895
  maxAttachmentBytes: MAX_COMPOSER_ATTACHMENT_BYTES,
762
896
  maxAttachmentTotalBytes: MAX_COMPOSER_ATTACHMENT_TOTAL_BYTES,
@@ -774,6 +908,7 @@
774
908
  getRunEvents: () => currentRunView?.events,
775
909
  appendRunEvent,
776
910
  scrollLogToBottom,
911
+ renderWrittenChanges: changes => Modules.WrittenChangesView.render(changes, { document, tx }),
777
912
  onRejectedHunks: summaries => {
778
913
  if (currentRunView && Array.isArray(summaries) && summaries.length) {
779
914
  currentRunView.rejectedHunks = summaries;
@@ -813,9 +948,15 @@
813
948
  saveState,
814
949
  sendBackgroundNative,
815
950
  getCurrentProjectId,
951
+ getScope: () => getCodexLocalSkillsCatalogScope(),
816
952
  getSkillLoadingSettings,
817
953
  isCodexOverleafSkillEnabled,
818
954
  setCodexOverleafSkillEnabled,
955
+ isCodexLocalSkillEnabled,
956
+ setCodexLocalSkillEnabled,
957
+ getCodexLocalSkills: () => getCodexLocalSkillsCatalog(),
958
+ getCodexLocalSkillWarnings: () => getCodexLocalSkillWarnings(),
959
+ refreshCodexLocalSkills: options => primeCodexLocalSkillsCatalog(options),
819
960
  setProjectSettingsStatus,
820
961
  tr,
821
962
  tx,
@@ -848,6 +989,7 @@
848
989
  let storageKey = LEGACY_STORAGE_KEY;
849
990
  let globalPreferencesController = null;
850
991
  let currentRunView = null;
992
+ let runSubmissionPending = false;
851
993
  let pendingRetryReplacement = null;
852
994
  let runQueueScheduler = null;
853
995
  let runInputCoordinator = null;
@@ -868,6 +1010,7 @@
868
1010
  let pendingStreamRenderEvents = new Map();
869
1011
  let storageNoticeKeys = new Set();
870
1012
  let composerSkillInvocation = null;
1013
+ let composerDraftRevision = 0;
871
1014
  let slashCodexOverleafSkills = [];
872
1015
  let slashCodexOverleafSkillsLoaded = false;
873
1016
  let slashCodexOverleafSkillsLoading = false;
@@ -922,6 +1065,16 @@
922
1065
  rolled_back: tx('The update failed its health check, so the previous version was restored.', '更新未通过健康检查,已自动恢复上一版本。')
923
1066
  }[state] || '')
924
1067
  });
1068
+ const diagnosticsExporter = Modules.DiagnosticsExport.create({
1069
+ root, window, document, Blob, URL, TextEncoder, AuditRecords, StorageDb: Modules.StorageDb,
1070
+ CodexOverleafCompatibility, getState: () => state, getCurrentRunView: () => currentRunView,
1071
+ getAccountScopeId: () => cachedAccountScopeId || '',
1072
+ getCurrentProjectId, findRunRecord, getMirrorFreshness, getExtensionCompatibilityMetadata,
1073
+ getModelDiscovery, getGovernanceRulesForCurrentProject, sendBackgroundNative,
1074
+ fallbackNativeCompatibility, isNativeCompatibilityCompatible, getNativeCompatibilityClassification,
1075
+ callPageBridge, showPluginToast, tr, RUN_SNAPSHOT_ZIP_TIMEOUT_MS
1076
+ });
1077
+
925
1078
  root[RUNTIME_INSTALLED_FLAG] = true;
926
1079
  root[RUNTIME_STATE_KEY] = { ok: true, alreadyInstalled: false };
927
1080
 
@@ -948,7 +1101,6 @@
948
1101
  if (message?.type === 'codex-overleaf/toggle-launcher') {
949
1102
  const visible = !PanelRenderer.isLauncherVisible(panelRendererInstance);
950
1103
  PanelRenderer.setLauncherVisible(panelRendererInstance, visible, { persist: true });
951
- if (!visible) closePanel();
952
1104
  sendResponse?.(getPanelStateResponse());
953
1105
  return;
954
1106
  }
@@ -969,7 +1121,7 @@
969
1121
  }
970
1122
  });
971
1123
 
972
- exposeSmokeHelper();
1124
+ diagnosticsExporter.exposeSmokeHelper();
973
1125
  init().catch(error => {
974
1126
  root[RUNTIME_STATE_KEY] = {
975
1127
  ok: false,
@@ -981,11 +1133,20 @@
981
1133
  throw error;
982
1134
  });
983
1135
 
1136
+ function normalizeLoadedPanelState(input) {
1137
+ // Shared history may belong to a live run in another tab. Loading it is
1138
+ // read-only; only an owner-lost journal may mark that run interrupted.
1139
+ return normalizePanelState(getGlobalPreferences().overlay(input), {
1140
+ restoreRunningRuns: Modules.StorageDb.sharedSessionsEnabled?.() !== true, recoverSettledRuns: true
1141
+ });
1142
+ }
1143
+
984
1144
  async function init() {
985
1145
  await getGlobalPreferences().initialize();
986
1146
  await refreshAccountScopeId();
987
1147
  storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
988
- state = normalizePanelState(getGlobalPreferences().overlay(await loadStoredState()), { restoreRunningRuns: true });
1148
+ state = normalizeLoadedPanelState(await loadStoredState());
1149
+ await getRunReviewController().recoverInterruptedUndoStatuses();
989
1150
  initializeRunQueueScheduler();
990
1151
  recoverInterruptedRunJournals()
991
1152
  .then(() => applyStateToPanel())
@@ -1057,7 +1218,8 @@
1057
1218
  version: CodexOverleafCompatibility?.BUILD_TARGET_VERSION ||
1058
1219
  chrome.runtime.getManifest?.().version ||
1059
1220
  '',
1060
- extensionId: getCurrentExtensionId()
1221
+ extensionId: getCurrentExtensionId(),
1222
+ installIdentity: { managed: MANAGED_INSTALLATION }
1061
1223
  };
1062
1224
  }
1063
1225
 
@@ -1067,129 +1229,9 @@
1067
1229
  : '';
1068
1230
  }
1069
1231
 
1070
- function exposeSmokeHelper() {
1071
- const helper = Object.freeze({
1072
- probeNative: smokeProbeNative,
1073
- probeProject: smokeProbeProject,
1074
- getProjectSnapshotMetrics: smokeProbeProject
1075
- });
1076
- try {
1077
- Object.defineProperty(globalThis, 'CodexOverleafSmoke', {
1078
- configurable: true,
1079
- enumerable: false,
1080
- value: helper
1081
- });
1082
- } catch (_error) {
1083
- globalThis.CodexOverleafSmoke = helper;
1084
- }
1085
- }
1086
-
1087
- async function smokeProbeNative() {
1088
- try {
1089
- const params = CodexOverleafCompatibility?.buildBridgePingParams
1090
- ? CodexOverleafCompatibility.buildBridgePingParams(getExtensionCompatibilityMetadata())
1091
- : {};
1092
- const response = await sendBackgroundNative({ method: 'bridge.ping', params });
1093
- const compatibility = CodexOverleafCompatibility?.evaluateNativeCompatibility
1094
- ? CodexOverleafCompatibility.evaluateNativeCompatibility(response, getExtensionCompatibilityMetadata())
1095
- : fallbackNativeCompatibility(response);
1096
- return {
1097
- supported: true,
1098
- ok: response?.ok === true && isNativeCompatibilityCompatible(compatibility),
1099
- status: compatibility?.status || (response?.ok ? 'ok' : 'native_missing'),
1100
- classification: getNativeCompatibilityClassification(compatibility),
1101
- errorCode: response?.error?.code || compatibility?.status || '',
1102
- nativeCompatibility: summarizeSmokeNativeCompatibility(compatibility, response)
1103
- };
1104
- } catch (_error) {
1105
- return {
1106
- supported: true,
1107
- ok: false,
1108
- status: 'native_probe_failed',
1109
- errorCode: 'native_probe_failed'
1110
- };
1111
- }
1112
- }
1113
1232
 
1114
- async function smokeProbeProject(options = {}) {
1115
- try {
1116
- const project = await callPageBridge('getProjectSnapshot', {
1117
- force: Boolean(options.force),
1118
- preferLightweight: true,
1119
- allowZipFallback: true,
1120
- allowEditorNavigation: false,
1121
- requireFullProject: false,
1122
- includeBinaryFiles: true,
1123
- includeContent: false,
1124
- zipTimeoutMs: RUN_SNAPSHOT_ZIP_TIMEOUT_MS
1125
- });
1126
- const files = Array.isArray(project?.files) ? project.files : [];
1127
- const skipped = Array.isArray(project?.capabilities?.skipped) ? project.capabilities.skipped : [];
1128
- const bytes = summarizeSmokeProjectBytes(files);
1129
- const ok = project?.ok !== false && files.length > 0;
1130
- return {
1131
- supported: true,
1132
- ok,
1133
- status: ok ? 'ok' : 'project_snapshot_unavailable',
1134
- errorCode: ok ? '' : project?.code || 'project_snapshot_unavailable',
1135
- counts: {
1136
- files: files.length,
1137
- skipped: skipped.length
1138
- },
1139
- bytes,
1140
- method: project?.capabilities?.method || ''
1141
- };
1142
- } catch (_error) {
1143
- return {
1144
- supported: true,
1145
- ok: false,
1146
- status: 'project_probe_failed',
1147
- errorCode: 'project_probe_failed',
1148
- counts: {
1149
- files: 0,
1150
- skipped: 0
1151
- },
1152
- bytes: {
1153
- text: 0,
1154
- binary: 0
1155
- }
1156
- };
1157
- }
1158
- }
1159
1233
 
1160
- function summarizeSmokeNativeCompatibility(compatibility = {}, response = {}) {
1161
- const native = compatibility?.native || response?.result || {};
1162
- return {
1163
- status: compatibility?.status || (response?.ok ? 'ok' : 'native_missing'),
1164
- classification: getNativeCompatibilityClassification(compatibility),
1165
- nativeVersion: compatibility?.nativeVersion || native.version || '',
1166
- version: compatibility?.version || native.version || '',
1167
- minimumNativeVersion: compatibility?.minimumNativeVersion || compatibility?.minNativeVersion || '',
1168
- protocolVersion: compatibility?.protocolVersion || native.protocolVersion || '',
1169
- supportedProtocol: compatibility?.supportedProtocol || native.supportedProtocol || ''
1170
- };
1171
- }
1172
1234
 
1173
- function summarizeSmokeProjectBytes(files = []) {
1174
- return files.reduce((bytes, file) => {
1175
- let size = Number(file?.size || file?.byteLength || 0);
1176
- if ((!Number.isFinite(size) || size <= 0) && typeof file?.content === 'string') {
1177
- size = new TextEncoder().encode(file.content).byteLength;
1178
- }
1179
- if (!Number.isFinite(size) || size <= 0) {
1180
- return bytes;
1181
- }
1182
- if (file?.kind === 'binary') {
1183
- bytes.binary += size;
1184
- } else {
1185
- bytes.text += size;
1186
- }
1187
- return bytes;
1188
- }, {
1189
- text: 0,
1190
- binary: 0
1191
- });
1192
- }
1193
1235
 
1194
1236
  function listSeparator() {
1195
1237
  return getLocale() === 'zh' ? '、' : ', ';
@@ -1209,7 +1251,6 @@
1209
1251
  callbacks: {
1210
1252
  onLauncherToggle: () => ensurePanelOpen(),
1211
1253
  onClosePanel: () => closePanel(),
1212
- onLauncherVisibilityChange: visible => { if (!visible) closePanel(); },
1213
1254
  onRefresh: () => refreshProbe({ userInitiated: true }),
1214
1255
  onNewSession: () => startNewSession(),
1215
1256
  onChangeHistory: () => openChangeHistory(),
@@ -1285,6 +1326,7 @@
1285
1326
  onOtToggleClick: handleExperimentalOtToggleClick
1286
1327
  }
1287
1328
  });
1329
+ writingStyleSettings.mount(panel);
1288
1330
  providerSettingsCoordinator.refreshSummary().catch(() => {});
1289
1331
 
1290
1332
  sessionPanelInstance = SessionPanel.create({
@@ -1321,6 +1363,7 @@
1321
1363
  onTaskKeydown: handleTaskInputKeydown,
1322
1364
  onTaskInput: handleTaskInput,
1323
1365
  onSlashMenuClick: handleSlashMenuClick,
1366
+ onMenuLayoutChange: updateSlashMenuLayout,
1324
1367
  onClearSkillInvocation: clearComposerSkillInvocation,
1325
1368
  onAddContext: toggleContextTray,
1326
1369
  onAttachClick: () => panel.querySelector('[data-attach-input]')?.click(),
@@ -1347,34 +1390,87 @@
1347
1390
  renderContextSelection();
1348
1391
  scheduleMirrorPrefetch({ reason: 'cold-start', delayMs: 2500 });
1349
1392
  primeCodexOverleafSkillsCatalog().catch(() => { /* non-blocking */ });
1393
+ primeCodexLocalSkillsCatalog().catch(() => { /* retry through settings, slash, or submission */ });
1350
1394
  }
1351
1395
 
1352
1396
  PanelRenderer.setVisible(panel, true);
1353
1397
  }
1354
1398
 
1355
- // One-shot catalog prime: the parallel-subagents broker gate reads
1356
- // state.codexOverleafSkills at run time. The catalog persists once filled
1357
- // (v1.6.1), but states saved by older builds have it stripped — without
1358
- // this prime, the gate stays silently off until the user happens to open
1359
- // the Skills page. Fire-and-forget on panel open; only fetches when the
1360
- // catalog is empty.
1361
- async function primeCodexOverleafSkillsCatalog() {
1399
+ // Panel prefetch and first submission share the same official catalog read.
1400
+ // A required read fails through the existing task-start error presentation.
1401
+ let codexOverleafSkillsCatalogPromise = null;
1402
+ async function primeCodexOverleafSkillsCatalog({ required = false } = {}) {
1362
1403
  if (Array.isArray(state?.codexOverleafSkills) && state.codexOverleafSkills.length) {
1363
- return;
1364
- }
1365
- if (getSkillLoadingSettings().loadCodexOverleafSkills === false) {
1366
- return;
1367
- }
1368
- const response = await sendBackgroundNative({
1369
- method: 'skills.list',
1370
- params: { scope: 'codex-overleaf' }
1404
+ return state.codexOverleafSkills;
1405
+ }
1406
+ if (!required && getSkillLoadingSettings().loadCodexOverleafSkills === false) return [];
1407
+ if (!codexOverleafSkillsCatalogPromise) {
1408
+ codexOverleafSkillsCatalogPromise = sendBackgroundNative({
1409
+ method: 'skills.list', params: { scope: 'codex-overleaf' }
1410
+ }).then(response => {
1411
+ const skills = Array.isArray(response?.result?.skills) ? response.result.skills : [];
1412
+ if (!response?.ok || !skills.length) throw new Error(tx(
1413
+ 'Could not load Codex Overleaf skills. Try again.', '无法加载 Codex Overleaf 技能,请重试。'));
1414
+ state = { ...state, codexOverleafSkills: skills };
1415
+ saveStateSoon();
1416
+ return skills;
1417
+ }).finally(() => { codexOverleafSkillsCatalogPromise = null; });
1418
+ }
1419
+ return codexOverleafSkillsCatalogPromise;
1420
+ }
1421
+
1422
+ // Local metadata stays in memory. A stale page/account read cannot become the
1423
+ // next page's selected catalog, and an empty successful directory is valid.
1424
+ let codexLocalSkillsCatalog = null;
1425
+ let codexLocalSkillsWarnings = [];
1426
+ let codexLocalSkillsCatalogScope = '';
1427
+ let codexLocalSkillsCatalogRequest = null;
1428
+ function getCodexLocalSkillsCatalogScope() {
1429
+ return [cachedAccountScopeId, getCurrentProjectId(), spaRouteGeneration].join(':');
1430
+ }
1431
+
1432
+ function getCodexLocalSkillsCatalog() {
1433
+ return codexLocalSkillsCatalogScope === getCodexLocalSkillsCatalogScope() ? codexLocalSkillsCatalog : null;
1434
+ }
1435
+
1436
+ function getCodexLocalSkillWarnings() {
1437
+ return codexLocalSkillsCatalogScope === getCodexLocalSkillsCatalogScope() ? codexLocalSkillsWarnings : [];
1438
+ }
1439
+
1440
+ async function primeCodexLocalSkillsCatalog({ required = false, force = false } = {}) {
1441
+ const scope = getCodexLocalSkillsCatalogScope();
1442
+ if (!required && !force && getSkillLoadingSettings().loadCodexLocalSkills === false) return [];
1443
+ if (!force && codexLocalSkillsCatalogScope === scope && Array.isArray(codexLocalSkillsCatalog)) return codexLocalSkillsCatalog;
1444
+ if (codexLocalSkillsCatalogRequest?.scope === scope) return codexLocalSkillsCatalogRequest.promise;
1445
+ const request = { scope, promise: null };
1446
+ request.promise = sendBackgroundNative({ method: 'skills.list', params: { scope: 'codex-local' } }).then(response => {
1447
+ if (!response?.ok || !Array.isArray(response.result?.skills)) throw new Error(tx(
1448
+ 'Could not load local Codex skills. Try again.', '无法加载 Codex 本地技能,请重试。'));
1449
+ if (scope !== getCodexLocalSkillsCatalogScope()) throw new Error(tx(
1450
+ 'The active project changed while loading skills. Try again.', '加载技能时活动项目已改变,请重试。'));
1451
+ const seen = new Set();
1452
+ const skills = response.result.skills.filter(skill => {
1453
+ const id = String(skill?.id || '');
1454
+ if (!isSafeSkillId(id) || seen.has(id)) return false;
1455
+ seen.add(id); return true;
1456
+ });
1457
+ codexLocalSkillsCatalog = skills;
1458
+ codexLocalSkillsWarnings = (Array.isArray(response.result.discoveryWarnings) ? response.result.discoveryWarnings : [])
1459
+ .filter(warning => warning?.code === 'codex_local_skills_skipped' && Number.isSafeInteger(warning.count) && warning.count > 0)
1460
+ .map(warning => ({ code: warning.code, count: warning.count }));
1461
+ codexLocalSkillsCatalogScope = scope;
1462
+ updateSkillsEntrySummary();
1463
+ updateSlashMenuForTaskInput();
1464
+ return skills;
1465
+ }).catch(() => {
1466
+ if (scope !== getCodexLocalSkillsCatalogScope()) throw new Error(tx(
1467
+ 'The active project changed while loading skills. Try again.', '加载技能时活动项目已改变,请重试。'));
1468
+ throw new Error(tx('Could not load local Codex skills. Try again.', '无法加载 Codex 本地技能,请重试。'));
1469
+ }).finally(() => {
1470
+ if (codexLocalSkillsCatalogRequest === request) codexLocalSkillsCatalogRequest = null;
1371
1471
  });
1372
- const skills = Array.isArray(response?.result?.skills) ? response.result.skills : [];
1373
- if (!response?.ok || !skills.length) {
1374
- return;
1375
- }
1376
- state = { ...state, codexOverleafSkills: skills };
1377
- saveStateSoon();
1472
+ codexLocalSkillsCatalogRequest = request;
1473
+ return request.promise;
1378
1474
  }
1379
1475
 
1380
1476
  function closePanel() {
@@ -1457,6 +1553,10 @@
1457
1553
  panel.querySelectorAll('[data-i18n]').forEach(element => {
1458
1554
  element.textContent = tr(element.dataset.i18n);
1459
1555
  });
1556
+ panel.querySelectorAll('[data-i18n-aria-label]').forEach(element => {
1557
+ element.setAttribute('aria-label', tr(element.dataset.i18nAriaLabel));
1558
+ });
1559
+ panelRendererInstance?.setLocale?.({ launcher: tx('Show or hide Codex panel', '显示或隐藏 Codex 边栏'), close: tx('Close Codex panel', '关闭 Codex 边栏') });
1460
1560
 
1461
1561
  setElementTitleAndAria('[data-panel-resize-handle]', tr('resizePanel'), tr('resizePanel'));
1462
1562
  setElementTitleAndAria('[data-refresh]', tr('refreshProbe'), tr('refreshProbe'));
@@ -1476,7 +1576,7 @@
1476
1576
  setElementTitleAndAria('[data-context-refresh]', tr('refreshFileList'), tr('refreshFileList'));
1477
1577
  setElementTitleAndAria('[data-reasoning]', tr('reasoningLabel'), tr('reasoningLabel'));
1478
1578
  setElementTitleAndAria('[data-speed]', tr('speedLabel'), tr('speedLabel'));
1479
- setElementTitleAndAria('[data-run]', currentRunView ? tr('queueNextInput') : tr('send'), currentRunView ? tr('queueNextInput') : tr('send'));
1579
+ syncComposerSendAvailability();
1480
1580
  setElementTitleAndAria('[data-stop-run]', tr('cancelRun'), tr('cancelRun'));
1481
1581
 
1482
1582
  const actions = panel.querySelector('.codex-vscode-head-actions');
@@ -1525,10 +1625,8 @@
1525
1625
  if (contextStatus && !contextStatus.dataset.customStatus) {
1526
1626
  contextStatus.textContent = tr('contextStatus');
1527
1627
  }
1528
- const emptyRunLabel = panel.querySelector('.empty-runs div');
1529
- if (emptyRunLabel) {
1530
- emptyRunLabel.textContent = tr('emptyRunLabel');
1531
- }
1628
+ // Re-render rather than patch text: the first div is the animated mark.
1629
+ if (arguments[0]?.reprojectDynamic !== false) runTimelineView.refreshLocale?.();
1532
1630
 
1533
1631
  syncModeControls();
1534
1632
  updateOtStatusDisplay();
@@ -1537,16 +1635,19 @@
1537
1635
  updateModelDisplay();
1538
1636
  renderSessionList();
1539
1637
  renderContextSelection();
1540
- const probeStatus = panel.querySelector('[data-probe-status]');
1541
- if (probeStatus?.dataset.refreshing === 'true') probeStatus.textContent = tr('refreshProbeLoading');
1542
- else if (probeStatus && probeStatusSnapshot?.projectId === getCurrentProjectId()) {
1543
- const { failed, probe, userInitiated } = probeStatusSnapshot;
1544
- const text = failed ? tr('refreshProbeFailed') : formatProbeStatusBar(probe);
1545
- probeStatus.textContent = userInitiated && !failed ? tr('refreshProbeDone', { status: text }) : text;
1546
- if (!failed) updateExistingProbeNotice(probe);
1547
- }
1638
+ renderContextSummary();
1639
+ renderPendingInputs();
1640
+ renderComposerAttachments();
1641
+ renderComposerSkillInvocation();
1642
+ writingStyleSettings.sync();
1548
1643
  updateSkillsEntrySummary();
1644
+ localSkillsPanel.refreshLocale?.();
1549
1645
  providerSettingsCoordinator.renderSummary();
1646
+ providerSettingsCoordinator.refreshLocale?.();
1647
+ settingsPanelInstance?.refreshLocale?.();
1648
+ Modules.UpdateNotice?.refreshLocale?.();
1649
+ DiagnosticsPanel.refreshLocale?.(diagnosticsPanelInstance, diagnosticsController.refreshLocale?.());
1650
+ if (!panel.querySelector('[data-context-tray]')?.hidden) renderContextFiles();
1550
1651
  if (settingsPanelInstance?.container?.querySelector('[data-storage-card]')?.open) refreshStorageUsageSummary();
1551
1652
  }
1552
1653
 
@@ -1603,88 +1704,14 @@
1603
1704
  }
1604
1705
 
1605
1706
 
1606
- async function exportDiagnosticsBundle() {
1607
- if (!AuditRecords?.buildDiagnosticBundle) {
1608
- throw new Error('Audit diagnostics helper is unavailable');
1609
- }
1610
- const [auditLogs, mirror, nativeDiagnostics] = await Promise.all([
1611
- getRecentAuditLogsForCurrentProject(),
1612
- getMirrorFreshness().catch(error => ({ status: 'unavailable', errorCode: error.message })),
1613
- getNativeDiagnosticsSummaryForBundle()
1614
- ]);
1615
- const bundle = AuditRecords.buildDiagnosticBundle({
1616
- excludeContent: true,
1617
- compatibility: {
1618
- extension: getExtensionCompatibilityMetadata(),
1619
- modelDiscovery: getModelDiscovery()
1620
- },
1621
- platform: nativeDiagnostics.platform,
1622
- nativeEnvironment: nativeDiagnostics.nativeEnvironment,
1623
- mirror,
1624
- auditLogs,
1625
- run: currentRunView ? {
1626
- id: currentRunView.recordId || '',
1627
- status: 'running'
1628
- } : {},
1629
- governance: getGovernanceRulesForCurrentProject(),
1630
- projectId: getCurrentProjectId()
1631
- });
1632
- const blob = new Blob([JSON.stringify(bundle, null, 2)], { type: 'application/json' });
1633
- const url = URL.createObjectURL(blob);
1634
- const link = document.createElement('a');
1635
- link.href = url;
1636
- link.download = `codex-overleaf-diagnostics-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
1637
- document.body.append(link);
1638
- link.click();
1639
- link.remove();
1640
- window.setTimeout(() => URL.revokeObjectURL(url), 1000);
1641
- showPluginToast(tr('diagnosticsExportDone'), { status: 'completed' });
1642
- }
1643
-
1644
- async function getNativeDiagnosticsSummaryForBundle() {
1645
- try {
1646
- const params = CodexOverleafCompatibility?.buildBridgePingParams
1647
- ? CodexOverleafCompatibility.buildBridgePingParams(getExtensionCompatibilityMetadata())
1648
- : {};
1649
- const response = await sendBackgroundNative({ method: 'bridge.ping', params });
1650
- if (!response?.ok) {
1651
- const errorCode = response?.error?.code || 'native_unavailable';
1652
- return {
1653
- platform: { status: 'unavailable', errorCode },
1654
- nativeEnvironment: { status: 'unavailable', errorCode }
1655
- };
1656
- }
1657
- return {
1658
- platform: {
1659
- host: response.result?.host || '',
1660
- platform: response.result?.platform || '',
1661
- version: response.result?.version || '',
1662
- protocolVersion: response.result?.protocolVersion || ''
1663
- },
1664
- nativeEnvironment: response.result?.environment || {}
1665
- };
1666
- } catch (error) {
1667
- const errorCode = error?.message || 'native_unavailable';
1668
- return {
1669
- platform: { status: 'unavailable', errorCode },
1670
- nativeEnvironment: { status: 'unavailable', errorCode }
1671
- };
1672
- }
1673
- }
1674
1707
 
1675
- async function getRecentAuditLogsForCurrentProject(limit = 12) {
1676
- const StorageDb = Modules.StorageDb;
1677
- if (!StorageDb?.getAllByIndex) {
1678
- return [];
1679
- }
1680
- const records = await StorageDb.getAllByIndex('auditLogs', 'projectId', getCurrentProjectId());
1681
- return (records || [])
1682
- .slice()
1683
- .sort((left, right) => String(right.createdAt || '').localeCompare(String(left.createdAt || '')))
1684
- .slice(0, limit);
1685
- }
1686
1708
 
1687
1709
 
1710
+
1711
+ function exportDiagnosticsBundle() {
1712
+ return diagnosticsExporter.exportDiagnosticsBundle();
1713
+ }
1714
+
1688
1715
  function isContextTrayClickTarget(target) {
1689
1716
  return contextTrayController.isContextTrayClickTarget(target);
1690
1717
  }
@@ -1780,15 +1807,7 @@
1780
1807
  function resetContextProject() { return contextTrayController.resetContextProject(); }
1781
1808
 
1782
1809
  async function runTask(options = {}) {
1783
- // Barrier on a still-flying background mirror refresh from the previous
1784
- // turn (v1.7.5) BEFORE startRunView creates the new run view: the mirror's
1785
- // wrap-up events must land in the run that produced them, not the new
1786
- // one, and mirror.sync landing mid-run would swap the local baseline
1787
- // under Codex.
1788
- const pendingMirror = getPendingMirrorRefresh?.();
1789
- if (pendingMirror) {
1790
- await pendingMirror.catch(() => {});
1791
- }
1810
+ // Capture the click before any preparation can yield to another draft/session.
1792
1811
  readPanelInputs();
1793
1812
  const queuedInput = options.queuedInput || null;
1794
1813
  const task = String(queuedInput?.text || state.task || '').trim();
@@ -1796,6 +1815,12 @@
1796
1815
  appendLog(tx('Enter a task first.', '请先输入任务。'));
1797
1816
  return;
1798
1817
  }
1818
+ const submittedSessionId = state.activeSessionId;
1819
+ const submittedSession = runController.captureSessionContext(state);
1820
+ const submittedThreadId = getActiveSession(state)?.codexThreadId || '';
1821
+ const submittedDraft = state.task;
1822
+ const submittedRetryReplacement = pendingRetryReplacement;
1823
+ const submittedWritingStyleScope = { accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId() };
1799
1824
  const submittedPanelState = {
1800
1825
  mode: state.mode,
1801
1826
  providerId: state.providerId,
@@ -1804,13 +1829,50 @@
1804
1829
  speedTier: state.speedTier,
1805
1830
  autoRecompile: state.autoRecompile !== false,
1806
1831
  requireReviewing: state.requireReviewing === true,
1807
- focusFiles: getActiveFocusFiles()
1832
+ focusFiles: getActiveFocusFiles(),
1833
+ selectionContext: selectionContextView.capture()
1808
1834
  };
1809
- const submittedCustomInstructions = getCustomInstructionsForCurrentProject();
1810
- const submittedSkillLoadingSettings = getSkillLoadingSettings();
1811
- const submittedAttachments = getComposerAttachmentsForRun();
1812
- const submittedSkillInvocation = getComposerSkillInvocationForRun();
1835
+ const queuedContext = queuedInput
1836
+ ? RunInputQueue.normalizeSubmissionContext(queuedInput.payload?.submissionContext) : null;
1837
+ const submittedCustomInstructions = queuedContext ? queuedContext.customInstructions : getCustomInstructionsForCurrentProject();
1838
+ const submittedSkillLoadingSettings = queuedContext ? queuedContext.skillLoadingSettings : getSkillLoadingSettings();
1839
+ const submittedAttachments = queuedInput ? [] : getComposerAttachmentsForRun();
1840
+ const submittedSkillInvocation = queuedContext ? queuedContext.skillInvocation : getComposerSkillInvocationForRun();
1841
+ let submittedEnabledSkillIds = queuedContext ? queuedContext.enabledCodexOverleafSkillIds : getEnabledCodexOverleafSkillIds();
1842
+ const submittedSkillEnabled = { ...state.codexOverleafSkillEnabled };
1843
+ const submittedLocalSkillEnabled = { ...state.codexLocalSkillEnabled };
1844
+ const submittedSelectionRevision = selectionContextView.getRevision?.();
1845
+ let submittedEnabledLocalSkillIds = queuedContext ? queuedContext.enabledCodexLocalSkillIds : [];
1846
+ if (!queuedInput && submittedSkillLoadingSettings.loadCodexLocalSkills !== false) {
1847
+ const localSkills = await primeCodexLocalSkillsCatalog({ required: true, force: submittedSkillInvocation?.scope === 'codex-local' });
1848
+ submittedEnabledLocalSkillIds = localSkills.filter(skill => Object.hasOwn(submittedLocalSkillEnabled, skill.id)
1849
+ ? submittedLocalSkillEnabled[skill.id] !== false : skill.enabled !== false).map(skill => skill.id);
1850
+ }
1851
+ if (!queuedInput && submittedSkillInvocation?.scope === 'codex-local'
1852
+ && (submittedSkillLoadingSettings.loadCodexLocalSkills === false || !submittedEnabledLocalSkillIds.includes(submittedSkillInvocation.id))) {
1853
+ throw new Error(tx('The selected local Codex skill is unavailable or disabled. Select an enabled skill and try again.',
1854
+ '所选 Codex 本地技能不可用或已关闭,请选择已启用的技能后重试。'));
1855
+ }
1856
+ if (!queuedInput && submittedSkillLoadingSettings.loadCodexOverleafSkills !== false) {
1857
+ const skills = await primeCodexOverleafSkillsCatalog({ required: true });
1858
+ submittedEnabledSkillIds = skills.map(skill => String(skill?.id || '').trim())
1859
+ .filter(id => id && submittedSkillEnabled[id] !== false);
1860
+ }
1861
+ // Keep the previous mirror's completion events on its own run.
1862
+ const pendingMirror = getPendingMirrorRefresh?.();
1863
+ if (pendingMirror) await pendingMirror.catch(() => {});
1813
1864
  await providerSettingsCoordinator.ensureLoaded();
1865
+ if (!queuedInput) await writingStyleSettings.ensureLoaded(true);
1866
+ if (submittedWritingStyleScope.accountScopeId !== cachedAccountScopeId
1867
+ || submittedWritingStyleScope.projectId !== getCurrentProjectId()) {
1868
+ throw new Error(tx('The active project changed before submission. Submit the task again.',
1869
+ '提交前活动项目已改变,请重新提交任务。'));
1870
+ }
1871
+ if (!findSessionById(submittedSessionId) || currentRunView) {
1872
+ throw new Error(tx('The submitted session changed before submission. Submit the task again.',
1873
+ '提交所属会话已改变,请重新提交任务。'));
1874
+ }
1875
+ submittedPanelState.writingStyle = queuedInput ? null : writingStyleSettings.capture();
1814
1876
  const executionSnapshot = queuedInput
1815
1877
  ? RunExecutionSnapshot.resolveForExecution(
1816
1878
  RunExecutionSnapshot.fromQueuePayload(queuedInput.payload),
@@ -1821,9 +1883,9 @@
1821
1883
  const submittedMode = executionSnapshot.mode;
1822
1884
  const submittedRequireReviewing = executionSnapshot.requireReviewing === true;
1823
1885
  const replaceRunId = queuedInput ? '' : runController.resolveRetryReplacement({
1824
- candidate: pendingRetryReplacement,
1825
- activeSessionId: state.activeSessionId,
1826
- runs: state.runs,
1886
+ candidate: submittedRetryReplacement,
1887
+ activeSessionId: submittedSessionId,
1888
+ runs: findSessionById(submittedSessionId)?.runs,
1827
1889
  projectRunSettlement: run => WritebackSettlement.projectRunSettlement(run)
1828
1890
  });
1829
1891
 
@@ -1831,6 +1893,9 @@
1831
1893
  runCancellationController = new AbortController();
1832
1894
  setRunning(true);
1833
1895
  currentRunView = startRunView({
1896
+ sessionId: submittedSessionId,
1897
+ sessionContext: submittedSession,
1898
+ codexThreadId: submittedThreadId,
1834
1899
  task,
1835
1900
  mode: submittedMode,
1836
1901
  model: executionSnapshot.model,
@@ -1842,9 +1907,10 @@
1842
1907
  queueItemId: queuedInput?.id || '',
1843
1908
  replaceRunId
1844
1909
  });
1845
- if (!queuedInput) pendingRetryReplacement = null;
1910
+ if (!queuedInput && pendingRetryReplacement === submittedRetryReplacement) pendingRetryReplacement = null;
1846
1911
  const runSessionId = currentRunView.sessionId;
1847
1912
  let runAuditDraft = null;
1913
+ let localSkillRestorePoint = null;
1848
1914
  try {
1849
1915
  if (queuedInput?.id) {
1850
1916
  await runQueueScheduler?.markExecuting(runSessionId, queuedInput.id, currentRunView.recordId);
@@ -1856,14 +1922,32 @@
1856
1922
  focusFiles: executionSnapshot.focusFiles,
1857
1923
  executionSnapshot
1858
1924
  }));
1925
+ if (submittedWritingStyleScope.accountScopeId !== cachedAccountScopeId
1926
+ || submittedWritingStyleScope.projectId !== getCurrentProjectId()) {
1927
+ throw Object.assign(createRunCancellationError(), {
1928
+ message: 'The active project or account changed before dispatch.'
1929
+ });
1930
+ }
1859
1931
  announceCrossTabRunStart();
1860
1932
  // Binary attachments belong to this submitted turn. The run record has
1861
1933
  // already captured their snapshots, so clear them from the composer
1862
1934
  // together with the submitted text. Persistent @context remains intact.
1863
- clearTaskComposer({ keepAttachments: false });
1935
+ const submittedComposerCleared = !queuedInput && clearTaskComposer({ keepAttachments: false, submission: {
1936
+ ...submittedWritingStyleScope,
1937
+ sessionId: submittedSessionId, task: submittedDraft, attachments: submittedAttachments,
1938
+ skillInvocation: submittedSkillInvocation, selectionContext: submittedPanelState.selectionContext,
1939
+ selectionRevision: submittedSelectionRevision
1940
+ } });
1941
+ if (submittedComposerCleared && submittedSkillInvocation?.scope === 'codex-local'
1942
+ && state.activeSessionId === submittedSessionId && !panel?.querySelector('[data-task]')?.value
1943
+ && !getComposerAttachmentsForRun().length && !composerAttachmentController.getAttachmentByteUsage?.().pending
1944
+ && !getComposerSkillInvocationForRun() && !selectionContextView.capture()) {
1945
+ localSkillRestorePoint = { draftRevision: composerDraftRevision,
1946
+ attachmentRevision: composerAttachmentController.getRevision?.(), selectionRevision: selectionContextView.getRevision?.() };
1947
+ }
1864
1948
  syncComposerSendAvailability(true);
1865
1949
  appendRunEvent({
1866
- title: submittedSkillInvocation?.id === 'skill-installer'
1950
+ title: submittedSkillInvocation?.id === 'skill-installer' && !submittedSkillInvocation.scope
1867
1951
  ? tx('I will use the Codex skill installer for this request.', '我会用 Codex skill installer 处理这个请求。')
1868
1952
  : tx('I will first understand your request, then inspect the relevant Overleaf files.', '我会先理解你的请求,再检查相关 Overleaf 文件。'),
1869
1953
  status: 'running',
@@ -1878,7 +1962,7 @@
1878
1962
  '@context': formatContextItems(executionSnapshot.focusFiles)
1879
1963
  }
1880
1964
  });
1881
- if (submittedSkillInvocation?.id === 'skill-installer') {
1965
+ if (submittedSkillInvocation?.id === 'skill-installer' && !submittedSkillInvocation.scope) {
1882
1966
  await awaitRunStep(runSkillInstallerTask({
1883
1967
  task,
1884
1968
  runSessionId,
@@ -1886,6 +1970,8 @@
1886
1970
  submittedMode,
1887
1971
  submittedCustomInstructions,
1888
1972
  submittedSkillLoadingSettings,
1973
+ submittedEnabledLocalSkillIds,
1974
+ submittedEnabledSkillIds,
1889
1975
  submittedAttachments,
1890
1976
  submittedSkillInvocation,
1891
1977
  executionSnapshot
@@ -1946,6 +2032,7 @@
1946
2032
  project,
1947
2033
  snapshotWarnings,
1948
2034
  focusFiles,
2035
+ focusFilesAreLiteral: true,
1949
2036
  isUsableProjectFileContent: Modules.ProjectFiles.isUsableProjectFileContent
1950
2037
  });
1951
2038
  let restrictToFocusFiles = runController.shouldRestrictWritebackToFocus({ focusFiles, focusedPartialSnapshot });
@@ -2014,8 +2101,7 @@
2014
2101
  }
2015
2102
  }
2016
2103
 
2017
- const activeSession = findSessionById(runSessionId) || getActiveSession(state);
2018
- const codexThreadId = activeSession?.codexThreadId || '';
2104
+ const codexThreadId = submittedThreadId;
2019
2105
 
2020
2106
  // Resolve @compile-log if mentioned in task
2021
2107
  let compileLogContext = null;
@@ -2043,14 +2129,14 @@
2043
2129
  }
2044
2130
  });
2045
2131
  appendRunEvent({
2046
- title: tx(`Compile log unavailable: ${compileLogContext.reason}`, `编译日志不可用:${compileLogContext.reason}`),
2132
+ title: tx(`Compile log unavailable: ${Modules.CompileAdapter.localizeCompileReason(compileLogContext.reason, getLocale())}`, `编译日志不可用:${Modules.CompileAdapter.localizeCompileReason(compileLogContext.reason, getLocale())}`),
2047
2133
  status: 'failed',
2048
2134
  failure: contextFailure
2049
2135
  });
2050
2136
  }
2051
2137
  }
2052
2138
 
2053
- const sensitiveFindings = await awaitRunStep(runSensitivePreflight({
2139
+ let sensitiveFindings = await awaitRunStep(runSensitivePreflight({
2054
2140
  task,
2055
2141
  project,
2056
2142
  rules: getGovernanceRulesForCurrentProject(),
@@ -2087,6 +2173,8 @@
2087
2173
  compileLogContext,
2088
2174
  customInstructions: submittedCustomInstructions,
2089
2175
  skillLoadingSettings: submittedSkillLoadingSettings,
2176
+ enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
2177
+ enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds,
2090
2178
  attachments: submittedAttachments,
2091
2179
  skillInvocation: submittedSkillInvocation,
2092
2180
  submittedMode
@@ -2108,6 +2196,25 @@
2108
2196
  fileOverlays = null;
2109
2197
  otWarmStart = false;
2110
2198
  restrictToFocusFiles = staleRetry.restrictToFocusFiles;
2199
+ const retryFindings = await awaitRunStep(runSensitivePreflight({
2200
+ task, project, rules: getGovernanceRulesForCurrentProject(), useExistingMirror: false
2201
+ }));
2202
+ if (retryFindings.blocked) {
2203
+ await finalizeAuditRecord(runAuditDraft, {
2204
+ resultStatus: 'blocked_sensitive', sensitiveFindings: retryFindings.findings,
2205
+ blockedFiles: retryFindings.findings.map(finding => ({
2206
+ path: finding.path || finding.source || 'task', reason: finding.detectorId || 'sensitive'
2207
+ }))
2208
+ });
2209
+ await finishRunView(tx('Blocked: sensitive content review required', '已阻止:需要处理敏感内容'), 'failed');
2210
+ return;
2211
+ }
2212
+ if (retryFindings.findings.length) {
2213
+ sensitiveFindings = {
2214
+ blocked: false, findings: [...sensitiveFindings.findings, ...retryFindings.findings]
2215
+ };
2216
+ runAuditDraft = await updateAuditSensitiveFindings(runAuditDraft, sensitiveFindings.findings);
2217
+ }
2111
2218
  response = await awaitRunStep(sendTrackedCodexRun(buildCodexRunParams({
2112
2219
  task,
2113
2220
  project,
@@ -2120,6 +2227,8 @@
2120
2227
  compileLogContext,
2121
2228
  customInstructions: submittedCustomInstructions,
2122
2229
  skillLoadingSettings: submittedSkillLoadingSettings,
2230
+ enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
2231
+ enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds,
2123
2232
  attachments: submittedAttachments,
2124
2233
  skillInvocation: submittedSkillInvocation,
2125
2234
  submittedMode
@@ -2151,14 +2260,17 @@
2151
2260
  compileLogContext,
2152
2261
  customInstructions: submittedCustomInstructions,
2153
2262
  skillLoadingSettings: submittedSkillLoadingSettings,
2263
+ enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
2264
+ enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds,
2154
2265
  attachments: submittedAttachments,
2155
2266
  skillInvocation: submittedSkillInvocation,
2156
2267
  submittedMode
2157
2268
  }));
2158
2269
  } else {
2159
- appendRunEvent({ title: tx('Cancelled: user chose not to create a new thread.', '已取消:用户选择不新建线程。'), status: 'rejected' });
2160
- await finalizeAuditRecord(runAuditDraft, { resultStatus: 'rejected' });
2161
- await finishRunView(tx('Cancelled', '已取消'), 'rejected');
2270
+ appendRunEvent({ title: tx('Cancelled: user chose not to create a new thread.', '已取消:用户选择不新建线程。'), status: 'info',
2271
+ activity: { v: 1, kind: 'lifecycle', state: 'cancelled', target: 'cancelled' } });
2272
+ await finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
2273
+ await finishRunView(tx('Cancelled', '已取消'), 'cancelled');
2162
2274
  return;
2163
2275
  }
2164
2276
  }
@@ -2166,11 +2278,16 @@
2166
2278
  if (!response.ok) {
2167
2279
  if (runCancellationRequested || isRunCancellationError(response.error)) {
2168
2280
  appendRunCancelledReport();
2169
- await finishRunView(tx('Cancelled', '已中断'), 'rejected');
2281
+ await finishRunView(tx('Cancelled', '已取消'), 'cancelled');
2170
2282
  void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
2171
2283
  return;
2172
2284
  }
2173
- const translated = translateRawError(response.error.message, { mode: submittedMode, locale: getLocale() });
2285
+ if (response.error?.code === 'codex_local_skill_unavailable' && localSkillRestorePoint) {
2286
+ restoreRejectedLocalSkillSubmission({ ...submittedWritingStyleScope, sessionId: submittedSessionId,
2287
+ task: submittedDraft, attachments: submittedAttachments, skillInvocation: submittedSkillInvocation,
2288
+ selectionContext: submittedPanelState.selectionContext }, localSkillRestorePoint);
2289
+ }
2290
+ const translated = translateRawError(response.error.message, { mode: submittedMode, locale: getLocale(), code: response.error?.code });
2174
2291
  // Structured FailureReason (§9.7): split bridge-availability from
2175
2292
  // Codex-side errors. `native_connection_failed` / `native_unavailable`
2176
2293
  // / `native_update_required` indicate the bridge itself is missing,
@@ -2182,7 +2299,11 @@
2182
2299
  // steps) existed but were never emitted, so everything collapsed into
2183
2300
  // "no usable result" and pointed users at the wrong recovery.
2184
2301
  const runErrorMessage = String(response.error?.message || '');
2185
- const codexRunFailure = response.error?.code === 'native_execution_interrupted'
2302
+ const codexRunFailure = response.error?.code === 'codex_local_skill_unavailable'
2303
+ ? buildContentFailure('codex_local_skill_unavailable', { path: 'codex.run' }, { technicalMessage: runErrorMessage })
2304
+ : translated.failureCode === 'codex_local_skills_failed'
2305
+ ? buildContentFailure('codex_local_skills_failed', { path: 'codex.run' }, { technicalMessage: runErrorMessage })
2306
+ : response.error?.code === 'native_execution_interrupted'
2186
2307
  ? buildContentFailure('native_request_failed', { path: 'codex.run' }, {
2187
2308
  // The host IS installed — the request was interrupted mid-flight.
2188
2309
  // Telling this user to reinstall would be a misdiagnosis.
@@ -2420,7 +2541,7 @@
2420
2541
  } catch (error) {
2421
2542
  if (runCancellationRequested || isRunCancellationError(error)) {
2422
2543
  appendRunCancelledReport();
2423
- await finishRunView(tx('Cancelled', '已中断'), 'rejected');
2544
+ await finishRunView(tx('Cancelled', '已取消'), 'cancelled');
2424
2545
  void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
2425
2546
  return;
2426
2547
  }
@@ -2507,6 +2628,8 @@
2507
2628
  submittedMode,
2508
2629
  submittedCustomInstructions,
2509
2630
  submittedSkillLoadingSettings,
2631
+ submittedEnabledLocalSkillIds,
2632
+ submittedEnabledSkillIds,
2510
2633
  submittedAttachments,
2511
2634
  submittedSkillInvocation,
2512
2635
  executionSnapshot
@@ -2515,8 +2638,7 @@
2515
2638
  title: tx('Starting the Codex skill installer.', '正在启动 Codex skill installer。'),
2516
2639
  status: 'running'
2517
2640
  });
2518
- const activeSession = findSessionById(runSessionId) || getActiveSession(state);
2519
- const codexThreadId = activeSession?.codexThreadId || '';
2641
+ const codexThreadId = currentRunView?.codexThreadId || '';
2520
2642
  const params = {
2521
2643
  projectId: getCurrentProjectId(),
2522
2644
  mode: submittedMode,
@@ -2524,27 +2646,29 @@
2524
2646
  model: executionSnapshot.model,
2525
2647
  reasoningEffort: executionSnapshot.reasoningEffort,
2526
2648
  speedTier: executionSnapshot.speedTier,
2527
- session: state.session,
2649
+ session: currentRunView?.sessionContext,
2650
+ clientRunId: currentRunView?.recordId || '',
2651
+ clientSessionId: runSessionId,
2652
+ accountScopeId: currentRunView?.runAccountScopeId || '',
2528
2653
  threadId: codexThreadId || undefined,
2529
2654
  customInstructions: submittedCustomInstructions || undefined,
2530
2655
  loadCodexLocalSkills: submittedSkillLoadingSettings?.loadCodexLocalSkills !== false,
2531
2656
  loadCodexOverleafSkills: submittedSkillLoadingSettings?.loadCodexOverleafSkills !== false,
2657
+ enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds,
2658
+ enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
2532
2659
  attachments: submittedAttachments.length ? submittedAttachments : undefined,
2533
2660
  skillInvocation: submittedSkillInvocation,
2534
2661
  focusFiles: [],
2535
2662
  skipMirrorSync: true
2536
2663
  };
2537
2664
 
2538
- const response = await sendNative({
2539
- method: 'codex.run',
2540
- params
2541
- });
2665
+ const response = await sendTrackedCodexRun(params);
2542
2666
 
2543
2667
  if (!response.ok) {
2544
2668
  if (runCancellationRequested || isRunCancellationError(response.error)) {
2545
2669
  appendRunCancelledReport();
2546
2670
  await finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
2547
- await finishRunView(tx('Cancelled', '已中断'), 'rejected');
2671
+ await finishRunView(tx('Cancelled', '已取消'), 'cancelled');
2548
2672
  return;
2549
2673
  }
2550
2674
  const translated = translateRawError(response.error.message, { mode: submittedMode, locale: getLocale() });
@@ -2738,19 +2862,31 @@
2738
2862
  if (currentRunView) {
2739
2863
  return queueComposerInput();
2740
2864
  }
2865
+ if (runSubmissionPending) return;
2866
+ runSubmissionPending = true;
2741
2867
  runTask().catch(error => {
2742
- setRunning(false);
2743
- nativeChannel.clearActiveRequest();
2744
- stopRunElapsedTick();
2745
- currentRunView = null;
2746
- runCancellationController = null;
2868
+ if (!currentRunView) {
2869
+ setRunning(false);
2870
+ nativeChannel.clearActiveRequest();
2871
+ stopRunElapsedTick();
2872
+ runCancellationController = null;
2873
+ }
2747
2874
  console.error('[codex-overleaf] failed to start task', error);
2748
- appendPlainLog(tx(`Could not start Codex task: ${error.message}`, `无法启动 Codex 任务:${error.message}`));
2749
- });
2875
+ appendPlainLog(tx(`Could not start Codex task: ${localizeVisibleReason(error.message)}`, `无法启动 Codex 任务:${localizeVisibleReason(error.message)}`));
2876
+ }).finally(() => { runSubmissionPending = false; });
2750
2877
  }
2751
2878
 
2752
2879
  const runGuidanceController = Modules.RunGuidanceController.create({
2753
- findRunRecord, appendRunEvent, getCurrentRunView: () => currentRunView,
2880
+ findRunRecord, getCurrentRunView: () => getCurrentRunViewForRender(),
2881
+ getActiveSessionId: () => state?.activeSessionId,
2882
+ getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
2883
+ getSessionCreatedAt: sessionId => findSessionById(sessionId)?.createdAt,
2884
+ getRunRoot: target => target.sessionId === state?.activeSessionId
2885
+ ? panel?.querySelector(`[data-run-id="${cssEscape(target.recordId)}"]`) : null,
2886
+ onRecordChanged: target => updateSessionById(target.sessionId, {}),
2887
+ onPresentationError: error => console.warn('[codex-overleaf] guidance presentation failed', error),
2888
+ RunGuidanceView: Modules.RunGuidanceView, tx, maxRunEvents: MAX_RUN_EVENTS,
2889
+ revealGuidance: row => runTimelineView.revealGuidance(row),
2754
2890
  renderRunEvent, cssEscape, bumpUnreadIfDetached
2755
2891
  });
2756
2892
 
@@ -2764,13 +2900,23 @@
2764
2900
  readInputs: readPanelInputs,
2765
2901
  getTask: () => panel?.querySelector('[data-task]')?.value || '',
2766
2902
  hasAttachments: () => getComposerAttachmentsForRun().length > 0,
2767
- capturePayload: () => RunExecutionSnapshot.toQueuePayload(captureCurrentExecutionSnapshot()),
2903
+ capturePayload: () => ({
2904
+ ...RunExecutionSnapshot.toQueuePayload(captureCurrentExecutionSnapshot()),
2905
+ submissionContext: RunInputQueue.normalizeSubmissionContext({
2906
+ customInstructions: getCustomInstructionsForCurrentProject(),
2907
+ skillLoadingSettings: getSkillLoadingSettings(),
2908
+ skillInvocation: getComposerSkillInvocationForRun(),
2909
+ enabledCodexOverleafSkillIds: getEnabledCodexOverleafSkillIds(),
2910
+ enabledCodexLocalSkillIds: getEnabledCodexLocalSkillIds()
2911
+ })
2912
+ }),
2768
2913
  prepareClaim: () => providerSettingsCoordinator.ensureLoaded(),
2769
2914
  resolveExecutionSnapshot: snapshot => RunExecutionSnapshot.resolveForExecution(snapshot, {
2770
2915
  catalog: providerSettingsCoordinator.getCatalog()
2771
2916
  }),
2772
2917
  clearTask: () => {
2773
2918
  panel.querySelector('[data-task]').value = '';
2919
+ selectionContextView.clear();
2774
2920
  updateSessionById(state.activeSessionId, { task: '' });
2775
2921
  autosizeTaskTextarea();
2776
2922
  syncComposerSendAvailability();
@@ -2782,10 +2928,18 @@
2782
2928
  );
2783
2929
  },
2784
2930
  run: item => runTask({ queuedInput: item }),
2785
- sendSteer: params => sendBackgroundNative({ method: 'codex.steer', params }),
2931
+ getGuideScope: () => ({ projectId: getCurrentProjectId(), accountScopeId: cachedAccountScopeId, generation: spaRouteGeneration }),
2932
+ isGuideCancelled: () => runCancellationRequested || runCancellationController?.signal?.aborted === true,
2933
+ preflightGuide: ({ task, assertCurrent }) => runSensitivePreflight({
2934
+ task, project: { files: [] }, rules: getGovernanceRulesForCurrentProject(),
2935
+ guidance: true, useExistingMirror: false, assertCurrent
2936
+ }),
2937
+ sendSteer: (params, context) => nativeCompatibilityController.sendBackgroundNative({ method: 'codex.steer', params }, context),
2786
2938
  getContainer: () => panel?.querySelector('[data-pending-inputs]'),
2787
2939
  createView: options => PendingInputView.create(options),
2788
2940
  appendEvent: (title, status) => appendRunEvent({ title, status }),
2941
+ previewGuidance: runGuidanceController.preview,
2942
+ clearGuidancePreview: runGuidanceController.clearPreview,
2789
2943
  appendGuidance: runGuidanceController.upsert,
2790
2944
  removeGuidance: runGuidanceController.remove,
2791
2945
  toast: (message, status) => showPluginToast(message, { status }),
@@ -2801,6 +2955,9 @@
2801
2955
  const requestedProviderId = submitted.providerId || state?.providerId || 'builtin';
2802
2956
  const selection = providerSettingsCoordinator.getRunSelection(requestedProviderId);
2803
2957
  const providerId = selection?.providerId || requestedProviderId;
2958
+ const selectedContext = Object.prototype.hasOwnProperty.call(submitted, 'selectionContext')
2959
+ ? submitted.selectionContext : selectionContextView.capture();
2960
+ const selectedPaths = submitted.focusFiles ?? getActiveFocusFiles();
2804
2961
  return RunExecutionSnapshot.capture({
2805
2962
  mode: submitted.mode ?? state?.mode,
2806
2963
  providerId,
@@ -2810,7 +2967,11 @@
2810
2967
  speedTier: submitted.speedTier ?? state?.speedTier,
2811
2968
  autoRecompile: submitted.autoRecompile ?? state?.autoRecompile !== false,
2812
2969
  requireReviewing: submitted.requireReviewing ?? state?.requireReviewing === true,
2813
- focusFiles: submitted.focusFiles ?? getActiveFocusFiles()
2970
+ focusFiles: selectedContext?.mode === 'edit' && selectedPaths.length
2971
+ ? [...new Set([...selectedPaths, selectedContext.path])] : selectedPaths,
2972
+ selectionContext: selectedContext,
2973
+ writingStyle: Object.prototype.hasOwnProperty.call(submitted, 'writingStyle')
2974
+ ? submitted.writingStyle : writingStyleSettings.capture()
2814
2975
  }, {
2815
2976
  source: 'submitted',
2816
2977
  requireProviderRevision: providerId !== 'builtin'
@@ -2818,6 +2979,18 @@
2818
2979
  }
2819
2980
 
2820
2981
  function queueComposerInput() {
2982
+ if (getSkillLoadingSettings().loadCodexLocalSkills !== false && !Array.isArray(getCodexLocalSkillsCatalog())) {
2983
+ primeCodexLocalSkillsCatalog({ required: true }).catch(() => {});
2984
+ showPluginToast(tx('Local Codex skills are still loading. Try submitting again when loading finishes.',
2985
+ 'Codex 本地技能仍在加载,请在加载完成后重新提交。'), { status: 'info' });
2986
+ return;
2987
+ }
2988
+ const invocation = getComposerSkillInvocationForRun();
2989
+ if (invocation?.scope === 'codex-local' && !getEnabledCodexLocalSkillIds()?.includes(invocation.id)) {
2990
+ showPluginToast(tx('The selected local Codex skill is unavailable or disabled. Select an enabled skill and try again.',
2991
+ '所选 Codex 本地技能不可用或已关闭,请选择已启用的技能后重试。'), { status: 'failed' });
2992
+ return;
2993
+ }
2821
2994
  return runInputCoordinator?.queueComposerInput();
2822
2995
  }
2823
2996
 
@@ -2867,7 +3040,7 @@
2867
3040
  const nativeResult = results[0];
2868
3041
  const nativeResponse = nativeResult?.status === 'fulfilled' ? nativeResult.value : null;
2869
3042
  if (nativeResponse && !nativeResponse.ok) {
2870
- const message = nativeResponse?.error?.message || tx('native host did not respond', 'native host 没有响应');
3043
+ const message = localizeVisibleReason(nativeResponse?.error?.message || tx('native host did not respond', 'native host 没有响应'));
2871
3044
  appendRunEvent({
2872
3045
  title: tx(`Cancel request was not delivered: ${message}`, `中断请求没有送达:${message}`),
2873
3046
  status: 'failed'
@@ -2909,25 +3082,8 @@
2909
3082
  force: true
2910
3083
  }
2911
3084
  });
2912
- if (response?.ok) {
2913
- const result = response.result || response;
2914
- const released = result.lockReleased === true || result.cancelled === true;
2915
- appendPlainLog(released
2916
- ? tx(
2917
- `Force-released the stuck Codex run for project ${projectKey}. You can retry now.`,
2918
- `已强制释放项目 ${projectKey} 的 Codex 占用,可以重试了。`
2919
- )
2920
- : tx(
2921
- `No stuck run was found for project ${projectKey}.`,
2922
- `没有发现项目 ${projectKey} 上的卡住任务。`
2923
- ));
2924
- return response;
2925
- }
2926
- const message = response?.error?.message || tx('native host did not respond', 'native host 没有响应');
2927
- appendPlainLog(tx(
2928
- `Force-release request was not delivered: ${message}`,
2929
- `强制释放请求没有送达:${message}`
2930
- ));
3085
+ appendPlainLog(Modules.RunRecoveryActions.formatForceCancelOutcome(response, tx));
3086
+ if (response?.ok !== true && response?.error?.message) appendPlainLog(localizeVisibleReason(response.error.message));
2931
3087
  return response;
2932
3088
  }
2933
3089
 
@@ -3031,6 +3187,12 @@
3031
3187
  .filter(id => id && isCodexOverleafSkillEnabled(id));
3032
3188
  }
3033
3189
 
3190
+ function getEnabledCodexLocalSkillIds() {
3191
+ if (getSkillLoadingSettings().loadCodexLocalSkills === false) return [];
3192
+ const skills = getCodexLocalSkillsCatalog();
3193
+ return Array.isArray(skills) ? skills.filter(skill => isCodexLocalSkillEnabled(skill.id, skill)).map(skill => skill.id) : undefined;
3194
+ }
3195
+
3034
3196
  function buildCodexRunParams({
3035
3197
  task,
3036
3198
  project,
@@ -3043,12 +3205,16 @@
3043
3205
  compileLogContext,
3044
3206
  customInstructions,
3045
3207
  skillLoadingSettings,
3208
+ enabledCodexOverleafSkillIds,
3209
+ enabledCodexLocalSkillIds,
3046
3210
  attachments,
3047
3211
  skillInvocation,
3048
3212
  submittedMode
3049
3213
  } = {}) {
3050
3214
  const executionSnapshot = currentRunView?.executionSnapshot || captureCurrentExecutionSnapshot();
3051
- const submittedState = RunExecutionSnapshot.applyToState(state, executionSnapshot);
3215
+ const submittedState = RunExecutionSnapshot.applyToState({
3216
+ ...state, session: currentRunView?.sessionContext || state.session
3217
+ }, executionSnapshot);
3052
3218
  const params = runController.buildCodexRunParams({
3053
3219
  currentProjectId: getCurrentProjectId(),
3054
3220
  state: submittedState,
@@ -3068,7 +3234,9 @@
3068
3234
  ? getSkillLoadingSettings()
3069
3235
  : { loadCodexLocalSkills: true, loadCodexOverleafSkills: true })
3070
3236
  : skillLoadingSettings,
3071
- enabledCodexOverleafSkillIds: getEnabledCodexOverleafSkillIds(),
3237
+ enabledCodexOverleafSkillIds: enabledCodexOverleafSkillIds === undefined
3238
+ ? getEnabledCodexOverleafSkillIds() : enabledCodexOverleafSkillIds,
3239
+ enabledCodexLocalSkillIds,
3072
3240
  attachments,
3073
3241
  skillInvocation,
3074
3242
  compileLogContext,
@@ -3078,16 +3246,31 @@
3078
3246
  ...params,
3079
3247
  clientRunId: currentRunView?.recordId || '',
3080
3248
  clientSessionId: currentRunView?.sessionId || '',
3081
- providerSelection: RunExecutionSnapshot.toProviderSelection(executionSnapshot)
3249
+ providerSelection: RunExecutionSnapshot.toProviderSelection(executionSnapshot),
3250
+ selectionContext: executionSnapshot.selectionContext || null,
3251
+ accountScopeId: currentRunView?.runAccountScopeId || cachedAccountScopeId || '',
3252
+ writingStyle: executionSnapshot.writingStyle || null
3082
3253
  };
3083
3254
  }
3084
3255
 
3085
3256
  async function sendTrackedCodexRun(params) {
3086
- const compatibilityGate = await nativeCompatibilityController.ensureForMethod('codex.run');
3257
+ const owner = currentRunView;
3258
+ const signal = runCancellationController?.signal;
3259
+ const assertCurrent = () => {
3260
+ if (signal?.aborted || (owner && owner !== currentRunView)) throw createRunCancellationError();
3261
+ throwIfRunCancellationRequested();
3262
+ if (owner && (owner.runProjectId !== getCurrentProjectId()
3263
+ || owner.runAccountScopeId !== cachedAccountScopeId)) {
3264
+ throw Object.assign(createRunCancellationError(), {
3265
+ message: 'The active project or account changed before dispatch.'
3266
+ });
3267
+ }
3268
+ };
3269
+ const compatibilityGate = await nativeCompatibilityController.ensureForMethod('codex.run', { assertCurrent });
3270
+ assertCurrent();
3087
3271
  if (!compatibilityGate.ok) {
3088
3272
  return compatibilityGate.response;
3089
3273
  }
3090
- throwIfRunCancellationRequested();
3091
3274
  const tracked = nativeChannel.sendNativeTracked(nativeCompatibilityController.attachEvidence({
3092
3275
  method: 'codex.run',
3093
3276
  params
@@ -3118,6 +3301,12 @@
3118
3301
  const projectKey = getCurrentProjectId();
3119
3302
  const recovery = await activeTurnControl.recoverJournals({
3120
3303
  projectKey,
3304
+ accountScopeId: cachedAccountScopeId,
3305
+ requireOwnerLost: Modules.StorageDb.sharedSessionsEnabled?.() === true,
3306
+ normalizeInterruptedRun: record => Object.assign(record, Modules.SessionState.normalizeRuns([record], {
3307
+ restoreRunningRuns: true,
3308
+ locale: getLocale()
3309
+ })[0]),
3121
3310
  findSession: findSessionById,
3122
3311
  getActiveSession: () => getActiveSession(state),
3123
3312
  createInterruptedRun: journal => ({
@@ -3160,7 +3349,7 @@
3160
3349
  maxEvents: MAX_RUN_EVENTS
3161
3350
  });
3162
3351
  if (recovery.changed) {
3163
- await saveState();
3352
+ if (await saveState({ recoveryRuns: recovery.recoveredRuns }) !== true) return;
3164
3353
  for (const requestId of recovery.acknowledgeIds) {
3165
3354
  await activeTurnControl.acknowledge(requestId).catch(() => false);
3166
3355
  }
@@ -3172,11 +3361,17 @@
3172
3361
  if (!StorageDb || !AuditRecords) {
3173
3362
  return null;
3174
3363
  }
3364
+ // Audit follows the captured run owner even if the visible page changes.
3365
+ // Missing owner identity must never be filled with a later account.
3366
+ const accountScopeId = currentRunView ? currentRunView.runAccountScopeId : cachedAccountScopeId;
3367
+ const projectId = currentRunView ? currentRunView.runProjectId : getCurrentProjectId();
3368
+ if (!accountScopeId || !projectId) return null;
3175
3369
  const executionSnapshot = input.executionSnapshot
3176
3370
  || currentRunView?.executionSnapshot
3177
3371
  || captureCurrentExecutionSnapshot();
3178
3372
  const draft = AuditRecords.buildAuditDraftRecord({
3179
- projectId: getCurrentProjectId(),
3373
+ accountScopeId,
3374
+ projectId,
3180
3375
  sessionId: input.sessionId || '',
3181
3376
  turnId: currentRunView?.recordId || '',
3182
3377
  mode: input.mode || executionSnapshot.mode,
@@ -3211,8 +3406,10 @@
3211
3406
  return draft;
3212
3407
  }
3213
3408
  const record = StorageDb.buildAuditLogRecord
3214
- ? StorageDb.buildAuditLogRecord({ ...draft, ...updates, id: draft.id, completedAt: updates.completedAt || draft.completedAt || '' })
3215
- : { ...draft, ...updates };
3409
+ ? StorageDb.buildAuditLogRecord({ ...draft, ...updates, id: draft.id,
3410
+ accountScopeId: draft.accountScopeId, projectId: draft.projectId,
3411
+ completedAt: updates.completedAt || draft.completedAt || '' })
3412
+ : { ...draft, ...updates, id: draft.id, accountScopeId: draft.accountScopeId, projectId: draft.projectId };
3216
3413
  return StorageDb.putRecord('auditLogs', record).then(() => record).catch(() => draft);
3217
3414
  }
3218
3415
 
@@ -3242,121 +3439,10 @@
3242
3439
  }
3243
3440
 
3244
3441
  async function runSensitivePreflight(options) {
3245
- options = options || {};
3246
- const { task, project, rules, useExistingMirror = false } = options;
3247
- const normalizedRules = normalizeGovernanceRules(rules);
3248
- if (normalizedRules.sensitiveCheckEnabled === false) {
3249
- return { blocked: false, findings: [] };
3250
- }
3251
- if (!SensitiveScan?.scanSensitiveInputs && !useExistingMirror) {
3252
- return { blocked: false, findings: [] };
3253
- }
3254
- let findings = SensitiveScan?.scanSensitiveInputs
3255
- ? SensitiveScan.scanSensitiveInputs({
3256
- task,
3257
- files: (project?.files || []).filter(isTextSnapshotFile)
3258
- })
3259
- : [];
3260
- if (useExistingMirror) {
3261
- findings = dedupeSensitiveFindings([
3262
- ...findings,
3263
- ...await scanNativeMirrorSensitiveFindings()
3264
- ]);
3265
- }
3266
- if (!findings.length) {
3267
- return { blocked: false, findings: [] };
3268
- }
3269
- appendRunEvent({
3270
- title: tx(
3271
- `Sensitive content check found ${findings.length} item(s).`,
3272
- `敏感内容检查发现 ${findings.length} 项。`
3273
- ),
3274
- status: 'failed',
3275
- detail: findings.slice(0, 8).map(finding => ({
3276
- [tr('detailFile')]: finding.path || finding.source || 'task',
3277
- [tr('detailReason')]: finding.detectorId,
3278
- [tx('Preview', '预览')]: finding.preview || '[REDACTED]'
3279
- }))
3280
- });
3281
- if (normalizedRules.sensitiveConfirmAllowed === true) {
3282
- const approved = await showPluginConfirm({
3283
- title: tr('sensitiveConfirmTitle'),
3284
- message: formatSensitiveConfirmMessage(findings),
3285
- confirmLabel: tr('sensitiveConfirmRun'),
3286
- cancelLabel: tr('confirmDefaultCancel'),
3287
- destructive: true
3288
- });
3289
- return { blocked: !approved, findings };
3290
- }
3291
- appendCompletionReport({
3292
- conclusion: tx(
3293
- 'This run was blocked before Codex received project context because sensitive content was detected.',
3294
- '检测到敏感内容,本轮已在向 Codex 发送项目上下文前阻止。'
3295
- ),
3296
- status: 'blocked',
3297
- operations: [],
3298
- applyResults: [],
3299
- nextStep: tx(
3300
- 'Remove the sensitive content, narrow @context, or enable explicit sensitive confirmation in Project Settings.',
3301
- '请移除敏感内容、缩小 @context,或在项目设置里允许敏感内容显式确认。'
3302
- )
3303
- });
3304
- return { blocked: true, findings };
3305
- }
3306
-
3307
- function formatSensitiveConfirmMessage(findings = []) {
3308
- const visible = findings.slice(0, 8).map(finding => {
3309
- const path = finding.path || finding.source || 'task';
3310
- return `${path}: ${finding.detectorId || 'sensitive'} (${finding.preview || '[REDACTED]'})`;
3311
- });
3312
- return [
3313
- tr('sensitiveConfirmMessage'),
3314
- '',
3315
- ...visible,
3316
- findings.length > visible.length ? tx(`${findings.length - visible.length} more finding(s).`, `另有 ${findings.length - visible.length} 项。`) : ''
3317
- ].filter(Boolean).join('\n');
3318
- }
3319
-
3320
- async function scanNativeMirrorSensitiveFindings() {
3321
- try {
3322
- const response = await sendBackgroundNative({
3323
- method: 'mirror.scanSensitive',
3324
- params: { projectId: getCurrentProjectId() }
3325
- });
3326
- if (response?.ok) {
3327
- return Array.isArray(response.result?.findings) ? response.result.findings : [];
3328
- }
3329
- return [{
3330
- detectorId: 'mirror-sensitive-scan-unavailable',
3331
- source: 'local-mirror',
3332
- preview: response?.error?.message || 'Could not scan the local mirror before Codex runs.'
3333
- }];
3334
- } catch (error) {
3335
- return [{
3336
- detectorId: 'mirror-sensitive-scan-unavailable',
3337
- source: 'local-mirror',
3338
- preview: error?.message || 'Could not scan the local mirror before Codex runs.'
3339
- }];
3340
- }
3341
- }
3342
-
3343
- function dedupeSensitiveFindings(findings = []) {
3344
- const seen = new Set();
3345
- const deduped = [];
3346
- for (const finding of findings || []) {
3347
- const key = [
3348
- finding?.detectorId || '',
3349
- finding?.source || '',
3350
- finding?.path || '',
3351
- finding?.preview || ''
3352
- ].join('\u0000');
3353
- if (seen.has(key)) {
3354
- continue;
3355
- }
3356
- seen.add(key);
3357
- deduped.push(finding);
3358
- }
3359
- return deduped;
3442
+ return Modules.SensitivePreflight.create({
3443
+ SensitiveScan, normalizeGovernanceRules, isTextSnapshotFile, sendBackgroundNative,
3444
+ getCurrentProjectId, appendRunEvent, appendCompletionReport, showPluginConfirm, tr, tx
3445
+ }).run(options);
3360
3446
  }
3361
3447
 
3362
3448
  function appendRunCancelledReport() {
@@ -3364,11 +3450,12 @@
3364
3450
  .some(item => item?.status === 'queued');
3365
3451
  appendRunEvent({
3366
3452
  title: tx('Current Codex task was cancelled.', '已中断当前 Codex 任务。'),
3367
- status: 'failed'
3453
+ status: 'info',
3454
+ activity: { v: 1, kind: 'lifecycle', state: 'cancelled', target: 'cancelled' }
3368
3455
  });
3369
3456
  appendCompletionReport({
3370
3457
  conclusion: tx('This run was cancelled. It did not continue syncing or writing to Overleaf.', '这轮已中断,没有继续同步或写入 Overleaf。'),
3371
- status: 'rejected',
3458
+ status: 'cancelled',
3372
3459
  operations: [],
3373
3460
  applyResults: [],
3374
3461
  nextStep: hasQueuedFollowUp
@@ -3407,6 +3494,7 @@
3407
3494
  : !hasInput;
3408
3495
  runButton.title = label;
3409
3496
  runButton.setAttribute('aria-label', label);
3497
+ if (currentRunView?.writebackRetry && hasInput) runButton.disabled = true;
3410
3498
  }
3411
3499
 
3412
3500
  function handleTaskInputKeydown(event) {
@@ -3421,6 +3509,7 @@
3421
3509
  }
3422
3510
 
3423
3511
  function handleTaskInput() {
3512
+ composerDraftRevision += 1;
3424
3513
  autosizeTaskTextarea();
3425
3514
  syncComposerSendAvailability();
3426
3515
  renderPendingInputs();
@@ -3443,10 +3532,8 @@
3443
3532
  subtitle: tr('slashInstallSkillSubtitle')
3444
3533
  }
3445
3534
  ];
3446
- if (getSkillLoadingSettings().loadCodexOverleafSkills === false) {
3447
- return commands;
3448
- }
3449
- for (const skill of slashCodexOverleafSkills) {
3535
+ const settings = getSkillLoadingSettings();
3536
+ for (const skill of settings.loadCodexOverleafSkills === false ? [] : slashCodexOverleafSkills) {
3450
3537
  const id = String(skill?.id || '').trim();
3451
3538
  if (!isSafeSkillId(id)) {
3452
3539
  continue;
@@ -3462,13 +3549,28 @@
3462
3549
  scope: 'codex-overleaf',
3463
3550
  skillId: id,
3464
3551
  title,
3465
- subtitle: tr('slashUseSkillSubtitle')
3552
+ subtitle: tr('slashUseOverleafSkillSubtitle')
3466
3553
  });
3467
3554
  }
3555
+ const localSkills = settings.loadCodexLocalSkills === false ? [] : (getCodexLocalSkillsCatalog() || []);
3556
+ for (const skill of localSkills) {
3557
+ if (!isSafeSkillId(skill.id) || !isCodexLocalSkillEnabled(skill.id, skill)) continue;
3558
+ const sameName = localSkills.filter(other => String(other.title || other.name || other.id) === String(skill.title || skill.name || skill.id)).length > 1;
3559
+ commands.push({ id: `local-skill:${skill.id}`, kind: 'codex-local-skill', scope: 'codex-local', skillId: skill.id,
3560
+ title: String(skill.title || skill.name || skill.id).trim().slice(0, 80),
3561
+ searchName: String(skill.name || ''),
3562
+ subtitle: tr('slashUseLocalSkillSubtitle') + (skill.sourceKind === 'plugin' && skill.sourceLabel ? ' · ' + skill.sourceLabel : '')
3563
+ + (sameName ? ' · ' + (skill.sourceHint || skill.id.slice(-8)) : '') });
3564
+ }
3468
3565
  return commands;
3469
3566
  }
3470
3567
 
3471
3568
  async function refreshCodexOverleafSkillsForSlashMenu() {
3569
+ if (getSlashTrigger() && getSkillLoadingSettings().loadCodexLocalSkills !== false) {
3570
+ primeCodexLocalSkillsCatalog().catch(() => {
3571
+ showPluginToast(tx('Could not load local Codex skills. Try again.', '无法加载 Codex 本地技能,请重试。'), { status: 'failed' });
3572
+ });
3573
+ }
3472
3574
  if (slashCodexOverleafSkillsLoaded || slashCodexOverleafSkillsLoading) {
3473
3575
  return;
3474
3576
  }
@@ -3502,7 +3604,8 @@
3502
3604
  const commands = getSlashCommands().filter(command => {
3503
3605
  return !query
3504
3606
  || command.title.toLowerCase().includes(query)
3505
- || command.id.toLowerCase().includes(query);
3607
+ || command.id.toLowerCase().includes(query)
3608
+ || String(command.searchName || '').toLowerCase().includes(query);
3506
3609
  });
3507
3610
  renderSlashMenu(commands);
3508
3611
  return;
@@ -3717,6 +3820,17 @@
3717
3820
  renderedSlashCommands = new Map(commands.map(command => [command.id, command]));
3718
3821
  menu.dataset.activeIndex = '0';
3719
3822
  menu.hidden = false;
3823
+ menu.scrollTop = 0;
3824
+ updateSlashMenuLayout();
3825
+ }
3826
+
3827
+ function updateSlashMenuLayout() {
3828
+ const menu = panel?.querySelector('[data-slash-menu]');
3829
+ const composer = panel?.querySelector('[data-composer-form]');
3830
+ if (!menu || menu.hidden || !composer) return;
3831
+ const visibleTop = Math.max(0, root.visualViewport?.offsetTop || 0, panel.getBoundingClientRect().top);
3832
+ const available = Math.max(0, Math.floor(composer.getBoundingClientRect().top - visibleTop - 12));
3833
+ menu.style.setProperty('--codex-slash-menu-space', `${available}px`);
3720
3834
  }
3721
3835
 
3722
3836
  function handleSlashMenuClick(event) {
@@ -3775,6 +3889,18 @@
3775
3889
  buttons.forEach((button, buttonIndex) => {
3776
3890
  button.dataset.active = buttonIndex === index ? 'true' : 'false';
3777
3891
  });
3892
+ const active = buttons[index];
3893
+ if (!active || !menu.clientHeight) return;
3894
+ const viewport = menu.getBoundingClientRect();
3895
+ const item = active.getBoundingClientRect();
3896
+ const top = viewport.top + menu.clientTop;
3897
+ const bottom = top + menu.clientHeight;
3898
+ // Scroll only this popup; scrollIntoView may move the chat or editor too.
3899
+ if (item.height > menu.clientHeight || item.top < top) {
3900
+ menu.scrollTop += item.top - top;
3901
+ } else if (item.bottom > bottom) {
3902
+ menu.scrollTop += item.bottom - bottom;
3903
+ }
3778
3904
  }
3779
3905
 
3780
3906
  function selectSlashCommand(commandId) {
@@ -3796,7 +3922,7 @@
3796
3922
  activateSkillInstallerComposerContext();
3797
3923
  return;
3798
3924
  }
3799
- if (command.kind === 'codex-overleaf-skill') {
3925
+ if (command.kind === 'codex-overleaf-skill' || command.kind === 'codex-local-skill') {
3800
3926
  activateCodexOverleafSkillComposerContext(command);
3801
3927
  }
3802
3928
  }
@@ -3823,6 +3949,7 @@
3823
3949
  }
3824
3950
 
3825
3951
  function activateSkillInstallerComposerContext() {
3952
+ composerDraftRevision += 1;
3826
3953
  composerSkillInvocation = {
3827
3954
  id: 'skill-installer',
3828
3955
  title: tr('skillInstallerComposerLabel')
@@ -3833,6 +3960,7 @@
3833
3960
  }
3834
3961
 
3835
3962
  function activateCodexOverleafSkillComposerContext(command) {
3963
+ composerDraftRevision += 1;
3836
3964
  const id = String(command?.skillId || '').trim();
3837
3965
  if (!isSafeSkillId(id)) {
3838
3966
  return;
@@ -3840,7 +3968,7 @@
3840
3968
  composerSkillInvocation = {
3841
3969
  id,
3842
3970
  title: String(command?.title || id).trim().slice(0, 80) || id,
3843
- scope: 'codex-overleaf'
3971
+ scope: command.scope === 'codex-local' ? 'codex-local' : 'codex-overleaf'
3844
3972
  };
3845
3973
  renderComposerSkillInvocation();
3846
3974
  const input = panel?.querySelector('[data-task]');
@@ -3848,6 +3976,7 @@
3848
3976
  }
3849
3977
 
3850
3978
  function clearComposerSkillInvocation() {
3979
+ composerDraftRevision += 1;
3851
3980
  composerSkillInvocation = null;
3852
3981
  renderComposerSkillInvocation();
3853
3982
  panel?.querySelector('[data-task]')?.focus?.();
@@ -3858,7 +3987,7 @@
3858
3987
  if (!normalized) {
3859
3988
  return null;
3860
3989
  }
3861
- if (normalized.id === 'skill-installer') {
3990
+ if (normalized.id === 'skill-installer' && !normalized.scope) {
3862
3991
  return {
3863
3992
  id: 'skill-installer',
3864
3993
  title: tr('skillInstallerComposerLabel')
@@ -3873,13 +4002,9 @@
3873
4002
  return null;
3874
4003
  }
3875
4004
  const title = String(value?.title || id).trim().slice(0, 80) || id;
3876
- if (id === 'skill-installer') {
3877
- return { id, title };
3878
- }
3879
- if (value?.scope !== 'codex-overleaf') {
3880
- return null;
3881
- }
3882
- return { id, title, scope: 'codex-overleaf' };
4005
+ if (id === 'skill-installer' && (!value?.scope || value.scope === 'builtin')) return { id, title };
4006
+ if (!['codex-overleaf', 'codex-local'].includes(value?.scope)) return null;
4007
+ return { id, title, scope: value.scope };
3883
4008
  }
3884
4009
 
3885
4010
  function isSafeSkillId(id) {
@@ -3900,7 +4025,7 @@
3900
4025
  }
3901
4026
  const clear = context.querySelector('[data-composer-skill-clear]');
3902
4027
  if (clear) {
3903
- const clearLabel = active?.id === 'skill-installer'
4028
+ const clearLabel = active?.id === 'skill-installer' && !active.scope
3904
4029
  ? tr('skillInstallerComposerClear')
3905
4030
  : tr('composerSkillClear');
3906
4031
  clear.title = clearLabel;
@@ -3941,72 +4066,6 @@
3941
4066
  return writebackController.buildSyncApplyOperations(syncChanges, project);
3942
4067
  }
3943
4068
 
3944
- function partitionUnsafeProjectPathOperations(operations = []) {
3945
- const safe = [];
3946
- const skipped = [];
3947
- for (const operation of operations || []) {
3948
- const normalized = normalizeOperationProjectPaths(operation);
3949
- const invalid = getInvalidOperationProjectPath(normalized);
3950
- if (invalid) {
3951
- skipped.push({
3952
- operation: normalized,
3953
- result: {
3954
- ok: false,
3955
- code: 'invalid_project_path',
3956
- reason: tx(`Invalid ${invalid}. Codex did not write this file.`, `路径无效:${invalid}。Codex 没有写入这个文件。`)
3957
- }
3958
- });
3959
- continue;
3960
- }
3961
- safe.push(normalized);
3962
- }
3963
- return { safe, skipped };
3964
- }
3965
-
3966
- function normalizeOperationProjectPaths(operation = {}) {
3967
- if (!operation || typeof operation !== 'object') {
3968
- return operation;
3969
- }
3970
- const normalized = { ...operation };
3971
- if (typeof operation.path === 'string') {
3972
- normalized.path = normalizeSafeProjectPath(operation.path);
3973
- if (!normalized.path) {
3974
- normalized.invalidProjectPath = true;
3975
- }
3976
- }
3977
- if (typeof operation.to === 'string') {
3978
- normalized.to = normalizeSafeProjectPath(operation.to);
3979
- if (!normalized.to) {
3980
- normalized.invalidProjectDestinationPath = true;
3981
- }
3982
- }
3983
- if (typeof operation.destinationPath === 'string') {
3984
- normalized.destinationPath = normalizeSafeProjectPath(operation.destinationPath);
3985
- if (!normalized.destinationPath) {
3986
- normalized.invalidProjectDestinationPath = true;
3987
- }
3988
- }
3989
- return normalized;
3990
- }
3991
-
3992
- function getInvalidOperationProjectPath(operation = {}) {
3993
- if (operation.invalidProjectPath || (requiresOperationPath(operation) && !operation.path)) {
3994
- return 'operation path';
3995
- }
3996
- if (operation.invalidProjectDestinationPath || (requiresOperationDestinationPath(operation) && !(operation.to || operation.destinationPath))) {
3997
- return 'operation destination path';
3998
- }
3999
- return '';
4000
- }
4001
-
4002
- function requiresOperationPath(operation = {}) {
4003
- return ['edit', 'create', 'delete', 'rename', 'move', 'binary-create', 'overwrite-binary'].includes(operation.type);
4004
- }
4005
-
4006
- function requiresOperationDestinationPath(operation = {}) {
4007
- return operation.type === 'rename' || operation.type === 'move';
4008
- }
4009
-
4010
4069
  function normalizeSafeProjectPath(value) {
4011
4070
  if (Modules.ProjectFiles?.normalizeSafeProjectPath) {
4012
4071
  return Modules.ProjectFiles.normalizeSafeProjectPath(value);
@@ -4018,157 +4077,9 @@
4018
4077
  .replace(/^\/+/, '');
4019
4078
  }
4020
4079
 
4021
- function evaluateGovernedOperations(operations = []) {
4022
- if (!GovernanceRules?.evaluateGovernedOperations) {
4023
- return { allowed: operations || [], blocked: [], rules: getGovernanceRulesForCurrentProject() };
4024
- }
4025
- return GovernanceRules.evaluateGovernedOperations(operations, getGovernanceRulesForCurrentProject());
4026
- }
4027
-
4028
- function buildGovernanceSkippedApplyResult(blockedItems = []) {
4029
- return {
4030
- ok: blockedItems.length === 0,
4031
- applied: [],
4032
- skipped: (blockedItems || []).map(item => ({
4033
- operation: item.operation,
4034
- result: {
4035
- ok: false,
4036
- code: 'governance_blocked',
4037
- reason: formatGovernanceBlockedReason(item),
4038
- reasonKey: item.reason || 'governance_blocked'
4039
- }
4040
- }))
4041
- };
4042
- }
4043
-
4044
- function formatGovernanceBlockedReason(item = {}) {
4045
- if (item.reason === 'readonly') {
4046
- return tx(
4047
- 'Project governance marked this path read-only, so Codex did not write it.',
4048
- '项目治理规则将此路径标记为只读,因此 Codex 没有写入。'
4049
- );
4050
- }
4051
- if (item.reason === 'writable_allowlist') {
4052
- return tx(
4053
- 'Project governance allows writes only to configured writable patterns, and this path is outside that allowlist.',
4054
- '项目治理规则只允许写入配置的可写路径,此路径不在允许范围内。'
4055
- );
4056
- }
4057
- return tx('Project governance blocked this write.', '项目治理规则阻止了此写入。');
4058
- }
4059
-
4060
- function filterSyncChangesByOperations(syncChanges = [], operations = []) {
4061
- const allowedPaths = new Set((operations || []).map(operation => operation.path).filter(Boolean));
4062
- return (syncChanges || []).filter(change => allowedPaths.has(change?.path));
4063
- }
4064
-
4065
- function mergeApplyResultSkipped(result = {}, skipped = []) {
4066
- if (!skipped.length) {
4067
- return result;
4068
- }
4069
- return {
4070
- ...(result || {}),
4071
- ok: false,
4072
- applied: Array.isArray(result?.applied) ? result.applied : [],
4073
- skipped: [
4074
- ...getSkippedEntries(result),
4075
- ...skipped
4076
- ]
4077
- };
4078
- }
4079
-
4080
- async function confirmBinaryOperations(operations = []) {
4081
- const binaryOperations = (operations || []).filter(operation => operation.type === 'binary-create' || operation.type === 'overwrite-binary');
4082
- if (!binaryOperations.length) {
4083
- return { operations, skipped: [] };
4084
- }
4085
- const approved = await showPluginConfirm({
4086
- title: tr('binaryAssetConfirmTitle'),
4087
- message: tr('binaryAssetConfirmMessage', { files: formatOperationFiles(binaryOperations) }),
4088
- confirmLabel: tr('binaryAssetConfirm'),
4089
- cancelLabel: tr('binaryAssetCancel'),
4090
- destructive: true
4091
- });
4092
- if (approved) {
4093
- return { operations, skipped: [] };
4094
- }
4095
- return {
4096
- operations: operations.filter(operation => operation.type !== 'binary-create' && operation.type !== 'overwrite-binary'),
4097
- skipped: binaryOperations.map(operation => ({
4098
- operation,
4099
- result: {
4100
- ok: false,
4101
- code: 'binary_confirmation_rejected',
4102
- reason: tx('Binary asset writeback requires explicit confirmation and was skipped.', '二进制资源写回需要显式确认,已跳过。')
4103
- }
4104
- }))
4105
- };
4106
- }
4107
-
4108
- function buildAuditDiffSummary(operations = []) {
4109
- const changedFiles = new Set();
4110
- let binaryFilesChanged = 0;
4111
- for (const operation of operations || []) {
4112
- if (operation?.path) {
4113
- changedFiles.add(operation.path);
4114
- }
4115
- if (operation?.type === 'binary-create' || operation?.type === 'overwrite-binary') {
4116
- binaryFilesChanged++;
4117
- }
4118
- }
4119
- return {
4120
- filesChanged: changedFiles.size,
4121
- additions: 0,
4122
- deletions: 0,
4123
- binaryFilesChanged
4124
- };
4125
- }
4126
-
4127
- function buildAuditSummaryFromApply({ operations = [], applyResults = [], blockedFiles = [], resultStatus = 'completed', saveVerification = null } = {}) {
4128
- const appliedFiles = [];
4129
- const skippedFiles = [];
4130
- for (const result of applyResults || []) {
4131
- for (const item of getAppliedEntries(result)) {
4132
- appliedFiles.push(summarizeOperationForAudit(item.operation, item.result, 'applied'));
4133
- }
4134
- for (const item of getSkippedEntries(result)) {
4135
- skippedFiles.push(summarizeOperationForAudit(item.operation, item.result, 'skipped'));
4136
- }
4137
- }
4138
- return {
4139
- changedFiles: (operations || []).map(operation => summarizeOperationForAudit(operation, {}, 'changed')),
4140
- ['diffSummary']: buildAuditDiffSummary(operations),
4141
- blockedFiles,
4142
- appliedFiles,
4143
- skippedFiles,
4144
- resultStatus,
4145
- saveVerification
4146
- };
4147
- }
4148
-
4149
- // Tolerates `operation` and `result` being null in addition to undefined.
4150
- // Default-parameter values fire only for `undefined`, but the v1.3.8
4151
- // write-guard (pageBridge.runWriteGuard / writebackRouter.checkWritebackRunProjectId)
4152
- // emits batch-level skips with `operation: null` — there is no specific
4153
- // op to attribute the block to. Without this normalization the audit pass
4154
- // crashed with "Cannot read properties of null (reading 'path')", the
4155
- // outer-catch swallowed the partial-sync conclusion, and the user saw the
4156
- // misleading "local Codex returned no usable result" fallback.
4157
- function summarizeOperationForAudit(operation, result, status = '') {
4158
- const op = operation || {};
4159
- const res = result || {};
4160
- return {
4161
- path: op.path || op.from || op.to || '',
4162
- destinationPath: op.destinationPath || op.to || '',
4163
- type: op.type || '',
4164
- reason: res.reasonKey || res.code || res.reason || op.reasonKey || op.reason || '',
4165
- status,
4166
- size: op.size
4167
- };
4168
- }
4169
-
4170
- function getSyncChangePatches(change = {}) {
4171
- return writebackController.getSyncChangePatches(change);
4080
+
4081
+ function getSyncChangePatches(change = {}) {
4082
+ return writebackController.getSyncChangePatches(change);
4172
4083
  }
4173
4084
 
4174
4085
  function normalizeTextPatches(patches) {
@@ -4276,6 +4187,17 @@
4276
4187
  // saveState. Returning null puts the record in degraded mode and excludes
4277
4188
  // it from cross-project queries (spec §5.2 storage rules).
4278
4189
  window.codexOverleafDeriveAccountScopeId = () => cachedAccountScopeId;
4190
+ var sharedSessionViewSync = Modules.SharedSessionViewSync.install({
4191
+ Bridge: Modules.SharedSessionBridge, StorageDb: Modules.StorageDb,
4192
+ getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
4193
+ getState: () => state, setState: next => { state = next; },
4194
+ isBusy: () => Boolean(readLiveRunViewForSaveStateGuard() || document.querySelector('dialog[open]')),
4195
+ normalize: normalizePanelState, render: applyStateToPanel,
4196
+ notice: error => appendStorageNoticeOnce('shared-history-refresh', tx(
4197
+ `Could not refresh shared history: ${error.message}`,
4198
+ `共享会话历史刷新失败:${error.message}`
4199
+ ))
4200
+ });
4279
4201
 
4280
4202
  // -----------------------------------------------------------------------
4281
4203
  // SPA route change lifecycle (spec §5.7).
@@ -4354,7 +4276,7 @@
4354
4276
  chromeApi: chrome, getState: () => state, setState: next => { state = next; }, getPanel: () => panel,
4355
4277
  applyTheme: applyPanelTheme, applyLocale: () => { applyLocaleToPanel(); refreshDashboardSoon(); },
4356
4278
  onPreloadChange: enabled => contextTrayController.scheduleContextPrefetch({ enabled }),
4357
- onSkillsChange: () => { renderLocalSkillList(); updateSkillsEntrySummary(); },
4279
+ onSkillsChange: () => { renderLocalSkillList(); updateSkillsEntrySummary(); updateSlashMenuForTaskInput(); },
4358
4280
  onError: error => showPluginToast(tx(
4359
4281
  `Global settings could not be saved or loaded: ${error.message}. Please retry.`,
4360
4282
  `全局设置未能保存或读取:${error.message}。请重试。`), { status: 'failed', sticky: true })
@@ -4369,7 +4291,8 @@
4369
4291
  try {
4370
4292
  storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
4371
4293
  const reloaded = await loadStoredState();
4372
- state = normalizePanelState(getGlobalPreferences().overlay(reloaded), { restoreRunningRuns: true });
4294
+ state = normalizeLoadedPanelState(reloaded);
4295
+ await getRunReviewController().recoverInterruptedUndoStatuses();
4373
4296
  applyStateToPanel();
4374
4297
  } catch (_error) {
4375
4298
  // Reload failures fall back to the in-memory state; the next saveState
@@ -4431,6 +4354,7 @@
4431
4354
  function leaveActiveProject(newId) {
4432
4355
  const prevId = activeProjectId;
4433
4356
  activeProjectId = newId; // null for non-project URLs (spec §5.7.1)
4357
+ getComposerDraftPersistence().syncScope();
4434
4358
  if (!prevId || prevId === newId) {
4435
4359
  return;
4436
4360
  }
@@ -4471,6 +4395,7 @@
4471
4395
  // project derivation: account menu is global chrome and should be
4472
4396
  // readable on per-project URLs too).
4473
4397
  await refreshAccountScopeId();
4398
+ getComposerDraftPersistence().syncScope();
4474
4399
  if (generation !== spaRouteGeneration || pathname !== window.location.pathname) {
4475
4400
  return;
4476
4401
  }
@@ -4699,7 +4624,7 @@
4699
4624
  const applied = await applyTaskOperations(project, operations);
4700
4625
  applyResults.push(applied);
4701
4626
  appendApplyResult(applied);
4702
- recordUndoFromApply(project, applied);
4627
+ await recordUndoFromApply(project, applied);
4703
4628
  const approved = await showPluginConfirm({
4704
4629
  title: tx('Confirm delete plan?', '确认删除计划?'),
4705
4630
  message: formatDeletePlan(result.deletePlan || []),
@@ -4713,7 +4638,7 @@
4713
4638
  const deleteApplied = await applyTaskOperations(project, pendingOperations, { allowHighRisk: true });
4714
4639
  applyResults.push(deleteApplied);
4715
4640
  appendApplyResult(deleteApplied);
4716
- recordUndoFromApply(project, deleteApplied);
4641
+ await recordUndoFromApply(project, deleteApplied);
4717
4642
  appendLog(tx('Deleted files after confirmation.', '已按确认删除文件。'));
4718
4643
  const summaryLine = appendChangeSummary({
4719
4644
  notes,
@@ -4766,7 +4691,7 @@
4766
4691
  appendOperationsPreview(operations, tx('Preparing to write', '准备写入'));
4767
4692
  const applied = await applyTaskOperations(project, operations);
4768
4693
  appendApplyResult(applied);
4769
- recordUndoFromApply(project, applied);
4694
+ await recordUndoFromApply(project, applied);
4770
4695
  appendLog(mode === 'auto' ? tx('Auto write task completed.', '自动写入任务完成。') : tx('Task completed.', '任务完成。'));
4771
4696
  const summaryLine = appendChangeSummary({
4772
4697
  notes,
@@ -4791,6 +4716,7 @@
4791
4716
  }
4792
4717
 
4793
4718
  async function applyTaskOperations(project, operations, options = {}) {
4719
+ const target = currentRunView;
4794
4720
  const runRequireReviewing = typeof options.requireReviewing === 'boolean'
4795
4721
  ? options.requireReviewing
4796
4722
  : state.requireReviewing === true;
@@ -4800,7 +4726,13 @@
4800
4726
  title: tr('undoNoWritesTitle'),
4801
4727
  status: 'completed'
4802
4728
  });
4803
- } else {
4729
+ }
4730
+ const reviewing = await ensureReviewingBeforeWrite(partitioned.safe, { requireReviewing: runRequireReviewing });
4731
+ if (!reviewing.ok) {
4732
+ return buildReviewingBlockedApplyResult(partitioned.safe, reviewing, partitioned.skipped);
4733
+ }
4734
+ if (partitioned.safe.length) await stageWritebackRecovery(partitioned.safe, target, runRequireReviewing, project);
4735
+ if (partitioned.safe.length) {
4804
4736
  appendRunEvent({
4805
4737
  title: tx('Saved a recoverable version before this run writes.', '已保存本轮写入前的可恢复版本。'),
4806
4738
  status: 'completed',
@@ -4809,10 +4741,7 @@
4809
4741
  }
4810
4742
  });
4811
4743
  }
4812
- const reviewing = await ensureReviewingBeforeWrite(partitioned.safe, { requireReviewing: runRequireReviewing });
4813
- if (!reviewing.ok) {
4814
- return buildReviewingBlockedApplyResult(partitioned.safe, reviewing, partitioned.skipped);
4815
- }
4744
+
4816
4745
  const applied = partitioned.safe.length
4817
4746
  ? await callPageBridge('applyOperations', {
4818
4747
  operations: partitioned.safe,
@@ -4820,7 +4749,7 @@
4820
4749
  requireReviewing: runRequireReviewing,
4821
4750
  requireEditing: !runRequireReviewing,
4822
4751
  // Welcome-panel + write-guard:
4823
- runProjectId: currentRunView?.runProjectId || ''
4752
+ runProjectId: target?.runProjectId || ''
4824
4753
  })
4825
4754
  : { ok: true, applied: [], skipped: [] };
4826
4755
 
@@ -5018,18 +4947,33 @@
5018
4947
  button.setAttribute('aria-busy', loading ? 'true' : 'false');
5019
4948
  }
5020
4949
 
5021
- function formatProbeStatusBar(probe) {
4950
+ function syncProbeStatus(mode = state?.mode) {
4951
+ const status = panel?.querySelector('[data-probe-status]');
4952
+ if (!status) return;
4953
+ if (status.dataset.refreshing === 'true') {
4954
+ status.textContent = tr('refreshProbeLoading');
4955
+ return;
4956
+ }
4957
+ if (probeStatusSnapshot?.projectId !== getCurrentProjectId()) return;
4958
+ const { failed, probe, userInitiated } = probeStatusSnapshot;
4959
+ const text = failed ? tr('refreshProbeFailed') : formatProbeStatusBar(probe, mode);
4960
+ status.textContent = userInitiated && !failed ? tr('refreshProbeDone', { status: text }) : text;
4961
+ status.dataset.ok = !failed && isProbeReadyForCurrentMode(probe, mode) ? 'true' : 'false';
4962
+ if (!failed) updateExistingProbeNotice(probe, mode);
4963
+ }
4964
+
4965
+ function formatProbeStatusBar(probe, mode = state?.mode) {
5022
4966
  const readiness = getProbeRunReadiness(probe);
5023
4967
  const reviewingOk = readiness.reviewingOk;
5024
4968
  const editorWritable = readiness.editorWritable;
5025
4969
 
5026
- if (state?.mode === 'ask') {
4970
+ if (mode === 'ask') {
5027
4971
  return appendOtStatusToProbeStatus(`${formatModeLabel('ask')} · ${readiness.contextLabel}`);
5028
4972
  }
5029
4973
  if (reviewingOk) {
5030
4974
  const status = editorWritable
5031
4975
  ? `${tr('canRun')} · ${readiness.contextLabel}`
5032
- : `${formatModeLabel(state?.mode)} · ${tr('validatingEditor')}`;
4976
+ : `${formatModeLabel(mode)} · ${tr('validatingEditor')}`;
5033
4977
  return appendOtStatusToProbeStatus(status);
5034
4978
  }
5035
4979
  if (readiness.contextReady) {
@@ -5059,9 +5003,9 @@
5059
5003
  status.textContent = appendOtStatusToProbeStatus(base);
5060
5004
  }
5061
5005
 
5062
- function isProbeReadyForCurrentMode(probe) {
5006
+ function isProbeReadyForCurrentMode(probe, mode = state?.mode) {
5063
5007
  const readiness = getProbeRunReadiness(probe);
5064
- return state?.mode === 'ask' || readiness.reviewingOk;
5008
+ return mode === 'ask' || readiness.reviewingOk;
5065
5009
  }
5066
5010
 
5067
5011
  function getProbeRunReadiness(probe) {
@@ -5126,22 +5070,22 @@
5126
5070
  appendLog(message);
5127
5071
  }
5128
5072
 
5129
- function updateExistingProbeNotice(probe) {
5073
+ function updateExistingProbeNotice(probe, mode = state?.mode) {
5130
5074
  if (currentRunView || !panel?.querySelector('[data-probe-notice]')) {
5131
5075
  return;
5132
5076
  }
5133
- const { ready, message } = formatProbeUserNotice(probe);
5077
+ const { ready, message } = formatProbeUserNotice(probe, mode);
5134
5078
  updateProbeNotice(ready ? '' : message);
5135
5079
  }
5136
5080
 
5137
- function formatProbeUserNotice(probe) {
5081
+ function formatProbeUserNotice(probe, mode = state?.mode) {
5138
5082
  const readiness = getProbeRunReadiness(probe);
5139
5083
  const reviewingOk = readiness.reviewingOk;
5140
5084
  const editorOk = readiness.editorOk;
5141
5085
  const manualOverride = probe.reviewing?.status === 'manual-override';
5142
5086
  const editorWriteBlocked = probe.capabilities?.editor?.write === false;
5143
5087
 
5144
- if (state?.mode === 'ask') {
5088
+ if (mode === 'ask') {
5145
5089
  return {
5146
5090
  ready: true,
5147
5091
  message: tx(
@@ -5155,8 +5099,8 @@
5155
5099
  return {
5156
5100
  ready: true,
5157
5101
  message: tx(
5158
- `Ready: ${formatModeLabel(state?.mode)} is selected. This page has not exposed a writable editor yet; Codex will reopen and verify the target file when writing. If writeback fails, reload Overleaf and retry.`,
5159
- `可以运行:已选择“${formatModeLabel(state?.mode)}”。当前页面暂时没有暴露可写编辑器,写入时会重新打开目标文件并验证;如果写回失败,再刷新 Overleaf 页面后重试。`
5102
+ `Ready: ${formatModeLabel(mode)} is selected. This page has not exposed a writable editor yet; Codex will reopen and verify the target file when writing. If writeback fails, reload Overleaf and retry.`,
5103
+ `可以运行:已选择“${formatModeLabel(mode)}”。当前页面暂时没有暴露可写编辑器,写入时会重新打开目标文件并验证;如果写回失败,再刷新 Overleaf 页面后重试。`
5160
5104
  )
5161
5105
  };
5162
5106
  }
@@ -5209,7 +5153,7 @@
5209
5153
  focusFiles: getActiveFocusFiles()
5210
5154
  });
5211
5155
  const sanitizedProject = sanitizeRunProjectSnapshot(project);
5212
- if (sanitizedProject?.capabilities?.fullProjectSnapshot) {
5156
+ if (sanitizedProject?.capabilities?.fullProjectSnapshot || sanitizedProject?.capabilities?.pathIdentityComplete === false) {
5213
5157
  return sanitizedProject;
5214
5158
  }
5215
5159
  const pageStateProject = await callPageBridge('getProjectSnapshot', {
@@ -5230,25 +5174,37 @@
5230
5174
  }
5231
5175
 
5232
5176
  function sanitizeRunProjectSnapshot(project) {
5233
- if (!project || typeof project !== 'object') {
5234
- return project;
5235
- }
5236
- const activePath = normalizeSnapshotPath(project.activePath || '');
5177
+ if (!project || typeof project !== 'object') return project;
5178
+ const normalize = Modules.ProjectFiles.normalizeSafeProjectPath;
5237
5179
  const files = Array.isArray(project.files) ? project.files : [];
5238
- const filePathSet = new Set(files.map(file => normalizeSnapshotPath(file?.path || '')).filter(Boolean));
5239
- const activePathKnown = activePath && filePathSet.has(activePath);
5240
- if (!activePath || activePathKnown) {
5241
- return project;
5180
+ const counts = new Map();
5181
+ for (const file of files) {
5182
+ const path = normalize(file?.path);
5183
+ if (path) counts.set(path, (counts.get(path) || 0) + 1);
5242
5184
  }
5185
+ const skipped = [];
5186
+ const safeFiles = files.filter(file => {
5187
+ const path = normalize(file?.path);
5188
+ if (path && counts.get(path) === 1) return true;
5189
+ skipped.push({ path: typeof file?.path === 'string' ? file.path : '',
5190
+ reason: path ? 'snapshot_path_duplicate' : 'snapshot_path_invalid' });
5191
+ return false;
5192
+ });
5193
+ const activePath = normalize(project.activePath || '');
5194
+ const activePathKnown = activePath && safeFiles.some(file => file.path === activePath);
5195
+ const rejectActive = Boolean(project.activePath && !activePathKnown);
5196
+ if (!skipped.length && !rejectActive) return project;
5243
5197
  return {
5244
5198
  ...project,
5245
- activePath: '',
5246
- files: files.map(file => file?.active ? { ...file, active: false } : file),
5199
+ activePath: rejectActive ? '' : project.activePath,
5200
+ files: safeFiles.map(file => rejectActive && file?.active ? { ...file, active: false } : file),
5247
5201
  capabilities: {
5248
5202
  ...(project.capabilities || {}),
5203
+ ...(skipped.length ? { fullProjectSnapshot: false, contentComplete: false, pathIdentityComplete: false,
5204
+ skipped: [...(project.capabilities?.skipped || []), ...skipped] } : {}),
5249
5205
  diagnostics: {
5250
5206
  ...(project.capabilities?.diagnostics || {}),
5251
- rejectedActivePath: project.activePath
5207
+ ...(rejectActive ? { rejectedActivePath: project.activePath } : {})
5252
5208
  }
5253
5209
  }
5254
5210
  };
@@ -5334,11 +5290,8 @@
5334
5290
  }
5335
5291
 
5336
5292
  function normalizeSnapshotPath(path) {
5337
- return String(path || '')
5338
- .replace(/^@file:/i, '')
5339
- .replace(/\\/g, '/')
5340
- .trim()
5341
- .replace(/^\/+/, '');
5293
+ // Snapshot/overlay/focus arrays already carry canonical names from file chips.
5294
+ return Modules.ProjectFiles.normalizeSafeProjectPath(path);
5342
5295
  }
5343
5296
 
5344
5297
  function mergeProjectWithSyncChangeBaseFiles(project = {}, syncChanges = []) {
@@ -5489,6 +5442,9 @@
5489
5442
  }
5490
5443
 
5491
5444
  async function callPageBridge(method, params) {
5445
+ if (method === 'applyOperations' && params?.reviewingPolicy === 'no-trace-undo') {
5446
+ return Modules.AssetTransferBroker.create({ callPageBridge: (name, input) => pageBridgeClient.call(name, input), sendBackgroundNative }).applyOperations(params);
5447
+ }
5492
5448
  return pageBridgeClient.call(method, params);
5493
5449
  }
5494
5450
 
@@ -5547,8 +5503,10 @@
5547
5503
  legacyKey,
5548
5504
  accountScopeId
5549
5505
  );
5506
+ const composerAttachments = await loadComposerAttachmentsForProject(accountScopeId, projectId);
5550
5507
 
5551
5508
  return {
5509
+ composerAttachments,
5552
5510
  model: prefs.model || '', providerId: prefs.providerId || 'builtin',
5553
5511
  reasoningEffort: prefs.reasoningEffort || '',
5554
5512
  speedTier: prefs.speedTier || '',
@@ -5560,6 +5518,7 @@
5560
5518
  loadCodexLocalSkills: prefs.loadCodexLocalSkills !== false,
5561
5519
  loadCodexOverleafSkills: prefs.loadCodexOverleafSkills !== false,
5562
5520
  codexOverleafSkillEnabled: prefs.codexOverleafSkillEnabled || {},
5521
+ codexLocalSkillEnabled: prefs.codexLocalSkillEnabled || {},
5563
5522
  experimentalOtByProject: prefs.experimentalOtByProject || {},
5564
5523
  customInstructionsByProject: prefs.customInstructionsByProject || {},
5565
5524
  governanceRulesByProject: normalizeGovernanceRulesByProject(prefs.governanceRulesByProject),
@@ -5569,9 +5528,11 @@
5569
5528
  accountScopeId: session.accountScopeId || accountScopeId,
5570
5529
  title: session.title || '',
5571
5530
  titleSource: session.titleSource || 'auto',
5572
- focusFiles: session.focusFiles || [],
5531
+ focusFiles: session.focusFiles || [], selectionContext: session.selectionContext?.projectId === projectId ? session.selectionContext : null,
5573
5532
  projectReferenceFiles: session.projectReferenceFiles || [],
5574
5533
  codexThreadId: session.codexThreadId || '',
5534
+ forkedFromSessionId: session.forkedFromSessionId || '',
5535
+ forkedFromRunId: session.forkedFromRunId || '',
5575
5536
  createdAt: session.createdAt,
5576
5537
  updatedAt: session.updatedAt,
5577
5538
  runs: Array.isArray(session.runs) ? session.runs : [],
@@ -5612,6 +5573,7 @@
5612
5573
  loadCodexLocalSkills: fallback.loadCodexLocalSkills !== false,
5613
5574
  loadCodexOverleafSkills: fallback.loadCodexOverleafSkills !== false,
5614
5575
  codexOverleafSkillEnabled: fallback.codexOverleafSkillEnabled || {},
5576
+ codexLocalSkillEnabled: fallback.codexLocalSkillEnabled || {},
5615
5577
  experimentalOtByProject: fallback.experimentalOtByProject || {},
5616
5578
  customInstructionsByProject: fallback.customInstructionsByProject || {},
5617
5579
  governanceRulesByProject: normalizeGovernanceRulesByProject(fallback.governanceRulesByProject),
@@ -5640,33 +5602,21 @@
5640
5602
  }
5641
5603
 
5642
5604
  async function saveState(options) {
5605
+ const endSharedSave = typeof sharedSessionViewSync !== 'undefined'
5606
+ ? sharedSessionViewSync?.beginSave() : null;
5643
5607
  try {
5644
5608
  const StorageDb = Modules.StorageDb;
5645
5609
  const Migration = Modules.StorageMigration;
5646
- // Welcome-panel + write-guard:
5647
- // post-navigation persistence gate. The normal URL-derived projectId
5648
- // belongs to the route the user is *currently looking at*. When a run
5649
- // is in flight on a different project (mid-run SPA navigation), saving
5650
- // under that URL-derived id would pollute another project's Recent-
5651
- // projects entry and surface bogus disabled rows. Two safety lanes:
5652
- // 1. `options.projectIdOverride` lets a caller that legitimately
5653
- // needs to persist to a non-active project supply the projectId
5654
- // explicitly (the only such caller today is the post-navigation
5655
- // settlement path in `persistPostNavigationRunStatus`, which goes
5656
- // directly through StorageDb — but the override is in place so
5657
- // future callers do not have to monkey-patch around saveState).
5658
- // 2. When no override is set AND a navigation-divergent run is in
5659
- // flight (currentRunView.runProjectId !== URL-derived id), log a
5660
- // warning and skip persistence. The settlement path owns that
5661
- // run's persistence; routing through saveState would write to the
5662
- // WRONG projectId record.
5610
+ // Settlement owns a run after navigation away from its project. Ordinary
5611
+ // view saves must not write that run into the current route's project.
5612
+ // An explicit override is reserved for callers that own a detached scope.
5663
5613
  const projectIdOverride = options && typeof options.projectIdOverride === 'string'
5664
5614
  ? options.projectIdOverride
5665
5615
  : '';
5666
5616
  const urlProjectId = getCurrentProjectId();
5667
5617
  const projectId = projectIdOverride || urlProjectId;
5668
5618
  if (!projectId) {
5669
- if (options?.reviewRunIds?.length) throw new Error('Review persistence lost its project.'); return;
5619
+ if (options?.requireDurable || options?.reviewRunIds?.length) throw new Error('Persistence lost its project.'); return;
5670
5620
  }
5671
5621
  // Read currentRunView through a defensive accessor so historical test
5672
5622
  // harnesses that inline `saveState` without re-declaring the module-
@@ -5678,13 +5628,14 @@
5678
5628
  'Skipped saveState: run is in flight on a different project than the current URL; settlement owns persistence.',
5679
5629
  '已跳过 saveState:本次运行所属项目与当前 URL 不一致,写回由 settlement 负责。'
5680
5630
  ));
5681
- if (options?.reviewRunIds?.length) throw new Error('Review persistence project changed.'); return;
5631
+ if (options?.requireDurable || options?.reviewRunIds?.length) throw new Error('Persistence project changed.'); return;
5682
5632
  }
5683
5633
  const compactState = prepareStateForStorage(state, { onAggressive: notifyAggressiveCompactionOnce });
5684
5634
  compactState.autoRecompile = state.autoRecompile;
5685
5635
  compactState.loadCodexLocalSkills = state.loadCodexLocalSkills !== false;
5686
5636
  compactState.loadCodexOverleafSkills = state.loadCodexOverleafSkills !== false;
5687
5637
  compactState.codexOverleafSkillEnabled = getCodexOverleafSkillEnabled();
5638
+ compactState.codexLocalSkillEnabled = getCodexLocalSkillEnabled();
5688
5639
  compactState.experimentalOtByProject = state.experimentalOtByProject;
5689
5640
  compactState.customInstructionsByProject = state.customInstructionsByProject;
5690
5641
  compactState.governanceRulesByProject = state.governanceRulesByProject;
@@ -5695,11 +5646,14 @@
5695
5646
  ? structuredClone(compactState)
5696
5647
  : JSON.parse(JSON.stringify(compactState));
5697
5648
 
5698
- const persistScopedState = persistenceContext => Modules.ScopedPersistenceCoordinator.persistPanelState({
5649
+ const retentionScope = { accountScopeId: typeof cachedAccountScopeId === 'string' ? cachedAccountScopeId : '', projectId, generation: typeof spaRouteGeneration === 'number' ? spaRouteGeneration : null };
5650
+ let written;
5651
+ const persistScopedState = async persistenceContext => {
5652
+ written = await Modules.ScopedPersistenceCoordinator.persistPanelState({
5699
5653
  state: stateSnapshot,
5700
5654
  compactState: compactStateSnapshot,
5701
5655
  projectId,
5702
- deletedSessionIds: options?.deletedSessionIds, reviewRunIds: options?.reviewRunIds,
5656
+ deletedSessionIds: options?.deletedSessionIds, reviewRunIds: options?.reviewRunIds, recoveryRuns: options?.recoveryRuns,
5703
5657
  Migration,
5704
5658
  StorageDb,
5705
5659
  SessionPersistence: Modules.SessionPersistence,
@@ -5707,7 +5661,9 @@
5707
5661
  normalizeGovernanceRulesByProject,
5708
5662
  normalizeCustomInstructionsByProject,
5709
5663
  persistenceContext
5710
- });
5664
+ });
5665
+ await getComposerDraftPersistence().save({ accountScopeId: persistenceContext.scope.accountScopeId, projectId });
5666
+ };
5711
5667
 
5712
5668
  const persistenceCoordinator = getScopedPersistenceCoordinator();
5713
5669
  if (!persistenceCoordinator) {
@@ -5727,7 +5683,7 @@
5727
5683
  persistScopedState
5728
5684
  );
5729
5685
  if (!committed.ok && (
5730
- options?.reviewRunIds?.length || options?.queueMutation
5686
+ options?.requireDurable || options?.reviewRunIds?.length || options?.recoveryRuns?.length || options?.queueMutation
5731
5687
  || options?.queueMutations?.length
5732
5688
  || committed.reason !== 'stale_view'
5733
5689
  )) {
@@ -5735,8 +5691,23 @@
5735
5691
  code: committed.reason
5736
5692
  });
5737
5693
  }
5694
+ if (committed.ok) {
5695
+ const retentionUpdates = StorageDb.projectRetentionUpdates?.({ liveState: state, capturedState: stateSnapshot, written,
5696
+ capturedScope: retentionScope, currentScope: { accountScopeId: typeof cachedAccountScopeId === 'string' ? cachedAccountScopeId : '',
5697
+ projectId: getCurrentProjectId(), generation: typeof spaRouteGeneration === 'number' ? spaRouteGeneration : null } }) || [];
5698
+ for (const update of retentionUpdates) for (const run of [...(state.runs || []), ...(state.sessions || []).flatMap(session => session.runs || [])]) {
5699
+ if (run.id !== update.runId) continue;
5700
+ if (update.retention) run.recoveryRetention = update.retention; else delete run.recoveryRetention;
5701
+ }
5702
+ if (retentionUpdates.some(update => update.retention)) appendStorageNoticeOnce('undo-retention', tx(
5703
+ 'Some recovery evidence was not retained durably. After reloading, check Overleaf history or Review.',
5704
+ '部分恢复材料未持久保留;刷新后请核对 Overleaf 历史或审阅。'));
5705
+ }
5706
+ if (options?.requireDurable || options?.recoveryRuns?.length) return true;
5738
5707
  }
5739
5708
  } catch (error) {
5709
+ if (options?.requireDurable) throw error;
5710
+ if (error?.code === 'recovery_scope_changed') return;
5740
5711
  if (
5741
5712
  error?.code === 'account_scope_unavailable' &&
5742
5713
  !options?.reviewRunIds?.length && !options?.queueMutation &&
@@ -5764,19 +5735,21 @@
5764
5735
  emitStorageQuotaFailure(error, fallbackError);
5765
5736
  }
5766
5737
  if (typeof appendStorageNoticeOnce === 'function') {
5767
- appendStorageNoticeOnce('save-failed', tx(`Failed to save session state: ${error.message}`, `保存会话状态失败:${error.message}`));
5738
+ appendStorageNoticeOnce('save-failed', tx(`Failed to save session state: ${localizeVisibleReason(error.message)}`, `保存会话状态失败:${localizeVisibleReason(error.message)}`));
5768
5739
  } else {
5769
5740
  throw error;
5770
5741
  }
5742
+ } finally {
5743
+ endSharedSave?.();
5771
5744
  }
5772
5745
  }
5773
5746
 
5774
-
5775
5747
  function prepareCompactFallbackState(inputState) {
5776
5748
  const compact = prepareStateForStorage(inputState);
5777
5749
  return {
5778
5750
  ...compact,
5779
5751
  __codexOverleafCompactFallback: true,
5752
+ composerAttachments: [],
5780
5753
  task: '',
5781
5754
  session: null,
5782
5755
  sessions: [],
@@ -5869,7 +5842,7 @@
5869
5842
  // here (e.g. fallback chrome.storage.local.set rethrew).
5870
5843
  emitStorageQuotaFailure(error, null);
5871
5844
  }
5872
- if (persistenceOptions?.reviewRunIds?.length) throw error;
5845
+ if (persistenceOptions?.requireDurable || persistenceOptions?.reviewRunIds?.length) throw error;
5873
5846
  appendPlainLog(tx(`Failed to save session state: ${formatStateSaveError(error)}`, `保存会话状态失败:${formatStateSaveError(error)}`));
5874
5847
  })
5875
5848
  .finally(() => {
@@ -5945,7 +5918,7 @@
5945
5918
  if (isStorageQuotaError(error)) {
5946
5919
  return tx('Local session history is too large. Delete some old tasks and retry.', '本地会话记录太大,请删除一些旧任务后重试。');
5947
5920
  }
5948
- return error?.message || String(error);
5921
+ return localizeVisibleReason(error?.message || String(error));
5949
5922
  }
5950
5923
 
5951
5924
  async function persistPanelInputs(event) {
@@ -6004,6 +5977,7 @@
6004
5977
  button.dataset.active = active ? 'true' : 'false';
6005
5978
  button.setAttribute('aria-pressed', active ? 'true' : 'false');
6006
5979
  });
5980
+ syncProbeStatus(currentMode);
6007
5981
  }
6008
5982
 
6009
5983
  function readPanelInputs() {
@@ -6056,18 +6030,59 @@
6056
6030
  return speedSelect?.value || state?.speedTier || 'standard';
6057
6031
  }
6058
6032
 
6059
-
6060
- let composerAttachmentsRestoredOnce = false;
6033
+ let composerDraftPersistence = null;
6061
6034
  let lastRunActivityBumpMs = 0;
6062
6035
 
6036
+ function getComposerDraftPersistence() {
6037
+ if (!composerDraftPersistence) composerDraftPersistence = Modules.ComposerAttachmentDraft.createController({
6038
+ getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId() }),
6039
+ getAttachments: () => composerAttachmentController.serializePersistableAttachments(),
6040
+ setAttachments: value => { state.composerAttachments = value; },
6041
+ clear: () => composerAttachmentController.clear({ notify: false }),
6042
+ restore: value => composerAttachmentController.restorePersistedAttachments(value),
6043
+ read: scope => Modules.StorageDb.readComposerAttachmentDraft(scope),
6044
+ write: (scope, value) => Modules.StorageDb.writeComposerAttachmentDraft(scope, value),
6045
+ scheduleSave: () => saveStateSoon()
6046
+ });
6047
+ return composerDraftPersistence;
6048
+ }
6049
+
6050
+ async function loadComposerAttachmentsForProject(accountScopeId, projectId) {
6051
+ if (!accountScopeId || !projectId) return [];
6052
+ try {
6053
+ return await getComposerDraftPersistence().load({ accountScopeId, projectId });
6054
+ } catch (error) {
6055
+ if (error?.code === 'stale_hydration' || error?.code === 'account_scope_unavailable') throw error;
6056
+ appendPlainLog(tx('Saved attachments could not be restored. Reattach the files before sending.',
6057
+ '已保存的附件无法恢复,请在发送前重新添加文件。'));
6058
+ return [];
6059
+ }
6060
+ }
6061
+
6063
6062
  function persistComposerAttachmentsToState() {
6064
6063
  if (typeof composerAttachmentController?.serializePersistableAttachments === 'function') {
6065
6064
  state.composerAttachments = composerAttachmentController.serializePersistableAttachments();
6066
6065
  }
6067
6066
  }
6068
6067
 
6069
- function clearTaskComposer({ keepAttachments = false } = {}) {
6068
+ function clearTaskComposer({ keepAttachments = false, submission = null } = {}) {
6070
6069
  const taskInput = panel?.querySelector('[data-task]');
6070
+ if (submission) {
6071
+ if (submission.projectId !== getCurrentProjectId()
6072
+ || submission.accountScopeId !== cachedAccountScopeId) return false;
6073
+ if (findSessionById(submission.sessionId)?.task !== submission.task) return false;
6074
+ if (state.activeSessionId !== submission.sessionId) {
6075
+ updateSessionById(submission.sessionId, { task: '' });
6076
+ saveStateSoon();
6077
+ return false;
6078
+ }
6079
+ if (composerAttachmentController.getAttachmentByteUsage?.().pending > 0
6080
+ || selectionContextView.getRevision?.() !== submission.selectionRevision) return false;
6081
+ if (!runController.isSubmittedComposerUnchanged(submission, {
6082
+ task: taskInput?.value, attachments: getComposerAttachmentsForRun(),
6083
+ skillInvocation: getComposerSkillInvocationForRun(), selectionContext: selectionContextView.capture()
6084
+ })) return false;
6085
+ }
6071
6086
  if (taskInput) {
6072
6087
  taskInput.value = '';
6073
6088
  }
@@ -6077,11 +6092,38 @@
6077
6092
  }
6078
6093
  composerSkillInvocation = null;
6079
6094
  renderComposerSkillInvocation();
6095
+ selectionContextView.clear();
6080
6096
  state = updateActiveSession(state, { task: '' });
6081
6097
  saveStateSoon();
6098
+ return true;
6099
+ }
6100
+
6101
+ function restoreRejectedLocalSkillSubmission(submission, checkpoint) {
6102
+ const input = panel?.querySelector('[data-task]');
6103
+ if (!checkpoint || !input || submission.accountScopeId !== cachedAccountScopeId
6104
+ || submission.projectId !== getCurrentProjectId() || submission.sessionId !== state.activeSessionId
6105
+ || !findSessionById(submission.sessionId) || input.value || state.task
6106
+ || composerDraftRevision !== checkpoint.draftRevision
6107
+ || getComposerAttachmentsForRun().length || composerAttachmentController.getAttachmentByteUsage?.().pending
6108
+ || composerAttachmentController.getRevision?.() !== checkpoint.attachmentRevision
6109
+ || selectionContextView.getRevision?.() !== checkpoint.selectionRevision
6110
+ || selectionContextView.capture() || getComposerSkillInvocationForRun()) return false;
6111
+ if (!composerAttachmentController.restoreSubmittedAttachments?.(submission.attachments, checkpoint.attachmentRevision)) return false;
6112
+ composerDraftRevision += 1;
6113
+ composerSkillInvocation = submission.skillInvocation;
6114
+ input.value = submission.task;
6115
+ state = updateActiveSession(state, { task: submission.task, selectionContext: submission.selectionContext });
6116
+ renderComposerSkillInvocation();
6117
+ selectionContextView.render();
6118
+ autosizeTaskTextarea();
6119
+ syncComposerSendAvailability();
6120
+ saveStateSoon();
6121
+ return true;
6082
6122
  }
6083
6123
 
6084
6124
  function applyStateToPanel(options = {}) {
6125
+ selectionContextView.render();
6126
+ writingStyleSettings.sync();
6085
6127
  applyPanelTheme(getThemePreference());
6086
6128
  if ((state.providerId || 'builtin') === 'builtin' && !modelSelectHasOption(state.model)) {
6087
6129
  renderModelOptions(getModelCatalog().FALLBACK_MODELS, state.model);
@@ -6118,23 +6160,14 @@
6118
6160
  applySessionLabel();
6119
6161
  renderSessionList();
6120
6162
  renderRunHistory(options);
6163
+ renderPendingInputs();
6121
6164
  getTrackedChangeCaptureController().schedule();
6122
6165
  renderContextSelection();
6123
6166
  renderContextSummary();
6124
- // v1.8.0 C4: refresh recovery — small attachments persisted in state are
6125
- // restored into the (empty) tray ONCE per page load, before the render
6126
- // below. Re-running the gate on later renders would resurrect files the
6127
- // user explicitly removed (fleet P2).
6128
- if (!composerAttachmentsRestoredOnce
6129
- && Array.isArray(state.composerAttachments) && state.composerAttachments.length
6130
- && !composerAttachmentController.getAttachmentsForRun?.().length
6131
- && typeof composerAttachmentController.restorePersistedAttachments === 'function') {
6132
- composerAttachmentController.restorePersistedAttachments(state.composerAttachments);
6133
- }
6134
- composerAttachmentsRestoredOnce = true;
6167
+ getComposerDraftPersistence().hydrate(state.composerAttachments);
6135
6168
  renderComposerAttachments();
6136
6169
  renderComposerSkillInvocation();
6137
- applyLocaleToPanel();
6170
+ applyLocaleToPanel({ reprojectDynamic: false });
6138
6171
  if (!panel.querySelector('[data-context-tray]')?.hidden) {
6139
6172
  renderContextFiles();
6140
6173
  }
@@ -6159,21 +6192,45 @@
6159
6192
  }
6160
6193
  panel.dataset.running = running ? 'true' : 'false';
6161
6194
  panel.dataset.cancelling = running && runCancellationRequested ? 'true' : 'false';
6195
+ panel.querySelectorAll('[data-run-id]').forEach(root => {
6196
+ const record = findRunRecord(root.dataset.runId);
6197
+ if (record) configureEditButton(root, record, { running });
6198
+ if (record) configureWritebackButton(root, record, { running });
6199
+ });
6162
6200
  renderPendingInputs();
6163
6201
  }
6164
6202
 
6165
- function prepareRetryReplacement(run) {
6203
+ function prepareRetryReplacement(run, task) {
6166
6204
  const sessionId = state?.activeSessionId || '';
6167
6205
  const source = findRunRecord(run?.id, sessionId);
6168
- const eligible = source && runController.isRetryReplacementEligible(
6206
+ const eligible = source && !source.forkSnapshot
6207
+ && (!source.runProjectId || source.runProjectId === getCurrentProjectId())
6208
+ && runController.isRetryReplacementEligible(
6169
6209
  source,
6170
6210
  WritebackSettlement.projectRunSettlement(source)
6171
6211
  );
6172
- pendingRetryReplacement = eligible ? { sessionId, runId: source.id } : null;
6212
+ const returnToDraft = eligible && !currentRunView
6213
+ && ['cancelled', 'rejected'].includes(source.status);
6214
+ const draft = { task: typeof task === 'string' ? task : String(run?.task || '') };
6215
+ if (returnToDraft) {
6216
+ const snapshot = source.executionSnapshot || {};
6217
+ const mode = snapshot.mode || source.mode;
6218
+ draft.runs = (state.runs || []).filter(item => item.id !== source.id);
6219
+ draft.mode = ['ask', 'auto'].includes(mode) ? mode : state.mode;
6220
+ draft.focusFiles = Array.isArray(snapshot.focusFiles) ? snapshot.focusFiles : state.focusFiles;
6221
+ draft.selectionContext = snapshot.selectionContext || null;
6222
+ }
6223
+ // Persist the composer through its session owner, not only the top-level alias.
6224
+ state = updateActiveSession(state, draft);
6225
+ pendingRetryReplacement = eligible && !returnToDraft ? { sessionId, runId: source.id } : null;
6226
+ if (returnToDraft) applyStateToPanel({ preserveScroll: true });
6173
6227
  return Boolean(eligible);
6174
6228
  }
6175
6229
 
6176
6230
  function startRunView({
6231
+ sessionId = state.activeSessionId,
6232
+ sessionContext = runController.captureSessionContext(state),
6233
+ codexThreadId = '',
6177
6234
  task,
6178
6235
  mode,
6179
6236
  model,
@@ -6214,41 +6271,39 @@
6214
6271
  executionSnapshot: executionSnapshot
6215
6272
  ? RunExecutionSnapshot.cloneSnapshot(executionSnapshot)
6216
6273
  : undefined,
6217
- // Welcome-panel + write-guard:
6218
- // Immutable per-run capture of the project this run was submitted
6219
- // against. Every writeback / accept / undo dispatch attaches this id
6220
- // to its page-bridge params so the page-side guard can verify the run
6221
- // is still acting on the same project before mutating the editor. No
6222
- // language-level enforcement — the field is treated as immutable by
6223
- // convention and never reassigned anywhere downstream.
6274
+ // Writeback/Accept/Undo carry this immutable project identity to Page.
6224
6275
  runProjectId: getCurrentProjectId() || '',
6225
6276
  runAccountScopeId: cachedAccountScopeId || ''
6226
6277
  };
6227
- const active = getActiveSession(state);
6278
+ const active = findSessionById(sessionId);
6228
6279
  const titlePatch = active?.titleSource !== 'manual'
6229
6280
  ? {
6230
6281
  title: deriveSessionTitle([], task),
6231
6282
  titleSource: 'auto'
6232
6283
  }
6233
6284
  : {};
6234
- state = updateActiveSession(state, {
6235
- runs: runController.replaceRunForRetry(state.runs, replaceRunId, record),
6285
+ updateSessionById(sessionId, {
6286
+ runs: runController.replaceRunForRetry(active?.runs, replaceRunId, record),
6236
6287
  ...titlePatch
6237
6288
  });
6238
6289
  saveStateSoon(replaceRunId ? 0 : 120);
6239
6290
 
6240
6291
  const log = panel.querySelector('[data-log]');
6241
- removeEmptyRunsMessage(log);
6242
- if (replaceRunId) log.querySelector(`[data-run-id="${cssEscape(replaceRunId)}"]`)?.remove();
6243
6292
  const root = renderRunCard(record);
6244
- log.append(root);
6245
- scrollLogToBottom({ force: true });
6293
+ if (state.activeSessionId === sessionId) {
6294
+ removeEmptyRunsMessage(log);
6295
+ if (replaceRunId) log.querySelector(`[data-run-id="${cssEscape(replaceRunId)}"]`)?.remove();
6296
+ log.append(root);
6297
+ scrollLogToBottom({ force: true });
6298
+ }
6246
6299
  startRunElapsedTick();
6247
6300
  renderSessionList();
6248
6301
  applySessionLabel();
6249
6302
 
6250
6303
  return {
6251
- sessionId: state.activeSessionId,
6304
+ sessionId,
6305
+ sessionContext,
6306
+ codexThreadId,
6252
6307
  recordId: record.id,
6253
6308
  // Mirror the immutable runProjectId on the view so writeback /
6254
6309
  // accept / undo dispatches don't need to re-look up the record.
@@ -6346,6 +6401,17 @@
6346
6401
  return;
6347
6402
  }
6348
6403
 
6404
+ const owner = currentRunView && findRunRecord(currentRunView.recordId, currentRunView.sessionId);
6405
+ if (owner && Modules.SubagentActivityModel.ingest(owner, event, {
6406
+ locale: getLocale(), sanitize: sanitizeAssistantVisibleText, nativeEventSeq: journalSeq
6407
+ })) {
6408
+ owner.nativeEventSeq = Math.max(Number(owner.nativeEventSeq || 0), Number(journalSeq || 0));
6409
+ scheduleRunStateSave('stream');
6410
+ const visible = getCurrentRunViewForRender();
6411
+ if (visible) runTimelineView.refreshActivitySummary?.(visible);
6412
+ return;
6413
+ }
6414
+
6349
6415
  const activity = mapAgentEventToActivity(event, { locale: getLocale() });
6350
6416
  if (!activity?.visible || activity.kind === 'technical') {
6351
6417
  return;
@@ -6415,9 +6481,11 @@
6415
6481
  kind: input.kind || 'activity',
6416
6482
  subagent: input.subagent === true ? true : undefined,
6417
6483
  technicalDetail: sanitizeAssistantVisibleValue(input.technicalDetail),
6484
+ activity: Modules.RunActivityModel.capture(input, sanitizeAssistantVisibleText),
6418
6485
  guidanceId: sanitizeAssistantVisibleText(input.guidanceId),
6419
6486
  streamKey: sanitizeAssistantVisibleText(input.streamKey),
6420
6487
  streamRole: sanitizeAssistantVisibleText(input.streamRole),
6488
+ streamPhase: sanitizeAssistantVisibleText(input.streamPhase),
6421
6489
  appendText: typeof input.appendText === 'string' ? sanitizeAssistantVisibleText(input.appendText) : input.appendText,
6422
6490
  replaceText: input.replaceText,
6423
6491
  // Structured FailureReason (§7) attached to the event when the caller
@@ -6531,6 +6599,7 @@
6531
6599
  }
6532
6600
 
6533
6601
  function appendRunEventToView(view, event) {
6602
+ const projected = event.kind !== 'stream' && runTimelineView.refreshActivitySummary?.(view, event);
6534
6603
  if (event.kind === 'report') {
6535
6604
  view.report.hidden = false;
6536
6605
  const record = view?.recordId ? findRunRecord(view.recordId, view.sessionId) : null;
@@ -6541,6 +6610,7 @@
6541
6610
  if (event.kind === 'technical') {
6542
6611
  return;
6543
6612
  }
6613
+ if (projected) { bumpUnreadIfDetached(); return; }
6544
6614
  if (runGuidanceController.appendToView(event, view)) return;
6545
6615
  if (event.kind === 'stream') {
6546
6616
  // Stream deltas update an existing line in place — they must not inflate
@@ -6548,7 +6618,7 @@
6548
6618
  upsertStreamEvent(view, event);
6549
6619
  return;
6550
6620
  }
6551
- view.events.append(renderRunEvent(event));
6621
+ runTimelineView.appendActivityDetail(view, renderRunEvent(event), event);
6552
6622
  bumpUnreadIfDetached();
6553
6623
  }
6554
6624
 
@@ -6559,6 +6629,7 @@
6559
6629
  if (existing) {
6560
6630
  const nextTitle = sanitizeAssistantVisibleText(event.title);
6561
6631
  const nextRole = sanitizeAssistantVisibleText(event.streamRole) || existing.streamRole;
6632
+ if (event.streamPhase) existing.streamPhase = event.streamPhase;
6562
6633
  const combinedTitle = event.replaceText
6563
6634
  ? nextTitle
6564
6635
  : sanitizeAssistantVisibleText(appendStreamText(existing.title, nextTitle));
@@ -6618,10 +6689,6 @@
6618
6689
  }
6619
6690
 
6620
6691
 
6621
-
6622
-
6623
-
6624
-
6625
6692
  function applyPanelWidth(width, options = {}) {
6626
6693
  const nextWidth = PanelRenderer.setWidth(panelRendererInstance, width, { notify: false });
6627
6694
  if (state) {
@@ -6633,10 +6700,6 @@
6633
6700
  return nextWidth;
6634
6701
  }
6635
6702
 
6636
-
6637
-
6638
-
6639
-
6640
6703
  function getCurrentProjectMathSources() {
6641
6704
  const sources = [];
6642
6705
  const seen = new Set();
@@ -6799,277 +6862,34 @@
6799
6862
  );
6800
6863
  }
6801
6864
 
6802
- async function undoRun(runId) {
6803
- const run = findRunRecord(runId);
6804
- if (!getRunUndoCount(run) || run.undoStatus === 'applied') {
6805
- return;
6806
- }
6807
- // Wait out any background mirror refresh before undoing (v1.7.5): a
6808
- // mirror.sync finishing after the undo would push the pre-undo snapshot
6809
- // as the local baseline and resurrect the undone content next run.
6810
- const pendingMirror = getPendingMirrorRefresh?.();
6811
- if (pendingMirror) {
6812
- await pendingMirror.catch(() => {});
6813
- }
6814
-
6815
- if ((Array.isArray(run.undoTrackedChanges) && run.undoTrackedChanges.length) || hasTrackedEditorUndo(run)) {
6816
- await undoRunTrackedChanges(runId, run);
6817
- return;
6818
- }
6819
-
6820
- const undoRestore = buildNoTraceUndoRestore(run);
6821
- const undoOperations = undoRestore.operations;
6822
- const unsafeFullFileUndo = findUnsafeFullFileUndoOperation(undoOperations, {
6823
- allowSnapshotRestore: undoRestore.snapshotRestore
6824
- });
6825
- if (unsafeFullFileUndo) {
6826
- appendRunRecordEvent(runId, {
6827
- title: tr('undoUnsafeFullFileTitle'),
6828
- status: 'failed',
6829
- detail: {
6830
- [tr('detailFile')]: unsafeFullFileUndo.path,
6831
- [tr('detailReason')]: tr('undoUnsafeFullFileReason')
6832
- }
6833
- });
6834
- return;
6835
- }
6836
-
6837
- // v1.8.0 C3: multi-file undos confirm through a per-file selection —
6838
- // undoing a subset leaves undoStatus 'partial' and the button usable;
6839
- // re-undoing an already-restored file is skipped by the base check.
6840
- const undoPaths = Array.from(new Set(undoOperations.map(operation => operation?.path).filter(Boolean)));
6841
- let selectedOperations = undoOperations;
6842
- if (undoPaths.length > 1 && typeof showUndoFileSelection === 'function') {
6843
- const selectedPaths = await showUndoFileSelection({
6844
- title: tr('undoFileSelectTitle'),
6845
- task: truncateRunTitle(run.task),
6846
- confirmLabel: tr('undoFileSelectConfirm'),
6847
- cancelLabel: tr('confirmDefaultCancel'),
6848
- selectAllLabel: tr('undoFileSelectAll'),
6849
- paths: undoPaths
6850
- });
6851
- if (!selectedPaths) {
6852
- return;
6853
- }
6854
- const selectedSet = new Set(selectedPaths);
6855
- selectedOperations = undoOperations.filter(operation => selectedSet.has(operation?.path));
6856
- } else {
6857
- const approved = await showPluginConfirm({
6858
- title: tr('undoNoTraceTitle'),
6859
- message: [
6860
- truncateRunTitle(run.task),
6861
- '',
6862
- tr('undoNoTraceMessage', { files: formatOperationFiles(undoOperations) })
6863
- ].join('\n'),
6864
- confirmLabel: tr('undoConfirm'),
6865
- cancelLabel: tr('confirmDefaultCancel'),
6866
- destructive: true
6867
- });
6868
- if (!approved) {
6869
- return;
6870
- }
6871
- }
6872
-
6873
- setRunUndoStatus(runId, 'running');
6874
- appendRunRecordEvent(runId, {
6875
- title: tr('undoNoTraceStarted'),
6876
- status: 'running',
6877
- detail: { [tr('detailWillUndo')]: formatOperationFiles(selectedOperations) }
6865
+ let runReviewController = null;
6866
+ function getRunReviewController() {
6867
+ if (!runReviewController) runReviewController = Modules.RunReviewController.create({
6868
+ Modules, WritebackSettlement, trackedChangeInFlight, findRunRecord,
6869
+ getRunUndoCount, isTrackedChangeLifecycleRun, getPendingMirrorRefresh, hasTrackedEditorUndo, buildNoTraceUndoRestore,
6870
+ findUnsafeFullFileUndoOperation, showUndoFileSelection, showPluginConfirm, tr,
6871
+ truncateRunTitle, formatOperationFiles, formatTrackedChangeFiles, formatTrackedUndoFiles,
6872
+ getPageBridgeTimeoutMs, callPageBridge, setRunUndoStatus, appendRunRecordEvent, getRunProjectIdForWriteback,
6873
+ isUndoResultEffectivelyApplied, writebackOrchestrator, appendUndoReviewingPolicyEvent, formatOperationType,
6874
+ formatApplyResultReason, buildTrackedUndoPostFiles, getTrackedChangeCaptureController, buildContentFailure,
6875
+ applyTrackedChangeSettlement, refreshRunCardControls, formatBridgeResultReason, advanceRunUndoProgress,
6876
+ saveStateSoon, flushQueuedSaveState,
6877
+ getState: () => state, getCurrentProjectId, getAccountScopeId: () => cachedAccountScopeId,
6878
+ getRouteGeneration: () => spaRouteGeneration, tx, notify: message => showPluginToast(message, { status: 'warning', sticky: true })
6878
6879
  });
6880
+ return runReviewController;
6881
+ }
6879
6882
 
6880
- const result = await callPageBridge('applyOperations', {
6881
- operations: selectedOperations,
6882
- baseFiles: run.undoBaseFiles || [],
6883
- reviewingPolicy: 'no-trace-undo',
6884
- // Welcome-panel + write-guard:
6885
- // route the undo through the same project-ID guard. The undo is
6886
- // bound to the run's original project, not the editor's active one.
6887
- runProjectId: getRunProjectIdForWriteback(run)
6888
- });
6889
- const undoApplied = isUndoResultEffectivelyApplied(run, result);
6890
- writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
6891
- appendUndoReviewingPolicyEvent(runId, result.reviewingPolicy);
6892
- appendRunRecordEvent(runId, {
6893
- title: tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
6894
- status: undoApplied ? 'completed' : result.skipped?.length ? 'failed' : 'completed',
6895
- detail: {
6896
- [tr('detailUndone')]: (result.applied || []).map(item => ({
6897
- [tr('detailAction')]: formatOperationType(item.operation?.type),
6898
- [tr('detailFile')]: item.operation?.path
6899
- })),
6900
- [tr('detailSkipped')]: (result.skipped || []).map(item => ({
6901
- [tr('detailAction')]: formatOperationType(item.operation?.type),
6902
- [tr('detailFile')]: item.operation?.path,
6903
- [tr('detailReason')]: formatApplyResultReason(item)
6904
- }))
6905
- }
6906
- });
6907
- // A subset undo never closes the ledger: unselected files can still
6908
- // be undone later, so the run stays 'partial' and the button usable.
6909
- const fullSelection = selectedOperations.length === undoOperations.length;
6910
- applyLegacyUndoSettlement(
6911
- runId,
6912
- undoApplied && fullSelection ? 'applied' : 'partial',
6913
- result
6914
- );
6883
+ async function undoRun(runId) {
6884
+ return getRunReviewController().undoRun(runId);
6915
6885
  }
6916
6886
 
6917
6887
  async function undoRunTrackedChanges(runId, run) {
6918
- const trackedUndo = Array.isArray(run.undoTrackedChanges) && run.undoTrackedChanges.length > 0;
6919
- const approved = await showPluginConfirm({
6920
- title: trackedUndo ? tr('undoTrackedTitle') : tr('undoNativeTitle'),
6921
- message: [
6922
- truncateRunTitle(run.task),
6923
- '',
6924
- trackedUndo
6925
- ? tr('undoTrackedMessage', { files: formatTrackedChangeFiles(run.undoTrackedChanges) })
6926
- : tr('undoNativeMessage', { files: formatTrackedUndoFiles(run) })
6927
- ].join('\n'),
6928
- confirmLabel: trackedUndo ? tr('undoTrackedConfirm') : tr('undoConfirm'),
6929
- cancelLabel: tr('confirmDefaultCancel'),
6930
- destructive: true
6931
- });
6932
- if (!approved) {
6933
- return;
6934
- }
6935
-
6936
- // Tracked-change-lifecycle runs (those holding tracked-change refs) reach a
6937
- // decisive terminal `rejected` once the reject returns. The UI-local
6938
- // in-flight lock disables the button; it is never persisted.
6939
- const lifecycleReject = trackedUndo && isTrackedChangeLifecycleRun(run);
6940
- if (lifecycleReject) {
6941
- trackedChangeInFlight.set(runId, 'reject');
6942
- refreshRunCardControls(runId);
6943
- } else {
6944
- setRunUndoStatus(runId, 'running');
6945
- }
6946
- appendRunRecordEvent(runId, {
6947
- title: trackedUndo ? tr('undoTrackedStarted') : tr('undoNativeStarted'),
6948
- status: 'running',
6949
- detail: { [tr('detailWillUndo')]: trackedUndo ? formatTrackedChangeFiles(run.undoTrackedChanges) : formatTrackedUndoFiles(run) }
6950
- });
6951
-
6952
- try {
6953
- const trackedUndoPostFilesAtDispatch = buildTrackedUndoPostFiles(run);
6954
- const result = await callPageBridge('rejectTrackedChanges', {
6955
- trackedChanges: run.undoTrackedChanges || [],
6956
- expectedFiles: run.undoExpectedFiles || [],
6957
- postFiles: trackedUndoPostFilesAtDispatch,
6958
- untrackedUndo: run.executionSnapshot
6959
- ? run.executionSnapshot.requireReviewing === false && ['submitted', 'legacy-captured'].includes(run.executionSnapshot.source)
6960
- : getTrackedChangeCaptureController().hasLegacyUntrackedCheckpoint(run),
6961
- // Welcome-panel + write-guard:
6962
- // bind the reject to the run's original project. If the user has
6963
- // navigated away the page-side guard refuses with
6964
- // `aborted_project_changed` and the document is left untouched.
6965
- runProjectId: getRunProjectIdForWriteback(run)
6966
- });
6967
- writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
6968
- appendRunRecordEvent(runId, {
6969
- title: trackedUndo
6970
- ? tr('undoTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 })
6971
- : tr('undoNativeResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
6972
- status: result.skipped?.length ? 'failed' : 'completed',
6973
- detail: {
6974
- [trackedUndo ? tr('detailRejected') : tr('detailUndone')]: (result.applied || []).map(item => ({
6975
- [tr('detailFile')]: item.trackedChange?.path || tr('unknownFile'),
6976
- [tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key
6977
- })),
6978
- [tr('detailSkipped')]: (result.skipped || []).map(item => ({
6979
- [tr('detailFile')]: item.trackedChange?.path || tr('unknownFile'),
6980
- [tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key || '',
6981
- [tr('detailReason')]: formatBridgeResultReason(item.result, item.trackedChange?.path)
6982
- }))
6983
- }
6984
- });
6985
- if (lifecycleReject) {
6986
- WritebackSettlement.attachUndoNotVerifiedFailure(run, result, {
6987
- buildFailure: buildContentFailure,
6988
- postFiles: trackedUndoPostFilesAtDispatch
6989
- });
6990
- await applyTrackedChangeSettlement(runId, 'reject', result);
6991
- return;
6992
- }
6993
- applyLegacyUndoSettlement(runId, result.skipped?.length ? 'partial' : 'applied', result);
6994
- } finally {
6995
- if (lifecycleReject) {
6996
- trackedChangeInFlight.delete(runId);
6997
- refreshRunCardControls(runId);
6998
- }
6999
- }
6888
+ return getRunReviewController().undoRunTrackedChanges(runId, run);
7000
6889
  }
7001
6890
 
7002
6891
  async function acceptRun(runId) {
7003
- const run = findRunRecord(runId);
7004
- if (!run || !isTrackedChangeLifecycleRun(run)) {
7005
- return;
7006
- }
7007
- const status = run.trackedChangeStatus || '';
7008
- // pending and needs_review are both actionable per §7: needs_review means
7009
- // the prior attempt could not prove a clean post-action state, and the
7010
- // user is supposed to be able to retry after inspecting Overleaf.
7011
- if (status !== 'pending' && status !== 'needs_review') {
7012
- return;
7013
- }
7014
- if (!Array.isArray(run.undoTrackedChanges) || !run.undoTrackedChanges.length) {
7015
- return;
7016
- }
7017
- if (trackedChangeInFlight.get(runId)) {
7018
- return;
7019
- }
7020
-
7021
- trackedChangeInFlight.set(runId, 'accept');
7022
- refreshRunCardControls(runId);
7023
- appendRunRecordEvent(runId, {
7024
- title: tr('runAcceptTrackedStarted'),
7025
- status: 'running',
7026
- detail: { [tr('detailAccepted')]: formatTrackedChangeFiles(run.undoTrackedChanges) }
7027
- });
7028
-
7029
- try {
7030
- const result = await callPageBridge('acceptTrackedChanges', {
7031
- trackedChanges: run.undoTrackedChanges || [],
7032
- expectedFiles: run.undoExpectedFiles || [],
7033
- postFiles: buildTrackedUndoPostFiles(run),
7034
- appliedOperations: Array.isArray(run.appliedOperations) ? run.appliedOperations : [],
7035
- runProjectId: getRunProjectIdForWriteback(run)
7036
- });
7037
- appendAcceptDiagnosticEvents(runId, Array.isArray(result.diagnostics) ? result.diagnostics : []);
7038
- appendRunRecordEvent(runId, {
7039
- title: tr('runAcceptTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
7040
- status: result.skipped?.length ? 'failed' : 'completed',
7041
- detail: {
7042
- [tr('detailAccepted')]: (result.applied || []).map(item => ({
7043
- [tr('detailFile')]: item.trackedChange?.path || tr('unknownFile'),
7044
- [tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key
7045
- })),
7046
- [tr('detailSkipped')]: (result.skipped || []).map(item => ({
7047
- [tr('detailFile')]: item.trackedChange?.path || tr('unknownFile'),
7048
- [tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key || '',
7049
- [tr('detailReason')]: formatBridgeResultReason(item.result, item.trackedChange?.path)
7050
- }))
7051
- }
7052
- });
7053
- if (result.ok === false) {
7054
- appendRunRecordEvent(runId, {
7055
- title: tr('runAcceptTrackedFailed'),
7056
- status: 'failed',
7057
- detail: {
7058
- [tr('detailReason')]: tr('runAcceptTrackedFailedReason')
7059
- }
7060
- });
7061
- if (!WritebackSettlement.isSuccessfulTrackedChangeSettlement(result)) {
7062
- refreshRunCardControls(runId);
7063
- }
7064
- }
7065
- WritebackSettlement.attachAcceptNotVerifiedFailure(run, result, {
7066
- buildFailure: buildContentFailure
7067
- });
7068
- await applyTrackedChangeSettlement(runId, 'accept', result);
7069
- } finally {
7070
- trackedChangeInFlight.delete(runId);
7071
- refreshRunCardControls(runId);
7072
- }
6892
+ return getRunReviewController().acceptRun(runId);
7073
6893
  }
7074
6894
 
7075
6895
  function getRunProjectIdForWriteback(run) {
@@ -7078,32 +6898,6 @@
7078
6898
  : '';
7079
6899
  }
7080
6900
 
7081
- // Translates the page bridge's Accept All `diagnostics` array into one
7082
- // run-card event row per step. Each entry is `{ step, info }`; the step
7083
- // names map 1:1 to the runAcceptTrackedStep* i18n keys. We render `info`
7084
- // as the event detail so the user can copy-paste the full trace without
7085
- // any extra page-state query.
7086
- function appendAcceptDiagnosticEvents(runId, diagnostics) {
7087
- for (const entry of diagnostics || []) {
7088
- const step = entry && entry.step;
7089
- const info = (entry && entry.info) || {};
7090
- const titleKey = WritebackSettlement.ACCEPT_DIAGNOSTIC_TITLE_KEYS[step];
7091
- if (!titleKey) {
7092
- continue;
7093
- }
7094
- const title = titleKey === 'runAcceptTrackedStepReplayStart' || titleKey === 'runAcceptTrackedStepReplayDone'
7095
- ? tr(titleKey, { path: info.path || tr('unknownFile') })
7096
- : tr(titleKey);
7097
- const status = WritebackSettlement.acceptDiagnosticStatus(step, info);
7098
- appendRunRecordEvent(runId, {
7099
- title,
7100
- kind: 'activity',
7101
- status,
7102
- detail: info
7103
- });
7104
- }
7105
- }
7106
-
7107
6901
  function buildContentFailure(code, operation, overrides) {
7108
6902
  return WritebackSettlement.buildContentFailure(
7109
6903
  code,
@@ -7113,35 +6907,206 @@
7113
6907
  );
7114
6908
  }
7115
6909
 
7116
- async function applyTrackedChangeSettlement(runId, kind, result) {
7117
- const run = findRunRecord(runId);
6910
+ async function applyTrackedChangeSettlement(runId, kind, result, options = {}) {
6911
+ const record = findRunRecord(runId);
6912
+ const run = kind === 'reject' ? advanceRunUndoProgress(record, result) : record;
7118
6913
  const settlement = WritebackSettlement.settleTrackedChangeLifecycle({
7119
6914
  kind,
7120
6915
  run,
7121
6916
  result,
7122
6917
  failureReasons: FailureReasons
7123
6918
  });
7124
- if (settlement.decision === 'blocked') {
6919
+ if (settlement.decision === 'blocked'
6920
+ && !(kind === 'reject' && (run !== record || record?.undoOperations?.some(Modules.UndoOperations.isFileUndoOperation)))) {
7125
6921
  return;
7126
6922
  }
7127
6923
  await Modules.ScopedPersistenceCoordinator.applyReviewTransition({
7128
6924
  getState: () => state, runId, transition: WritebackSettlement.applySettlementTransition(run, settlement),
7129
- persist: flushQueuedSaveState
6925
+ persist: flushQueuedSaveState, assertCurrent: options.assertCurrent
7130
6926
  });
7131
6927
  }
7132
6928
 
6929
+
6930
+ function advanceRunUndoProgress(run, result) {
6931
+ getDirectoryCleanupRecovery().capture({ recordId: run.id, sessionId: state.activeSessionId,
6932
+ runProjectId: run.runProjectId, runAccountScopeId: run.runAccountScopeId }, result);
6933
+ const postFiles = buildTrackedUndoPostFiles(run);
6934
+ return Modules.UndoOperations.advanceUndoCheckpoint(run, result, file =>
6935
+ WritebackSettlement.isUndoVerifiedContentMatching({ undoExpectedFiles: [file] }, result, { postFiles })
6936
+ );
6937
+ }
7133
6938
  function getRunUndoCount(run) {
7134
6939
  if (!run) {
7135
6940
  return 0;
7136
6941
  }
7137
6942
  const trackedEditorUndoCount = hasTrackedEditorUndo(run) ? 1 : 0;
7138
6943
  if (trackedEditorUndoCount) {
7139
- return trackedEditorUndoCount;
6944
+ return trackedEditorUndoCount + Modules.UndoOperations.getFileUndoOperations(run).length;
7140
6945
  }
7141
- return (run.undoOperations?.length || 0)
6946
+ return Modules.UndoOperations.prepareRunUndo(run).undoOperations.length
7142
6947
  + (run.undoTrackedChanges?.length || 0);
7143
6948
  }
7144
6949
 
6950
+ function recoveryOwner(target) {
6951
+ if (!target.runProjectId || target.runProjectId !== getCurrentProjectId() || !cachedAccountScopeId) {
6952
+ throw new Error('Writeback recovery lost its project or account identity.');
6953
+ }
6954
+ return { projectId: target.runProjectId, accountScopeId: cachedAccountScopeId,
6955
+ sessionId: target.sessionId, runId: target.recordId, createdAt: new Date().toISOString() };
6956
+ }
6957
+
6958
+ let directoryCleanupRecovery = null;
6959
+ function getDirectoryCleanupRecovery() {
6960
+ return directoryCleanupRecovery || (directoryCleanupRecovery = Modules.DirectoryCleanupRecovery.create({
6961
+ getProjectId: getCurrentProjectId, getAccountScopeId: () => cachedAccountScopeId,
6962
+ getRouteGeneration: () => spaRouteGeneration, findRunRecord, refresh: refreshRunCardControls,
6963
+ onOmitted: (runId, code) => appendRunRecordEvent(runId, { kind: 'technical', status: 'warning',
6964
+ title: tx('Some folder observations exceeded the storage limit; existing observations were kept.',
6965
+ '部分文件夹观测记录超过存储上限,已有记录已保留。'), detail: { code } })
6966
+ }));
6967
+ }
6968
+ async function stageWritebackRecovery(operations, target, requireReviewing) {
6969
+ const owner = recoveryOwner(target), generation = spaRouteGeneration, view = currentRunView;
6970
+ const assertCurrent = () => {
6971
+ const record = findRunRecord(target.recordId, target.sessionId);
6972
+ if (owner.projectId !== getCurrentProjectId() || owner.accountScopeId !== cachedAccountScopeId
6973
+ || generation !== spaRouteGeneration || view !== currentRunView || !view
6974
+ || view.recordId !== owner.runId || view.sessionId !== owner.sessionId
6975
+ || (target.runAccountScopeId && target.runAccountScopeId !== owner.accountScopeId)
6976
+ || !record || record.forkSnapshot || record.runProjectId !== owner.projectId) {
6977
+ throw new Error('The project, account or writeback run changed before dispatch.');
6978
+ }
6979
+ return record;
6980
+ };
6981
+ const record = assertCurrent();
6982
+ const creates = operations.filter(op => op.type === 'create' && typeof op.content === 'string');
6983
+ record.retryRequireReviewing = requireReviewing === true;
6984
+ if (!creates.length) return;
6985
+ const candidate = Modules.WritebackIntent.normalize({ ...owner, id: crypto.randomUUID(),
6986
+ operations: creates, baseFiles: [], requireReviewing });
6987
+ const safe = Modules.SessionState.pickWritebackRecovery({ ...record, retryWriteback: candidate }).retryWriteback;
6988
+ if (safe) record.retryWriteback = safe; else delete record.retryWriteback;
6989
+ await flushQueuedSaveState();
6990
+ assertCurrent();
6991
+ }
6992
+
6993
+ async function settleWritebackRecovery(applied, project, verification, target) {
6994
+ const record = findRunRecord(target.recordId, target.sessionId);
6995
+ if (!record) return;
6996
+ const owner = recoveryOwner(target);
6997
+ const skippedPaths = new Set((applied.skipped || []).map(entry => entry.operation?.path));
6998
+ const creates = (record.retryWriteback?.operations || []).filter(op => skippedPaths.has(op.path));
6999
+ // Untouched edits (never opened / rejected) and deletes whose server check failed are safe to replay.
7000
+ const edits = (applied.skipped || []).filter(({ operation: op, result }) => result?.failure?.changedDocument === false && ((op?.type === 'edit'
7001
+ && (result.failure.stage === 'navigation' || result.failure.code === 'write_operation_failed')) || (op?.type === 'delete' && result.failure.code === 'source_zip_unavailable'))).map(entry => entry.operation);
7002
+ const base = { ...(record.retryWriteback || { ...owner, id: crypto.randomUUID(), requireReviewing: record.retryRequireReviewing === true }),
7003
+ baseFiles: project?.files || [], requestId: applied.receiptUnconfirmed ? applied.requestId || '' : '' };
7004
+ // An oversized edit pre-image must not cost the create retries (the intent has a 300 KiB ceiling).
7005
+ const safeRetry = [[...creates, ...edits], creates].filter(ops => ops.length).map(ops => Modules.SessionState
7006
+ .pickWritebackRecovery({ ...record, retryWriteback: Modules.WritebackIntent.normalize({ ...base, operations: ops }) }).retryWriteback).find(Boolean);
7007
+ if (safeRetry) record.retryWriteback = safeRetry; else delete record.retryWriteback;
7008
+ delete record.retryRequireReviewing;
7009
+ const next = await writebackController.buildSaveCheck(applied, project, owner);
7010
+ const files = new Map((record.saveCheck?.files || []).map(file => [file.path, file]));
7011
+ for (const file of next?.files || []) {
7012
+ if (verification.state === 'verified_saved') files.delete(file.path);
7013
+ else files.set(file.path, file);
7014
+ }
7015
+ if (files.size) record.saveCheck = Modules.WritebackIntent.normalizeSaveCheck({ ...owner, files: [...files.values()] });
7016
+ else {
7017
+ delete record.saveCheck;
7018
+ if (verification.state === 'verified_saved') record.saveConfirmedAt = new Date().toISOString();
7019
+ }
7020
+ await flushQueuedSaveState();
7021
+ }
7022
+
7023
+ async function retryRunWriteback(runId) {
7024
+ if (currentRunView || trackedChangeInFlight.size) return;
7025
+ const session = getActiveSession(state);
7026
+ const record = (session?.runs || []).find(run => run.id === runId);
7027
+ const intent = Modules.WritebackIntent.normalize(record?.retryWriteback);
7028
+ const check = Modules.WritebackIntent.normalizeSaveCheck(record?.saveCheck);
7029
+ const owner = intent || check;
7030
+ if (!record || record.forkSnapshot || !owner || owner.projectId !== getCurrentProjectId()
7031
+ || owner.accountScopeId !== cachedAccountScopeId || owner.sessionId !== session.id
7032
+ || owner.runId !== record.id || record.undoStatus === 'applied' || record.trackedChangeStatus === 'rejected') return;
7033
+ const originalStatus = record.status;
7034
+ const retryAutoRecompile = typeof record.executionSnapshot?.autoRecompile === 'boolean'
7035
+ ? record.executionSnapshot.autoRecompile : undefined;
7036
+ // State is renormalized during the awaits below (probes, session sync), which
7037
+ // replaces run objects; always write through the current copy.
7038
+ const live = () => findRunRecord(runId, session.id) || record;
7039
+ let finished = false;
7040
+ runCancellationRequested = false;
7041
+ runCancellationController = new AbortController();
7042
+ record.status = 'running';
7043
+ record.retryingWriteback = true;
7044
+ renderRunHistory({ preserveScroll: true });
7045
+ const root = panel.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
7046
+ currentRunView = { root, recordId: runId, sessionId: session.id, runProjectId: owner.projectId,
7047
+ runAccountScopeId: owner.accountScopeId, executionSnapshot: record.executionSnapshot,
7048
+ startedAt: Date.now(), writebackRetry: true, events: root?.querySelector('[data-run-events]'),
7049
+ report: root?.querySelector('[data-run-report]'), status: root?.querySelector('[data-run-status]'),
7050
+ runProcess: root?.querySelector('[data-run-process]'), processLabel: root?.querySelector('[data-run-process-summary]') };
7051
+ setRunning(true);
7052
+ const assertOwner = () => {
7053
+ if (runCancellationRequested || currentRunView?.recordId !== runId) {
7054
+ throw Object.assign(new Error('Save checking was cancelled.'), { code: 'codex_cancelled' });
7055
+ }
7056
+ if (owner.projectId !== getCurrentProjectId() || owner.accountScopeId !== cachedAccountScopeId) {
7057
+ throw Object.assign(new Error('The project or account changed.'), { code: 'aborted_project_changed' });
7058
+ }
7059
+ };
7060
+ try {
7061
+ assertOwner();
7062
+ if (intent?.requestId) {
7063
+ const receipt = await callPageBridge('getWritebackReceipt', { requestId: intent.requestId, runProjectId: owner.projectId });
7064
+ if (receipt?.state === 'running') throw new Error(tx('The previous upload is still finishing. Try again shortly.', '上一轮上传仍在收尾,请稍后再试。'));
7065
+ }
7066
+ if (intent?.operations.length) {
7067
+ // Edits replay against the run's original base files, so the stale guard still refuses changed documents.
7068
+ const result = await applySyncChangesToOverleaf(intent.operations.map(op => op.type === 'edit'
7069
+ ? { type: 'write', path: op.path, patches: op.patches, content: op.replaceAll } : op.type === 'delete' ? { type: 'delete', path: op.path } : { ...op, type: 'create' }),
7070
+ { id: owner.projectId, files: intent.baseFiles }, { mode: 'auto', requireReviewing: intent.requireReviewing,
7071
+ autoRecompile: retryAutoRecompile,
7072
+ retryCreates: true, assistantMessage: tx('Retried the remaining file transfers for this task.', '已重试本轮未完成的文件同步。') });
7073
+ assertOwner();
7074
+ if (result.settlement?.facts) live().settlement = WritebackSettlement.mergeRetrySettlement(live().settlement, result.settlement.facts); // Keep files outside the retry.
7075
+ await finishRunView(result.hasSkippedOperations ? tx('Sync incomplete', '同步未完成') : tx('Synced', '已同步'),
7076
+ result.hasSkippedOperations ? 'failed' : 'completed');
7077
+ finished = true;
7078
+ } else if (check) {
7079
+ const result = await writebackController.confirmSaveCheck(check, { assertCurrent: assertOwner,
7080
+ readSnapshot: params => callPageBridge('getProjectSnapshot', { ...params, runProjectId: owner.projectId }) });
7081
+ assertOwner();
7082
+ const current = live();
7083
+ if (result.ok) {
7084
+ delete current.saveCheck;
7085
+ current.saveConfirmedAt = new Date().toISOString();
7086
+ if (current.settlement) current.settlement.evidence = { ...current.settlement.evidence, saved: 'verified' };
7087
+ }
7088
+ showPluginToast(result.ok ? tx('Saved content confirmed.', '已确认文件保存。')
7089
+ : tx('Saved content is still unconfirmed. No file was uploaded again.', '保存状态仍待确认,本次没有重复上传文件。'));
7090
+ }
7091
+ await flushQueuedSaveState();
7092
+ } catch (error) {
7093
+ showPluginToast(error.message || String(error));
7094
+ } finally {
7095
+ // Only finishRunView settles a new terminal status; every other exit
7096
+ // returns the card to the status it had before the retry.
7097
+ for (const copy of new Set([record, live()])) {
7098
+ delete copy.retryingWriteback;
7099
+ if (!finished && copy.status === 'running') copy.status = originalStatus;
7100
+ }
7101
+ if (currentRunView?.recordId === runId) currentRunView = null;
7102
+ runCancellationRequested = false;
7103
+ runCancellationController = null;
7104
+ setRunning(false);
7105
+ saveStateSoon();
7106
+ renderRunHistory({ preserveScroll: true });
7107
+ }
7108
+ }
7109
+
7145
7110
  function appendUndoReviewingPolicyEvent(runId, reviewingPolicy) {
7146
7111
  if (!reviewingPolicy || reviewingPolicy.policy !== 'no-trace-undo') {
7147
7112
  return;
@@ -7224,8 +7189,10 @@
7224
7189
  });
7225
7190
  }
7226
7191
 
7227
- function recordUndoFromApply(project, applyResult) {
7228
- const appliedEntries = getAppliedEntries(applyResult);
7192
+ async function recordUndoFromApply(project, applyResult) {
7193
+ const directoryRecovery = Modules.DirectoryCleanupRecovery ? getDirectoryCleanupRecovery() : null;
7194
+ directoryRecovery?.capture(currentRunView, applyResult);
7195
+ const appliedEntries = getAppliedEntries(applyResult).filter(entry => entry.result?.idempotent !== true);
7229
7196
  if (!currentRunView?.recordId || !appliedEntries.length) {
7230
7197
  return;
7231
7198
  }
@@ -7244,14 +7211,15 @@
7244
7211
  ...appliedOperations
7245
7212
  ];
7246
7213
  record.appliedOperations = combinedAppliedOperations;
7214
+ const createdCheckpoint = buildCreatedFileUndoCheckpoint(project, combinedAppliedOperations, record);
7247
7215
 
7248
7216
  if ((currentRunView?.executionSnapshot?.requireReviewing ?? state.requireReviewing) === true) {
7249
7217
  const combinedTrackedChanges = normalizeApplyTrackedChanges([
7250
7218
  ...(Array.isArray(record.undoTrackedChanges) ? record.undoTrackedChanges : []),
7251
7219
  ...trackedChanges
7252
7220
  ]);
7253
- record.undoOperations = [];
7254
- record.undoBaseFiles = [];
7221
+ record.undoOperations = createdCheckpoint.undoOperations;
7222
+ record.undoBaseFiles = createdCheckpoint.undoBaseFiles;
7255
7223
  record.undoTrackedChanges = combinedTrackedChanges;
7256
7224
  record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(
7257
7225
  project,
@@ -7259,6 +7227,7 @@
7259
7227
  combinedTrackedChanges,
7260
7228
  record.undoExpectedFiles
7261
7229
  );
7230
+ record.undoOperations = Modules.UndoOperations.getFileUndoOperations(record);
7262
7231
  record.undoStatus = '';
7263
7232
  record.partialWriteback = skippedEntries.length > 0;
7264
7233
  // Recording tracked-change refs enters the run into the tracked-change
@@ -7286,6 +7255,11 @@
7286
7255
  [tr('detailMethod')]: tr('undoCheckpointNativeMethod')
7287
7256
  }
7288
7257
  });
7258
+ } else if (record.undoOperations.length) {
7259
+ appendRunEvent({
7260
+ title: tr('undoCheckpointPlain', { count: record.undoOperations.length }),
7261
+ status: 'completed'
7262
+ });
7289
7263
  } else {
7290
7264
  appendRunEvent({
7291
7265
  title: tr('undoCheckpointMissing'),
@@ -7300,6 +7274,15 @@
7300
7274
  }
7301
7275
 
7302
7276
  const checkpoint = buildUndoCheckpoint(project, combinedAppliedOperations);
7277
+ const createdPaths = new Set(createdCheckpoint.undoOperations.map(operation => operation.path));
7278
+ checkpoint.undoOperations = [
7279
+ ...checkpoint.undoOperations.filter(operation => !createdPaths.has(operation.path)),
7280
+ ...createdCheckpoint.undoOperations
7281
+ ];
7282
+ checkpoint.undoBaseFiles = [
7283
+ ...checkpoint.undoBaseFiles.filter(file => !createdPaths.has(file.path)),
7284
+ ...createdCheckpoint.undoBaseFiles
7285
+ ];
7303
7286
  if (!checkpoint.undoOperations.length) {
7304
7287
  return;
7305
7288
  }
@@ -7374,8 +7357,10 @@
7374
7357
  timestamp: event.timestamp || new Date().toISOString(),
7375
7358
  kind: event.kind || 'activity',
7376
7359
  technicalDetail: event.technicalDetail,
7360
+ activity: Modules.RunActivityModel.capture(event, sanitizeAssistantVisibleText),
7377
7361
  guidanceId: sanitizeAssistantVisibleText(event.guidanceId)
7378
7362
  };
7363
+ if (event.failure) normalized.failure = sanitizeAssistantVisibleValue(event.failure);
7379
7364
  record.events = [...(record.events || []), normalized].slice(-MAX_RUN_EVENTS);
7380
7365
  saveStateSoon();
7381
7366
 
@@ -7400,29 +7385,24 @@
7400
7385
  refreshRunCardControls(runId);
7401
7386
  }
7402
7387
 
7403
- function applyLegacyUndoSettlement(runId, undoStatus, result) {
7404
- const run = findRunRecord(runId);
7405
- if (!run) return;
7406
- Object.assign(run, WritebackSettlement.applySettlementTransition(
7407
- run,
7408
- WritebackSettlement.settleLegacyUndo({
7409
- run,
7410
- status: undoStatus,
7411
- result
7412
- })
7413
- ));
7414
- saveStateSoon();
7415
- refreshRunCardControls(runId);
7388
+ async function applyLegacyUndoSettlement(runId, undoStatus, result) {
7389
+ return getRunReviewController().applyLegacyUndoSettlement(runId, undoStatus, result);
7416
7390
  }
7417
7391
 
7418
7392
  function refreshRunCardControls(runId) {
7419
- const run = findRunRecord(runId);
7420
- const root = panel?.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
7393
+ const run = findRunRecord(runId), root = panel?.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
7421
7394
  if (!run || !root) {
7422
7395
  return;
7423
7396
  }
7397
+ if (runTimelineView.refreshResolvedUndo?.(runId)) return;
7424
7398
  configureAcceptButton(root, run);
7425
7399
  configureUndoButton(root, run);
7400
+ const reportEvent = (run.events || []).filter(event => event.kind === 'report').pop();
7401
+ const report = root.querySelector('[data-run-report]');
7402
+ if (reportEvent && report) {
7403
+ report.replaceChildren(renderCompletionReport(reportEvent, run));
7404
+ report.hidden = false;
7405
+ }
7426
7406
  }
7427
7407
 
7428
7408
  function findRunRecord(runId, sessionId = '') {
@@ -7543,6 +7523,11 @@
7543
7523
  // v1.8.0 C1: a change-history row jumps to the run it describes —
7544
7524
  // switch to its session if needed, then scroll + flash the run card.
7545
7525
  async function jumpToHistoryRun(record = {}) {
7526
+ if (!cachedAccountScopeId || record.accountScopeId !== cachedAccountScopeId
7527
+ || record.projectId !== getCurrentProjectId()) {
7528
+ showPluginToast(tx('The project or account changed. Reopen file history and try again.', '项目或账号已变化,请重新打开文件修改记录。'));
7529
+ return;
7530
+ }
7546
7531
  closeCustomInstructionsSettings();
7547
7532
  if (record.sessionId && record.sessionId !== state.activeSessionId) {
7548
7533
  const target = (state.sessions || []).find(session => session.id === record.sessionId);
@@ -7621,7 +7606,7 @@
7621
7606
  if (primaryNext) return primaryNext;
7622
7607
  return tx('Expand the write result, review the skipped reasons, then retry after fixing them.', '请展开写入结果查看跳过原因,处理后可以重试。');
7623
7608
  }
7624
- if (input.status === 'rejected') {
7609
+ if (input.status === 'cancelled' || input.status === 'rejected') {
7625
7610
  return tx('Adjust the task description and run again.', '可以调整任务描述后重新运行。');
7626
7611
  }
7627
7612
  if (input.status === 'blocked' || input.status === 'failed') {