codex-overleaf-link 2.5.0 → 2.6.0

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 (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -57,6 +57,11 @@
57
57
  fallbackUserMessage: 'Codex could not resolve the requested selection or context.',
58
58
  fallbackNextAction: 'Select the target again or specify the file/section explicitly.'
59
59
  },
60
+ selection_scope_violation: {
61
+ stage: 'preflight', severity: 'blocked', defaultRetryable: false,
62
+ fallbackUserMessage: 'A generated change exceeded the attached selection and was blocked.',
63
+ fallbackNextAction: 'Select the intended region again and request only changes within it. Valid written parts can be undone.'
64
+ },
60
65
  base_file_missing_at_start: {
61
66
  stage: 'context', severity: 'error', defaultRetryable: true,
62
67
  fallbackUserMessage: 'A file needed for this task was missing during the initial project read.',
@@ -101,6 +106,26 @@
101
106
  },
102
107
 
103
108
  // 9.2 Preflight
109
+ sensitive_content_blocked: {
110
+ stage: 'preflight', severity: 'blocked', defaultRetryable: false,
111
+ fallbackUserMessage: 'Sensitive content was found before sending, so Codex did not receive this request.',
112
+ fallbackNextAction: 'Remove the sensitive content, narrow @context, or enable explicit sensitive confirmation in Project Settings.'
113
+ },
114
+ governance_blocked: {
115
+ stage: 'preflight', severity: 'blocked', defaultRetryable: false,
116
+ fallbackUserMessage: 'Project file rules blocked this write. Codex did not write the affected path.',
117
+ fallbackNextAction: 'Review the project file rules in Settings > Protection & privacy, then request changes within the allowed paths.'
118
+ },
119
+ governance_readonly: {
120
+ stage: 'preflight', severity: 'blocked', defaultRetryable: false,
121
+ fallbackUserMessage: 'Project file rules marked a path in this operation read-only. Codex did not apply this operation.',
122
+ fallbackNextAction: 'Review the read-only paths in Settings > Protection & privacy, then request changes within the allowed paths.'
123
+ },
124
+ governance_writable_allowlist: {
125
+ stage: 'preflight', severity: 'blocked', defaultRetryable: false,
126
+ fallbackUserMessage: 'A path in this operation is outside the configured writable paths. Codex did not apply this operation.',
127
+ fallbackNextAction: 'Review the writable paths in Settings > Protection & privacy, then request changes within those paths.'
128
+ },
104
129
  missing_base_file: {
105
130
  stage: 'preflight', severity: 'blocked', defaultRetryable: true,
106
131
  fallbackUserMessage: 'A write safety check needed a base file that is missing from the run snapshot.',
@@ -121,11 +146,26 @@
121
146
  fallbackUserMessage: 'The edit anchor matches multiple locations.',
122
147
  fallbackNextAction: 'Narrow the request or select a smaller region.'
123
148
  },
149
+ file_plan_confirmation_rejected: {
150
+ stage: 'preflight', severity: 'blocked', defaultRetryable: false,
151
+ fallbackUserMessage: 'The complete file plan was cancelled; no part was applied.',
152
+ fallbackNextAction: 'Review the intended changes and start a new run if you want to apply them.'
153
+ },
154
+ file_tree_controls_unavailable: {
155
+ stage: 'preflight', severity: 'blocked', defaultRetryable: false,
156
+ fallbackUserMessage: 'Codex could not safely perform this file operation with the available Overleaf controls.',
157
+ fallbackNextAction: 'Check the file operation in Overleaf before starting a new task.'
158
+ },
124
159
  delete_confirmation_missing: {
125
160
  stage: 'preflight', severity: 'blocked', defaultRetryable: true,
126
161
  fallbackUserMessage: 'A destructive delete was not confirmed.',
127
162
  fallbackNextAction: 'Confirm the delete explicitly if intended.'
128
163
  },
164
+ delete_confirmation_rejected: {
165
+ stage: 'preflight', severity: 'blocked', defaultRetryable: false,
166
+ fallbackUserMessage: 'The deletion was cancelled and was not applied. Other allowed changes in this run may still proceed.',
167
+ fallbackNextAction: 'Review the other changes. If deletion is still intended, request it in a new task.'
168
+ },
129
169
  binary_write_blocked: {
130
170
  stage: 'preflight', severity: 'blocked', defaultRetryable: false,
131
171
  fallbackUserMessage: 'Codex tried to write unsupported binary content.',
@@ -197,6 +237,11 @@
197
237
  },
198
238
 
199
239
  // 9.4 Reviewing
240
+ reviewing_unavailable: {
241
+ stage: 'reviewing', severity: 'blocked', defaultRetryable: false,
242
+ fallbackUserMessage: "Track Changes is unavailable for this project. It depends on the project owner's Overleaf plan.",
243
+ fallbackNextAction: 'Turn off Track in the composer to write without tracked changes. Codex still records each change so you can undo it.'
244
+ },
200
245
  reviewing_state_unknown: {
201
246
  stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
202
247
  fallbackUserMessage: 'Codex could not determine whether Reviewing/Track Changes is enabled.',
@@ -234,6 +279,21 @@
234
279
  },
235
280
 
236
281
  // 9.5 Undo
282
+ snapshot_undo_current_mismatch: {
283
+ stage: 'undo', severity: 'blocked', defaultRetryable: false,
284
+ fallbackUserMessage: 'The current content or document identity does not match this run\'s undo checkpoint. Codex stopped Undo and kept the recovery data.',
285
+ fallbackNextAction: 'Inspect the file and any later edits before trying Undo again.'
286
+ },
287
+ snapshot_undo_open_failed: {
288
+ stage: 'undo', severity: 'blocked', defaultRetryable: true,
289
+ fallbackUserMessage: 'Codex could not open the file to restore its saved checkpoint.',
290
+ fallbackNextAction: 'Open the file in Overleaf, wait for it to load, then retry Undo.'
291
+ },
292
+ snapshot_undo_unavailable: {
293
+ stage: 'undo', severity: 'blocked', defaultRetryable: false,
294
+ fallbackUserMessage: 'The saved checkpoint is unavailable for this Undo. Codex kept the remaining recovery data.',
295
+ fallbackNextAction: 'Inspect the file and the run details before continuing.'
296
+ },
237
297
  undo_preflight_content_drift: {
238
298
  stage: 'undo', severity: 'blocked', defaultRetryable: false,
239
299
  fallbackUserMessage: "Current content no longer matches this run's written content.",
@@ -303,6 +363,21 @@
303
363
  fallbackUserMessage: 'Native host version or capabilities do not match the extension.',
304
364
  fallbackNextAction: 'Update the native host.'
305
365
  },
366
+ codex_local_skill_unavailable: {
367
+ stage: 'preflight', severity: 'blocked', defaultRetryable: true,
368
+ fallbackUserMessage: 'The selected local Codex skill is unavailable or disabled. This run did not start.',
369
+ fallbackNextAction: 'Open Skills, select an available enabled skill, and submit again.'
370
+ },
371
+ codex_local_skills_failed: {
372
+ stage: 'preflight', severity: 'blocked', defaultRetryable: true,
373
+ fallbackUserMessage: 'Local Codex skills could not be loaded. This run did not start.',
374
+ fallbackNextAction: 'Open Skills to reload the local catalog, then submit again.'
375
+ },
376
+ codex_context_limit: {
377
+ stage: 'codex', severity: 'error', defaultRetryable: true,
378
+ fallbackUserMessage: 'This run stopped because the model context limit was exceeded.',
379
+ fallbackNextAction: 'Retry the task. If the context limit is reached again, use /compact or start a new conversation with less context.'
380
+ },
306
381
  codex_no_usable_result: {
307
382
  stage: 'codex', severity: 'error', defaultRetryable: true,
308
383
  fallbackUserMessage: 'Local Codex returned no usable final report or operations.',
@@ -445,6 +520,8 @@
445
520
  * @type {Record<string, string | ((result: any) => string)>}
446
521
  */
447
522
  const LEGACY_CODE_MAP = {
523
+ governance_blocked: result => result.reasonKey === 'readonly' ? 'governance_readonly'
524
+ : result.reasonKey === 'writable_allowlist' ? 'governance_writable_allowlist' : 'governance_blocked',
448
525
  path_not_found: 'target_file_not_found',
449
526
  file_open_failed: 'target_file_open_failed',
450
527
  editor_not_ready: 'target_editor_not_ready',
@@ -554,6 +631,7 @@
554
631
  nextAction: catalog.fallbackNextAction,
555
632
  evidence: {}
556
633
  };
634
+ if (canonicalCode === 'reviewing_unavailable') failure.changedDocument = false;
557
635
  if (legacyReason) {
558
636
  failure.technicalMessage = legacyReason;
559
637
  }
@@ -580,6 +658,262 @@
580
658
  return out;
581
659
  }
582
660
 
661
+ // Display projection only: these aliases and exact historical templates do
662
+ // not enter the canonical catalog, persistence, retry or recovery decisions.
663
+ const REVIEW_DISPLAY_CODES = new Set([
664
+ 'missing_tracked_changes', 'tracked_change_file_open_failed', 'tracked_change_reject_control_not_found',
665
+ 'tracked_change_reject_not_confirmed', 'tracked_change_accept_control_not_found', 'tracked_change_accept_not_confirmed',
666
+ 'tracked_change_editor_undo_open_failed', 'tracked_change_editor_undo_current_mismatch', 'editor_undo_control_not_found',
667
+ 'editor_undo_no_progress', 'editor_undo_max_iterations', 'tracked_change_undo_max_iterations',
668
+ 'tracked_change_undo_verify_open_failed', 'tracked_change_undo_verify_failed', 'accept_missing_run_content',
669
+ 'accept_editor_undo_unavailable', 'accept_editing_not_confirmed', 'accept_editing_not_stable',
670
+ 'accept_replay_rollback_missing_content', 'review_scope_unavailable', 'review_request_unverified',
671
+ 'review_owner_disconnected', 'review_deadline_exceeded', 'review_cancelled',
672
+ 'review_project_changed', 'tracked_change_not_found', 'snapshot_undo_current_mismatch',
673
+ 'snapshot_undo_open_failed', 'snapshot_undo_unavailable', 'file_tree_controls_unavailable',
674
+ 'native_untracked_undo_pending_changes'
675
+ ]);
676
+ const REVIEW_DISPLAY_TEMPLATES = [
677
+ ['governance_readonly', 'Project governance marked this path read-only, so Codex did not write it.'],
678
+ ['governance_readonly', '项目治理规则将此路径标记为只读,因此 Codex 没有写入。'],
679
+ ['governance_readonly', '项目文件规则将此操作涉及的路径标记为只读。Codex 未执行此操作。'],
680
+ ['governance_writable_allowlist', 'Project governance allows writes only to configured writable patterns, and this path is outside that allowlist.'],
681
+ ['governance_writable_allowlist', '项目治理规则只允许写入配置的可写路径,此路径不在允许范围内。'],
682
+ ['governance_writable_allowlist', '此操作涉及的路径不在配置的可写路径范围内。Codex 未执行此操作。'],
683
+ ['governance_blocked', 'Project governance blocked this write.'],
684
+ ['governance_blocked', '项目治理规则阻止了此写入。'],
685
+ ['governance_blocked', '项目文件规则阻止了此次写入。Codex 未写入受影响的路径。'],
686
+ ["missing_tracked_changes", "这轮写入没有可识别的 Overleaf 留痕记录;为避免制造新的红线,Codex 没有执行文本补丁撤销。"],
687
+ ["missing_tracked_changes", "No matching Overleaf tracked-change records were found for this run, so Codex did not undo with text patches."],
688
+ ["missing_tracked_changes", "No matching Overleaf tracked-change records were found for this run. Codex did not undo with text patches."],
689
+ ["missing_tracked_changes", "这轮写入没有可识别的 Overleaf 留痕记录;Codex 没有执行文本补丁撤销。"],
690
+ ["tracked_change_file_open_failed", "无法打开 {file} 来查找这轮写入的留痕记录;Codex 没有用文本补丁伪撤销。"],
691
+ ["tracked_change_file_open_failed", "无法打开 {file} 来继续拒绝这轮留痕记录;请在 Overleaf 审阅面板手动处理。"],
692
+ ["tracked_change_file_open_failed", "Could not open {file} to find this run's tracked changes. Handle them manually in the Overleaf review tools."],
693
+ ["tracked_change_file_open_failed", "Could not open the file to find this run's tracked changes. Handle them manually in the Overleaf review tools."],
694
+ ["tracked_change_file_open_failed", "Could not open the file to find this run's tracked changes. Check it in the Overleaf review panel."],
695
+ ["tracked_change_file_open_failed", "无法打开文件来查找这轮写入的留痕记录;请在 Overleaf 审阅面板检查。"],
696
+ ["tracked_change_reject_control_not_found", "找到了这轮写入的留痕记录,但没有找到对应的 Reject/拒绝按钮;Codex 没有用文本补丁伪撤销。"],
697
+ ["tracked_change_reject_control_not_found", "还有 {file} 的留痕记录未处理,但没有找到对应的 Reject/拒绝按钮;请在 Overleaf 审阅面板手动拒绝。"],
698
+ ["tracked_change_reject_control_not_found", "Some tracked changes in {file} remain, but the matching Reject button was not found. Reject them manually in Overleaf."],
699
+ ["tracked_change_reject_control_not_found", "Some tracked changes remain, but the matching Reject button was not found. Reject them manually in Overleaf."],
700
+ ["tracked_change_reject_control_not_found", "Some tracked changes remain, but the matching Reject button was not found. Check them in the Overleaf review panel."],
701
+ ["tracked_change_reject_control_not_found", "仍有留痕记录未处理,但未找到对应的拒绝按钮;请在 Overleaf 审阅面板检查。"],
702
+ ["tracked_change_reject_not_confirmed", "Codex 点击了 Reject/拒绝,但 Overleaf 页面仍显示这条留痕记录;请在 Overleaf 审阅面板手动拒绝。"],
703
+ ["tracked_change_reject_not_confirmed", "Codex clicked Reject, but Overleaf still shows this tracked change. Reject it manually in the Overleaf review panel."],
704
+ ["tracked_change_reject_not_confirmed", "Codex clicked Reject, but Overleaf still shows this tracked change. Check it in the Overleaf review panel."],
705
+ ["tracked_change_reject_not_confirmed", "Codex 已点击拒绝,但 Overleaf 仍显示该留痕记录;请在审阅面板检查。"],
706
+ ["tracked_change_accept_control_not_found", "Some tracked changes in {file} remain, but the matching Accept button was not found. Accept them manually in Overleaf."],
707
+ ["tracked_change_accept_control_not_found", "Some tracked changes remain, but the matching Accept button was not found. Accept them manually in Overleaf."],
708
+ ["tracked_change_accept_control_not_found", "Some tracked changes remain, but the matching Accept button was not found. Check them in the Overleaf review panel."],
709
+ ["tracked_change_accept_control_not_found", "仍有留痕记录未处理,但未找到对应的接受按钮;请在 Overleaf 审阅面板检查。"],
710
+ ["tracked_change_accept_not_confirmed", "Codex clicked Accept, but Overleaf still shows this tracked change. Accept it manually in the Overleaf review panel."],
711
+ ["tracked_change_accept_not_confirmed", "Codex clicked Accept, but Overleaf still shows this tracked change. Check it in the Overleaf review panel."],
712
+ ["tracked_change_accept_not_confirmed", "Codex 已点击接受,但 Overleaf 仍显示该留痕记录;请在审阅面板检查。"],
713
+ ["tracked_change_editor_undo_open_failed", "无法打开 {file} 来执行 Overleaf 原生撤销。"],
714
+ ["tracked_change_editor_undo_open_failed", "Could not open {file} for Overleaf native undo."],
715
+ ["tracked_change_editor_undo_open_failed", "Could not open the file for Overleaf native undo."],
716
+ ["tracked_change_editor_undo_open_failed", "Could not open the file for Overleaf native Undo."],
717
+ ["tracked_change_editor_undo_open_failed", "无法打开文件来执行 Overleaf 原生撤销。"],
718
+ ["tracked_change_editor_undo_current_mismatch", "{file} 当前内容已经不是本轮写入后的内容;为避免撤掉你的后续修改,Codex 不使用 Overleaf 原生撤销。"],
719
+ ["tracked_change_editor_undo_current_mismatch", "{file} no longer matches this run's written content, so Codex did not use Overleaf native undo."],
720
+ ["tracked_change_editor_undo_current_mismatch", "The current content no longer matches this run's written content, so Codex did not use Overleaf native undo."],
721
+ ["tracked_change_editor_undo_current_mismatch", "The current content no longer matches this run's written content. Codex stopped native Undo to preserve later edits."],
722
+ ["tracked_change_editor_undo_current_mismatch", "当前内容与本轮写入后的内容不一致;Codex 已停止原生撤销,以保留后续编辑。"],
723
+ ["editor_undo_control_not_found", "没有找到 Overleaf 编辑器自己的 Undo/撤销按钮,无法一次性撤销 {file} 的本轮留痕。"],
724
+ ["editor_undo_control_not_found", "Could not find Overleaf's editor Undo button to undo this run in {file}."],
725
+ ["editor_undo_control_not_found", "Could not find Overleaf's editor Undo button to undo this run."],
726
+ ["editor_undo_control_not_found", "未找到 Overleaf 编辑器的撤销按钮,无法撤销本轮改动。"],
727
+ ["editor_undo_no_progress", "Codex 点击了 Overleaf Undo/撤销,但 {file} 内容没有变化。"],
728
+ ["editor_undo_no_progress", "Codex clicked Overleaf Undo, but {file} did not change."],
729
+ ["editor_undo_no_progress", "Codex clicked Overleaf Undo, but the file did not change."],
730
+ ["editor_undo_no_progress", "Codex 已点击 Overleaf 撤销,但文件内容没有变化。"],
731
+ ["editor_undo_max_iterations", "{file} 经过多次 Overleaf 原生撤销后仍未回到本轮写入前内容,已停止以避免撤销其它修改。"],
732
+ ["editor_undo_max_iterations", "{file} did not return to its pre-run content after repeated Overleaf native undo steps. Codex stopped to avoid undoing other edits."],
733
+ ["editor_undo_max_iterations", "The file did not return to its pre-run content after repeated Overleaf native undo steps. Codex stopped to avoid undoing other edits."],
734
+ ["editor_undo_max_iterations", "The file did not return to its pre-run content after repeated native Undo steps. Codex stopped to avoid undoing other edits."],
735
+ ["editor_undo_max_iterations", "经过多次原生撤销后,文件仍未恢复到本轮写入前的内容;Codex 已停止,以免撤销其他编辑。"],
736
+ ["tracked_change_undo_max_iterations", "{file} 仍有未完成的留痕记录;Codex 已停止以避免误拒绝其它改动。"],
737
+ ["tracked_change_undo_max_iterations", "{file} still has tracked changes after the reject sweep, so Codex stopped to avoid mis-rejecting other edits."],
738
+ ["tracked_change_undo_max_iterations", "{file} still has tracked changes after the per-expected-file reject sweep, so Codex stopped to avoid mis-rejecting other edits."],
739
+ ["tracked_change_undo_max_iterations", "{file} still has tracked changes left. Codex stopped to avoid rejecting unrelated edits."],
740
+ ["tracked_change_undo_max_iterations", "Tracked changes are still left. Codex stopped to avoid rejecting unrelated edits."],
741
+ ["tracked_change_undo_max_iterations", "仍有未处理的留痕记录;Codex 已停止,以免误拒绝其他编辑。"],
742
+ ["tracked_change_undo_verify_open_failed", "撤销后无法打开 {file} 验证内容;请刷新 Overleaf 后检查。"],
743
+ ["tracked_change_undo_verify_open_failed", "Could not open {file} after undo to verify content. Reload Overleaf and check manually."],
744
+ ["tracked_change_undo_verify_open_failed", "Could not open the file after undo to verify content. Reload Overleaf and check manually."],
745
+ ["tracked_change_undo_verify_open_failed", "Could not open the file after Undo to verify its content. Reload Overleaf and check it."],
746
+ ["tracked_change_undo_verify_open_failed", "撤销后无法打开文件验证内容;请刷新 Overleaf 后检查。"],
747
+ ["tracked_change_undo_verify_failed", "{file} 拒绝留痕后内容没有回到写入前状态;请在 Overleaf 审阅面板检查这轮修改。"],
748
+ ["tracked_change_undo_verify_failed", "{file} did not return to its pre-run content after rejecting tracked changes. Check this run's changes in the Overleaf review panel."],
749
+ ["tracked_change_undo_verify_failed", "The file did not return to its pre-run content after rejecting tracked changes. Check this run's changes in the Overleaf review panel."],
750
+ ["tracked_change_undo_verify_failed", "拒绝留痕后,文件内容仍未恢复到写入前状态;请在 Overleaf 审阅面板检查本轮修改。"],
751
+ ["accept_missing_run_content", "这轮写入没有可识别的写入前/写入后内容;Codex 没有把留痕改动接受为永久文本。"],
752
+ ["accept_missing_run_content", "{file} 缺少写入前/写入后内容;Codex 没有重放本轮改动。"],
753
+ ["accept_missing_run_content", "The pre-write or post-write content is unavailable. Codex did not replay or accept the tracked changes."],
754
+ ["accept_missing_run_content", "缺少写入前或写入后的内容;Codex 未重放或接受留痕改动。"],
755
+ ["accept_editor_undo_unavailable", "没有可执行的 Overleaf 原生撤销或安全快照回滚来清掉本轮留痕;Codex 没有接受这轮改动。"],
756
+ ["accept_editor_undo_unavailable", "Native Undo and a safe checkpoint rollback are unavailable. Codex did not accept this run."],
757
+ ["accept_editor_undo_unavailable", "原生撤销及安全的检查点回滚均不可用;Codex 未接受本轮改动。"],
758
+ ["accept_editing_not_confirmed", "无法确认 Overleaf 已切换到 Editing 模式;Codex 没有把本轮改动重写为永久文本。"],
759
+ ["accept_editing_not_confirmed", "切换后仍未能确认 Overleaf 处于 Editing 模式;为避免重写又留下留痕,Codex 没有重放本轮改动。"],
760
+ ["accept_editing_not_confirmed", "本次操作前未能确认 Overleaf 处于 Editing 模式;为避免重写又留下留痕,Codex 没有继续重放本轮剩余改动。"],
761
+ ["accept_editing_not_confirmed", "Codex could not re-confirm Editing mode before replaying {file}; Codex stopped to avoid landing a tracked write."],
762
+ ["accept_editing_not_confirmed", "Codex could not confirm Editing mode for the untracked Accept replay, so the run was not finalized."],
763
+ ["accept_editing_not_confirmed", "Codex could not switch Overleaf to Editing mode for the untracked Accept replay, so the run was not finalized."],
764
+ ["accept_editing_not_confirmed", "Codex toggled Overleaf to Editing, but the post-toggle state did not positively confirm Editing — Codex did not replay the run."],
765
+ ["accept_editing_not_confirmed", "Codex could not confirm Overleaf Editing mode. It stopped replaying this run to avoid creating new tracked changes."],
766
+ ["accept_editing_not_confirmed", "无法确认 Overleaf 已进入编辑模式;Codex 已停止重放本轮改动,以免产生新的留痕。"],
767
+ ["accept_editing_not_stable", "Overleaf Editing mode did not remain stable long enough for an untracked Accept All replay; Codex did not replay this write."],
768
+ ["accept_editing_not_stable", "Overleaf Editing mode did not remain stable long enough for an untracked Accept replay. Codex did not replay this write."],
769
+ ["accept_editing_not_stable", "Overleaf 编辑模式未保持稳定,无法无留痕地重放接受的改动;Codex 未重放此次写入。"],
770
+ ["accept_replay_rollback_missing_content", "Accept All detected fresh tracked changes, but could not roll back because the pre/post content was unavailable."],
771
+ ["accept_replay_rollback_missing_content", "Accept created fresh tracked changes, but rollback could not run because the pre-write or post-write content was unavailable."],
772
+ ["accept_replay_rollback_missing_content", "接受改动时产生了新的留痕,但缺少写入前或写入后的内容,无法回滚。"],
773
+ ["review_scope_unavailable", "The native tracked-change scope could not be safely isolated. No review mutation was attempted. Use Overleaf Review to handle the changes individually."],
774
+ ["review_scope_unavailable", "The native tracked-change scope could not be safely isolated. No review mutation was attempted. Check the changes individually in Overleaf Review."],
775
+ ["review_scope_unavailable", "无法安全确认本轮原生留痕的范围;尚未尝试审阅写入。请在 Overleaf 审阅面板逐项检查。"],
776
+ ["review_request_unverified", "The native review request could not be fully verified; some target changes may already be accepted. No text replay was attempted. Check Overleaf Review before retrying."],
777
+ ["review_request_unverified", "原生审阅请求未能完全核实,部分目标改动可能已接受;未尝试文本重放。请先在 Overleaf 审阅面板检查,再决定是否重试。"],
778
+ ["review_owner_disconnected", "The Undo owner disconnected; no further review writes were started."],
779
+ ["review_owner_disconnected", "撤销操作的连接已断开;未再启动后续审阅写入。"],
780
+ ["review_deadline_exceeded", "The review time budget expired; no further review writes were started."],
781
+ ["review_deadline_exceeded", "The review time budget expired."],
782
+ ["review_deadline_exceeded", "审阅操作已超时;未再启动后续审阅写入。"],
783
+ ["review_cancelled", "The review was cancelled; no further review writes were started."],
784
+ ["review_cancelled", "审阅操作已取消;未再启动后续审阅写入。"],
785
+ ["review_project_changed", "The original review project is no longer active."],
786
+ ["review_project_changed", "原审阅项目已不再处于活动状态。"],
787
+ ["tracked_change_not_found", "没有在 Overleaf 页面里找到这轮写入对应的留痕记录;Codex 没有用文本补丁伪撤销。"],
788
+ ["tracked_change_not_found", "No matching tracked changes were found on the Overleaf page for this run."],
789
+ ["tracked_change_not_found", "No matching tracked changes were found on the Overleaf page for this run. Codex did not undo with text patches."],
790
+ ["snapshot_undo_current_mismatch", "{file} 当前内容或文档身份与本轮撤销检查点不一致;Codex 保留恢复材料并停止撤销。"],
791
+ ["snapshot_undo_current_mismatch", "{file} 当前内容已经不是本轮写入后的内容;为避免覆盖你的后续修改,Codex 不执行快照撤销。"],
792
+ ["snapshot_undo_current_mismatch", "The current content or document identity does not match this run's undo checkpoint. Codex stopped Undo and kept the recovery data."],
793
+ ["snapshot_undo_current_mismatch", "当前内容或文档身份与本轮撤销检查点不一致;Codex 已停止撤销并保留恢复材料。"],
794
+ ["snapshot_undo_open_failed", "无法打开 {file} 来执行快照撤销。"],
795
+ ["snapshot_undo_open_failed", "Codex could not open the file to restore its saved checkpoint."],
796
+ ["snapshot_undo_open_failed", "Codex 无法打开文件来恢复已保存的检查点。"],
797
+ ["snapshot_undo_unavailable", "The saved checkpoint is unavailable for this undo."],
798
+ ["snapshot_undo_unavailable", "The saved checkpoint is unavailable for this Undo. Codex kept the remaining recovery data."],
799
+ ["snapshot_undo_unavailable", "本次撤销所需的检查点不可用;Codex 已保留剩余恢复材料。"]
800
+ ].sort((a, b) => Number(a[1].includes('{file}')) - Number(b[1].includes('{file}')))
801
+ .map(([code, template]) => ({ code, template,
802
+ pattern: new RegExp('^' + template.split('{file}').map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('(.+)') + '$') }));
803
+
804
+ function safeDisplayProjectPath(value) {
805
+ if (typeof value !== 'string' || !value || value.length > 300 || /[\r\n]/.test(value)) return '';
806
+ const path = value.replace(/\\/g, '/');
807
+ return !path.startsWith('/') && !/^[A-Za-z]:/.test(path) && !path.split('/').includes('..') ? path : '';
808
+ }
809
+
810
+ function isContextLimitError(message, code) {
811
+ const text = String(message || '');
812
+ return ['codex_context_limit', 'context_length_exceeded', 'context_window_exceeded'].includes(code)
813
+ || /\b(?:codex_context_limit|context_length_exceeded|context_window_exceeded)\b/i.test(text)
814
+ || /Codex ran out of room in the model['’]s context window/i.test(text)
815
+ || /maximum context length is \d+ tokens[\s\S]*?(?:you requested|resulted in) \d+ tokens/i.test(text);
816
+ }
817
+
818
+ function recoverDisplayFailure(failure, allowFilePrefix = true) {
819
+ if (!failure || typeof failure !== 'object') return failure;
820
+ if (failure.code === 'codex_no_usable_result' && failure.stage === 'codex'
821
+ && isContextLimitError(failure.technicalMessage)) return { ...failure, code: 'codex_context_limit',
822
+ userMessage: FAILURE_CODE_CATALOG.codex_context_limit.fallbackUserMessage,
823
+ nextAction: FAILURE_CODE_CATALOG.codex_context_limit.fallbackNextAction };
824
+ const originalCode = failure.evidence?.originalCode;
825
+ const displayCodeAllowed = code => REVIEW_DISPLAY_CODES.has(code)
826
+ && (code !== 'native_untracked_undo_pending_changes'
827
+ || (failure.changedDocument !== true && failure.mayHaveMutated !== true && failure.evidence?.writeStarted !== true));
828
+ const explicitCode = displayCodeAllowed(originalCode) ? originalCode
829
+ : displayCodeAllowed(failure.code) ? failure.code : '';
830
+ const message = String(failure.userMessage || '');
831
+ for (const { code, pattern } of REVIEW_DISPLAY_TEMPLATES) {
832
+ if (explicitCode && code !== explicitCode) continue;
833
+ const match = pattern.exec(message);
834
+ if (!match) continue;
835
+ const captured = ['当前文件', 'The target file', 'the next file'].includes(match[1]) ? '' : match[1];
836
+ if (captured && safeDisplayProjectPath(captured) !== captured) continue;
837
+ return { ...failure, code, file: failure.file || captured || '' };
838
+ }
839
+ // Catalog defaults are also product-owned exact messages in older compact
840
+ // records. Recover only a display code; their old retry semantics stay put.
841
+ const catalogCode = Object.keys(FAILURE_CODE_CATALOG).find(code => code !== 'unknown_legacy_failure'
842
+ && FAILURE_CODE_CATALOG[code].fallbackUserMessage === message);
843
+ if (!explicitCode && catalogCode) return { ...failure, code: catalogCode };
844
+ // Earlier formatters persisted "file: message". Only accept one safe file
845
+ // prefix whose complete remaining body resolves to a known product message.
846
+ const prefixed = allowFilePrefix && /^([^:\r\n]+): (.+)$/.exec(message);
847
+ if (prefixed && safeDisplayProjectPath(prefixed[1]) === prefixed[1]) {
848
+ const body = { ...failure, userMessage: prefixed[2], file: failure.file || prefixed[1] };
849
+ const recovered = recoverDisplayFailure(body, false);
850
+ if (recovered !== body && recovered.code !== 'unknown_legacy_failure') return recovered;
851
+ }
852
+ return explicitCode ? { ...failure, code: explicitCode } : failure;
853
+ }
854
+
855
+ // A single renderer prevents the Notice and expanded results from using
856
+ // different code lists or emitting a dynamic filename twice.
857
+ function formatFailureReasonForDisplay(failure, locale, i18nLookup) {
858
+ const display = recoverDisplayFailure(failure);
859
+ const lookup = typeof i18nLookup === 'function' ? i18nLookup : () => undefined;
860
+ const messageCode = getFailureMessageCode(display);
861
+ const key = messageCode ? 'failureReason_' + messageCode + '_user' : '';
862
+ const template = key ? lookup(key) : '';
863
+ const file = safeDisplayProjectPath(display?.file);
864
+ const localized = localizeFailureReason(display ? { ...display, file } : display, locale, lookup);
865
+ const message = template?.includes('{file}') ? localized.userMessage
866
+ : [file, localized.userMessage].filter(Boolean).join(': ');
867
+ return { ...localized, code: display?.code, message, file, localized: Boolean(template && template !== key) };
868
+ }
869
+
870
+ function getFailureMessageCode(failure) {
871
+ // Reload recovery retains its historical retry code. Only its presentation
872
+ // identifies the page interruption instead of a failed bridge request.
873
+ if (failure?.code === 'native_request_failed' && failure.source === 'panel_reload') {
874
+ return 'panel_reload_interrupted';
875
+ }
876
+ return typeof failure?.code === 'string' ? failure.code : '';
877
+ }
878
+
879
+ function localizeFailureMessage(message, locale, i18nLookup) {
880
+ return formatFailureReasonForDisplay({ code: 'unknown_legacy_failure', userMessage: message }, locale, i18nLookup).message;
881
+ }
882
+
883
+ // Writeback consumers share catalog normalization, including file context,
884
+ // while retaining the mutation evidence carried by older result envelopes.
885
+ function normalizeWritebackFailure(result, operation) {
886
+ if (!result || typeof result !== 'object' || (!result.failure && !result.code && result.ok !== false)) return null;
887
+ const failure = normalizeFailureReason(result, operation);
888
+ const explicit = result.failure && typeof result.failure === 'object' ? result.failure : {};
889
+ const changedDocument = typeof explicit.changedDocument === 'boolean' ? explicit.changedDocument : result.changedDocument;
890
+ if (typeof changedDocument === 'boolean') failure.changedDocument = changedDocument;
891
+ if (explicit.mayHaveMutated === true || result.mayHaveMutated === true) failure.mayHaveMutated = true;
892
+ const terminalState = explicit.terminalState || result.terminalState;
893
+ if (ALLOWED_SEVERITY_PER_TERMINAL[terminalState]?.has(failure.severity)) failure.terminalState = terminalState;
894
+ // An unrecognized transport code can still carry a valid write-stage hint.
895
+ if (failure.code === 'unknown_legacy_failure') {
896
+ const stage = explicit.stage || result.stage;
897
+ if (FAILURE_STAGES.has(stage)) failure.stage = stage;
898
+ }
899
+ return failure;
900
+ }
901
+
902
+ function isUnconfirmedWriteFailure(failure) {
903
+ if (!failure || typeof failure !== 'object') return false;
904
+ return failure.terminalState === 'needs_review' || failure.mayHaveMutated === true
905
+ || ['write_timeout', 'partial_write_needs_review', 'writeback_result_unconfirmed', 'write_result_missing'].includes(failure.code)
906
+ || (failure.changedDocument === true && ['write', 'verify', 'undo', 'accept'].includes(failure.stage));
907
+ }
908
+
909
+ // A potentially changed document needs review before unrelated blockers can
910
+ // offer a retry. Keep the generic severity/stage order within each group.
911
+ function selectPrimaryWritebackFailure(failures) {
912
+ if (!Array.isArray(failures)) return null;
913
+ const review = failures.filter(isUnconfirmedWriteFailure);
914
+ return selectPrimaryFailure(review.length ? review : failures);
915
+ }
916
+
583
917
  function buildFallbackFailureReason(code, op) {
584
918
  const catalog = FAILURE_CODE_CATALOG[code] || FAILURE_CODE_CATALOG.unknown_legacy_failure;
585
919
  return augmentWithOperation({
@@ -648,10 +982,15 @@
648
982
  */
649
983
  function localizeFailureReason(failure, locale, i18nLookup) {
650
984
  void locale;
651
- const lookup = i18nLookup instanceof Function ? i18nLookup : function noopLookup() { return undefined; };
652
- const code = failure && typeof failure.code === 'string' ? failure.code : '';
985
+ failure = recoverDisplayFailure(failure);
986
+ const lookup = key => {
987
+ const value = typeof i18nLookup === 'function' ? i18nLookup(key) : undefined;
988
+ return value && value !== key ? value : undefined;
989
+ };
990
+ const code = getFailureMessageCode(failure);
653
991
  const userTemplate = code ? lookup('failureReason_' + code + '_user') : undefined;
654
- const nextTemplate = code ? lookup('failureReason_' + code + '_next') : undefined;
992
+ const nextTemplate = (code ? lookup('failureReason_' + code + '_next') : undefined)
993
+ || (REVIEW_DISPLAY_CODES.has(code) ? lookup('failureReason_review_detail_next') : undefined);
655
994
  const userMessage = userTemplate
656
995
  ? interpolateFailureTemplate(userTemplate, failure)
657
996
  : (failure && failure.userMessage) || '';
@@ -682,7 +1021,13 @@
682
1021
  STAGE_TIE_BREAKER_ORDER,
683
1022
  validateFailureReason,
684
1023
  normalizeFailureReason,
1024
+ normalizeWritebackFailure,
1025
+ isUnconfirmedWriteFailure,
1026
+ selectPrimaryWritebackFailure,
685
1027
  selectPrimaryFailure,
686
- localizeFailureReason
1028
+ localizeFailureReason,
1029
+ formatFailureReasonForDisplay,
1030
+ localizeFailureMessage,
1031
+ isContextLimitError
687
1032
  };
688
1033
  });
@@ -11,17 +11,29 @@
11
11
  const PATCH_MESSAGE = 'codex-overleaf/global-preferences/patch';
12
12
  const DEFAULTS = Object.freeze({ theme: 'dark', locale: 'en', preloadProjectContext: true,
13
13
  loadCodexLocalSkills: true, loadCodexOverleafSkills: true });
14
- const FIELDS = Object.freeze([...Object.keys(DEFAULTS), 'codexOverleafSkillEnabled']);
14
+ const SKILL_MAP_FIELDS = Object.freeze(['codexOverleafSkillEnabled', 'codexLocalSkillEnabled']);
15
+ const FIELDS = Object.freeze([...Object.keys(DEFAULTS), ...SKILL_MAP_FIELDS]);
16
+ const MAX_SKILL_PREFERENCES = 200;
15
17
 
16
18
  function validSkillId(id) {
17
19
  return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(id)
18
20
  && !['__proto__', 'prototype', 'constructor'].includes(id) && !id.includes('..');
19
21
  }
20
22
 
23
+ function normalizeSkillEnabledMap(value) {
24
+ const result = {};
25
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return result;
26
+ for (const [id, enabled] of Object.entries(value)) {
27
+ if (validSkillId(id) && typeof enabled === 'boolean') result[id] = enabled;
28
+ }
29
+ if (Object.keys(result).length > MAX_SKILL_PREFERENCES) throw new Error('Too many skill preferences.');
30
+ return result;
31
+ }
32
+
21
33
  function pick(value = {}) {
22
- const skills = {};
23
- for (const [id, enabled] of Object.entries(value.codexOverleafSkillEnabled || {}).slice(0, 200)) {
24
- if (validSkillId(id) && typeof enabled === 'boolean') skills[id] = enabled;
34
+ const overleafSkills = {};
35
+ for (const [id, enabled] of Object.entries(value.codexOverleafSkillEnabled || {}).slice(0, MAX_SKILL_PREFERENCES)) {
36
+ if (validSkillId(id) && typeof enabled === 'boolean') overleafSkills[id] = enabled;
25
37
  }
26
38
  return {
27
39
  theme: ['dark', 'light', 'auto'].includes(value.theme) ? value.theme : 'dark',
@@ -29,7 +41,8 @@
29
41
  preloadProjectContext: value.preloadProjectContext !== false,
30
42
  loadCodexLocalSkills: value.loadCodexLocalSkills !== false,
31
43
  loadCodexOverleafSkills: value.loadCodexOverleafSkills !== false,
32
- codexOverleafSkillEnabled: skills
44
+ codexOverleafSkillEnabled: overleafSkills,
45
+ codexLocalSkillEnabled: normalizeSkillEnabledMap(value.codexLocalSkillEnabled)
33
46
  };
34
47
  }
35
48
 
@@ -43,8 +56,8 @@
43
56
  || typeof DEFAULTS[key] === 'boolean' && typeof item !== 'boolean') {
44
57
  throw new Error('Invalid global preference: ' + key);
45
58
  }
46
- if (key === 'codexOverleafSkillEnabled') {
47
- if (!item || typeof item !== 'object' || Array.isArray(item) || Object.keys(item).length > 200) {
59
+ if (SKILL_MAP_FIELDS.includes(key)) {
60
+ if (!item || typeof item !== 'object' || Array.isArray(item) || Object.keys(item).length > MAX_SKILL_PREFERENCES) {
48
61
  throw new Error('Invalid skill preference map.');
49
62
  }
50
63
  for (const [id, enabled] of Object.entries(item)) {
@@ -57,9 +70,12 @@
57
70
  }
58
71
 
59
72
  function applyPatch(values, patch) {
60
- return { ...pick(values), ...patch, codexOverleafSkillEnabled: {
61
- ...pick(values).codexOverleafSkillEnabled, ...(patch.codexOverleafSkillEnabled || {})
62
- } };
73
+ const previous = pick(values);
74
+ const result = { ...previous, ...patch };
75
+ for (const field of SKILL_MAP_FIELDS) {
76
+ result[field] = normalizeSkillEnabledMap({ ...previous[field], ...(patch[field] || {}) });
77
+ }
78
+ return result;
63
79
  }
64
80
 
65
81
  function changedKeys(left, right) {
@@ -71,7 +87,9 @@
71
87
  throw new Error('Invalid global preference revision.');
72
88
  }
73
89
  validatePatch(value.values);
74
- if (!FIELDS.every(key => Object.hasOwn(value.values, key))) throw new Error('Incomplete global preferences.');
90
+ // The local-skill map was added within v1; existing complete v1 snapshots
91
+ // inherit their source defaults until an explicit preference is saved.
92
+ if (!FIELDS.filter(key => key !== 'codexLocalSkillEnabled').every(key => Object.hasOwn(value.values, key))) throw new Error('Incomplete global preferences.');
75
93
  return { schemaVersion: 1, revision: value.revision, values: pick(value.values) };
76
94
  }
77
95
 
@@ -95,7 +113,6 @@
95
113
  const patch = validatePatch(input);
96
114
  const previous = await read();
97
115
  const values = applyPatch(previous.values, patch);
98
- if (Object.keys(values.codexOverleafSkillEnabled).length > 200) throw new Error('Too many skill preferences.');
99
116
  if (!changedKeys(previous.values, values).length) return previous;
100
117
  const snapshot = { schemaVersion: 1, revision: previous.revision + 1, values };
101
118
  await storage.set({ [STORAGE_KEY]: snapshot });
@@ -127,5 +144,5 @@
127
144
  }
128
145
 
129
146
  return Object.freeze({ STORAGE_KEY, LEGACY_KEY, READ_MESSAGE, PATCH_MESSAGE, FIELDS,
130
- pick, validatePatch, validateSnapshot, applyPatch, changedKeys, createStore, installBackground });
147
+ normalizeSkillEnabledMap, pick, validatePatch, validateSnapshot, applyPatch, changedKeys, createStore, installBackground });
131
148
  });
@@ -203,43 +203,10 @@
203
203
  }
204
204
 
205
205
  function normalizeSafeProjectPathFallback(value) {
206
- if (typeof value !== 'string' || /[\u0000-\u001f\u007f]/.test(value)) {
207
- return '';
208
- }
209
- const trimmed = value.trim();
210
- if (!trimmed || trimmed.startsWith('/') || trimmed.includes('\\') || /^[A-Za-z]:/.test(trimmed)) {
211
- return '';
212
- }
213
- let decoded = trimmed;
214
- for (let index = 0; index < 3; index += 1) {
215
- if (/%(?:2f|5c)/i.test(decoded)) {
216
- return '';
217
- }
218
- let next;
219
- try {
220
- next = decodeURIComponent(decoded);
221
- } catch (_error) {
222
- return '';
223
- }
224
- if (next === decoded) {
225
- break;
226
- }
227
- decoded = next;
228
- }
229
- if (/[\u0000-\u001f\u007f]/.test(decoded)
230
- || decoded.startsWith('/')
231
- || decoded.includes('\\')
232
- || /^[A-Za-z]:/.test(decoded)) {
233
- return '';
234
- }
235
- const normalized = decoded.replace(/\/+/g, '/').trim();
236
- if (!normalized) {
237
- return '';
238
- }
239
- const segments = normalized.split('/');
240
- return segments.some(segment => !segment || segment === '.' || segment === '..')
241
- ? ''
242
- : normalized;
206
+ if (typeof value !== 'string' || !value || value.length > 2048 || value.trim() !== value
207
+ || /[\u0000-\u001f\u007f]/.test(value) || value.startsWith('/')
208
+ || value.includes('\\') || /^[A-Za-z]:/.test(value)) return '';
209
+ return value.split('/').some(segment => !segment || segment === '.' || segment === '..') ? '' : value;
243
210
  }
244
211
 
245
212
  return {