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
@@ -12,27 +12,46 @@
12
12
  const tx = options.tx;
13
13
  const tr = options.tr;
14
14
 
15
+ let installIdentity = options.getExtensionCompatibilityMetadata?.()?.installIdentity || {};
16
+ function resolveInstallCommand(compatibility = {}) {
17
+ const metadata = options.getExtensionCompatibilityMetadata?.() || {};
18
+ installIdentity = { ...installIdentity, ...(metadata.installIdentity || {}), ...(compatibility.installIdentity || {}) };
19
+ if (installIdentity.managed) return Compatibility?.buildInstallCommand?.(
20
+ compatibility.recommendedVersion || Compatibility?.BUILD_TARGET_VERSION,
21
+ compatibility.native?.platform || compatibility.platform || installIdentity.platform,
22
+ metadata.extensionId, installIdentity
23
+ ) || '';
24
+ return compatibility.updateCommand || compatibility.installCommand || options.installCommand || '';
25
+ }
26
+
15
27
  function sendNative(payload) {
16
28
  return dispatch(payload, 'sendNative');
17
29
  }
18
30
 
19
- function sendBackgroundNative(payload) {
20
- return dispatch(payload, 'sendBackgroundNative');
31
+ function sendBackgroundNative(payload, context = {}) {
32
+ return dispatch(payload, 'sendBackgroundNative', context);
21
33
  }
22
34
 
23
- async function dispatch(payload, channelMethod) {
24
- const compatibilityGate = await ensureForMethod(payload?.method);
25
- if (!compatibilityGate.ok) return compatibilityGate.response;
26
- // Post-Undo cache bookkeeping must survive the ended run's cancellation.
27
- if (gatedMethods.has(payload?.method) && payload.method !== 'mirror.invalidate') {
28
- options.throwIfCancellationRequested?.();
35
+ async function dispatch(payload, channelMethod, context = {}) {
36
+ let compatibilityGate;
37
+ try {
38
+ compatibilityGate = await ensureForMethod(payload?.method, context);
39
+ if (!compatibilityGate.ok) return compatibilityGate.response;
40
+ // Post-Undo cache bookkeeping must survive the ended run's cancellation.
41
+ if (gatedMethods.has(payload?.method) && payload.method !== 'mirror.invalidate') {
42
+ options.throwIfCancellationRequested?.();
43
+ }
44
+ context.assertCurrent?.();
45
+ } catch (error) {
46
+ if (context.assertCurrent && error && typeof error === 'object') error.requestDispatched = false;
47
+ throw error;
29
48
  }
30
49
  return nativeChannel[channelMethod](
31
50
  attachEvidence(payload, compatibilityGate.compatibility)
32
51
  );
33
52
  }
34
53
 
35
- async function ensureForMethod(method) {
54
+ async function ensureForMethod(method, { assertCurrent } = {}) {
36
55
  const metadata = options.getExtensionCompatibilityMetadata?.() || {};
37
56
  const params = Compatibility?.buildBridgePingParams
38
57
  ? Compatibility.buildBridgePingParams(metadata)
@@ -41,9 +60,12 @@
41
60
  method: 'bridge.ping',
42
61
  params
43
62
  });
63
+ // A retired run must not publish even a compatibility failure into a successor.
64
+ assertCurrent?.();
44
65
  const compatibility = Compatibility?.evaluateNativeCompatibility
45
66
  ? Compatibility.evaluateNativeCompatibility(response, metadata)
46
67
  : options.fallbackNativeCompatibility(response);
68
+ const installCommand = resolveInstallCommand(compatibility);
47
69
  const allowed = !gatedMethods.has(method) ||
48
70
  (Compatibility?.isNativeMethodAllowed
49
71
  ? Compatibility.isNativeMethodAllowed(method, compatibility)
@@ -51,14 +73,14 @@
51
73
  if (allowed) return { ok: true, compatibility };
52
74
 
53
75
  const message = formatBlockedMessage(method, compatibility);
54
- const installCommand = options.installCommand;
55
76
  const error = {
56
77
  code: 'native_update_required',
57
78
  message,
58
79
  status: compatibility.status || 'unknown_native',
59
80
  classification: options.getNativeCompatibilityClassification(compatibility),
60
- installCommand: compatibility.installCommand || installCommand,
61
- updateCommand: compatibility.updateCommand || compatibility.installCommand || installCommand,
81
+ installIdentity,
82
+ installCommand,
83
+ updateCommand: installCommand,
62
84
  currentNativeVersion: compatibility.currentNativeVersion
63
85
  || compatibility.nativeVersion
64
86
  || compatibility.native?.version
@@ -66,7 +88,7 @@
66
88
  requiredVersion: compatibility.requiredVersion || Compatibility?.BUILD_TARGET_VERSION || '',
67
89
  releaseUrl: compatibility.releaseUrl || ''
68
90
  };
69
- notifyBlocked(error, compatibility);
91
+ notifyBlocked(error, compatibility, method);
70
92
  return { ok: false, compatibility, response: { ok: false, error } };
71
93
  }
72
94
 
@@ -81,7 +103,7 @@
81
103
  };
82
104
  }
83
105
 
84
- function notifyBlocked(error, compatibility = {}) {
106
+ function notifyBlocked(error, compatibility = {}, method) {
85
107
  const message = error?.message || String(error || '');
86
108
  if (options.getCurrentRunView?.()) {
87
109
  options.appendRunEvent?.({ title: message, status: 'failed' });
@@ -89,7 +111,17 @@
89
111
  return;
90
112
  }
91
113
  showUpdateGuidance(compatibility);
92
- options.showPluginToast?.(message, { status: 'failed', sticky: true });
114
+ const missing = compatibility.status === 'native_missing';
115
+ options.showPluginToast?.(missing ? tr('nativeCompatibilityMissingGuidance') : message, {
116
+ status: 'failed',
117
+ sticky: true,
118
+ ...(missing ? {
119
+ title: tr('nativeCompatibilityMissingTitle'),
120
+ groupKey: 'native-missing',
121
+ groupItem: method || 'native request',
122
+ groupSummary: count => tr('nativeCompatibilityUnavailableFeatures', { count })
123
+ } : {})
124
+ });
93
125
  }
94
126
 
95
127
  async function refreshBadge() {
@@ -102,6 +134,7 @@
102
134
  : {};
103
135
  const response = await sendBackgroundNative({ method: 'bridge.ping', params });
104
136
  const compatibility = Compatibility.evaluateNativeCompatibility(response, metadata);
137
+ resolveInstallCommand(compatibility);
105
138
  const classification = options.getNativeCompatibilityClassification(compatibility);
106
139
  if (classification === 'compatible') {
107
140
  options.setBadge?.(renderer.headerEl, { type: 'none' });
@@ -158,9 +191,7 @@
158
191
  || compatibility.nativeVersion
159
192
  || native.version
160
193
  || tx('missing', '未安装');
161
- const command = compatibility.updateCommand
162
- || compatibility.installCommand
163
- || options.installCommand;
194
+ const command = resolveInstallCommand(compatibility);
164
195
  const releaseUrl = compatibility.releaseUrl || '';
165
196
  const overlay = options.document.createElement('div');
166
197
  overlay.className = 'codex-plugin-confirm';
@@ -176,11 +207,24 @@
176
207
  );
177
208
  const card = options.document.createElement('section');
178
209
  card.className = 'codex-plugin-confirm-card';
210
+ const head = options.document.createElement('div');
211
+ head.className = 'codex-plugin-confirm-head';
212
+ const icon = options.document.createElement('img');
213
+ icon.className = 'codex-plugin-confirm-icon';
214
+ icon.alt = '';
215
+ icon.setAttribute('aria-hidden', 'true');
216
+ icon.src = options.getIconUrl?.() || '';
217
+ const titleWrap = options.document.createElement('div');
218
+ const brand = options.document.createElement('div');
219
+ brand.className = 'codex-plugin-confirm-brand';
220
+ brand.textContent = 'Codex Overleaf Link';
179
221
  const title = options.document.createElement('div');
180
222
  title.className = 'codex-plugin-confirm-title';
181
223
  title.textContent = nativeMissing
182
224
  ? tx('One step left: install the native host', '还差一步:安装 Native host')
183
225
  : tx('Native host update available', 'Native host 可更新');
226
+ titleWrap.append(brand, title);
227
+ head.append(icon, titleWrap);
184
228
  const body = options.document.createElement('div');
185
229
  body.className = 'codex-plugin-confirm-body';
186
230
  body.textContent = [
@@ -193,26 +237,24 @@
193
237
  `Extension v${extensionVersion} / Native v${nativeVersion}`,
194
238
  `扩展 v${extensionVersion} / Native v${nativeVersion}`
195
239
  ),
196
- tx(
197
- 'Run the platform-specific command below, reload the extension, then refresh Overleaf.',
198
- '运行下面的平台命令,重新加载扩展,然后刷新 Overleaf。'
199
- )
240
+ !command && installIdentity.managed
241
+ ? tx('Repair Managed using the original managed root, browser and extension ID with install-managed.',
242
+ '请使用原管理根目录、浏览器及扩展 ID,通过 install-managed 修复托管安装。')
243
+ : tx('Run the command below, reload the extension, then refresh Overleaf.', '运行下面的命令,重新加载扩展,然后刷新 Overleaf。')
244
+ + (installIdentity.managed && (native.platform || installIdentity.platform) === 'win32'
245
+ ? tx(' Use PowerShell.', ' 请使用 PowerShell。') : '')
200
246
  ].join('\n\n');
201
247
  const commandCode = options.document.createElement('code');
248
+ commandCode.className = 'codex-native-guidance-command';
202
249
  commandCode.textContent = command;
203
- Object.assign(commandCode.style, {
204
- display: 'block',
205
- whiteSpace: 'pre-wrap',
206
- marginTop: '0.75rem'
207
- });
208
250
  body.append(commandCode);
209
251
  if (releaseUrl) {
210
252
  const link = options.document.createElement('a');
253
+ link.className = 'codex-native-guidance-link';
211
254
  link.href = releaseUrl;
212
255
  link.target = '_blank';
213
256
  link.rel = 'noopener noreferrer';
214
257
  link.textContent = tx('Open GitHub Release', '打开 GitHub Release');
215
- Object.assign(link.style, { display: 'block', marginTop: '0.75rem' });
216
258
  body.append(link);
217
259
  }
218
260
  const actions = options.document.createElement('div');
@@ -221,7 +263,9 @@
221
263
  copy.type = 'button';
222
264
  copy.className = 'codex-plugin-confirm-confirm';
223
265
  copy.textContent = tx('Copy command', '复制命令');
266
+ copy.disabled = !command;
224
267
  copy.addEventListener('click', async () => {
268
+ if (!command) return;
225
269
  await options.navigator.clipboard.writeText(command);
226
270
  copy.textContent = tx('Copied', '已复制');
227
271
  });
@@ -231,13 +275,13 @@
231
275
  close.textContent = tx('Close', '关闭');
232
276
  close.addEventListener('click', () => overlay.remove());
233
277
  actions.append(close, copy);
234
- card.append(title, body, actions);
278
+ card.append(head, body, actions);
235
279
  overlay.append(card);
236
280
  overlay.addEventListener('click', event => {
237
281
  if (event.target === overlay) overlay.remove();
238
282
  });
239
283
  panel.append(overlay);
240
- copy.focus();
284
+ (command ? copy : close).focus();
241
285
  }
242
286
 
243
287
  function scheduleFirstUseOnboardingTip() {
@@ -833,7 +833,7 @@
833
833
  if (getCurrentProjectId() !== projectId) return { useExistingMirror: false, reason: 'project_changed' };
834
834
  const otWarmStart = otWarmMirrorController.canUseOtWarmStart({
835
835
  enabled: isExperimentalOtEnabled() && !otWarmMirrorState.failureActive,
836
- focusFiles,
836
+ focusFiles, focusFilesAreLiteral: true,
837
837
  mirrorStatus
838
838
  });
839
839
  if (!mirrorStatus?.exists || (!otWarmStart.ok && !isMirrorReusable(mirrorStatus))) {
@@ -858,10 +858,9 @@
858
858
  return { useExistingMirror: false, reason: 'overlay_probe_failed', mirrorStatus };
859
859
  }
860
860
  if (getCurrentProjectId() !== projectId) return { useExistingMirror: false, reason: 'project_changed' };
861
- const fileOverlays = buildSnapshotFileOverlays(overlayProject, focusFiles);
862
- if (!fileOverlays.length) {
863
- return { useExistingMirror: false, reason: 'missing_current_overlay', mirrorStatus };
864
- }
861
+ const fileOverlays = buildSnapshotFileOverlays(overlayProject, focusFiles).filter(file =>
862
+ !otWarmStart.ok || focusFiles.some(path => normalizeSnapshotPath(path) === normalizeSnapshotPath(file.path)));
863
+ if (!fileOverlays.length) return { useExistingMirror: false, reason: 'missing_current_overlay', mirrorStatus };
865
864
  if (focusFiles.length && !focusFiles.every(path => {
866
865
  const normalized = normalizeSnapshotPath(path);
867
866
  return fileOverlays.some(file => normalizeSnapshotPath(file.path) === normalized);
@@ -896,7 +895,7 @@
896
895
  appendLog(tx(`Cannot continue: ${formatProjectSnapshotWarning(warning)}`, `无法继续:${formatProjectSnapshotWarning(warning)}`));
897
896
  }
898
897
  appendCompletionReport({
899
- conclusion: tr('warmMirrorStaleBlockedConclusion'),
898
+ conclusion: tr('warmMirrorStaleBlockedConclusion'), conclusionKey: 'warm_mirror_stale',
900
899
  status: 'blocked',
901
900
  operations: [],
902
901
  applyResults: [],
@@ -1,10 +1,10 @@
1
1
  (function initCodexOverleafOtWarmMirrorController(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory(require('../shared/projectFiles'));
4
4
  } else {
5
- root.CodexOverleafOtWarmMirrorController = factory();
5
+ root.CodexOverleafModuleRegistry.define('OtWarmMirrorController', ['ProjectFiles'], factory);
6
6
  }
7
- })(typeof window !== 'undefined' ? window : globalThis, function otWarmMirrorControllerFactory() {
7
+ })(typeof window !== 'undefined' ? window : globalThis, function otWarmMirrorControllerFactory(projectFiles) {
8
8
  'use strict';
9
9
 
10
10
  const OT_POLL_INTERVAL_MS = 1000;
@@ -17,6 +17,7 @@
17
17
  const OT_MAX_BATCH_BYTES = 2 * 1024 * 1024;
18
18
  const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
19
19
  const PAUSE_STATES = new Set(['running', 'writing', 'undoing', 'compiling']);
20
+ const normalizeLiteralPath = projectFiles.normalizeSafeProjectPath;
20
21
 
21
22
  function buildPatchFilesRequest({ projectId, events, nativeCompatibility } = {}) {
22
23
  const files = [];
@@ -24,8 +25,8 @@
24
25
  if (files.length >= OT_MAX_PATCH_BATCH) {
25
26
  break;
26
27
  }
27
- const filePath = normalizePath(event?.path);
28
- if (!isSafeRelativePath(filePath) || typeof event?.baseHash !== 'string') {
28
+ const filePath = normalizeLiteralPath(event?.path);
29
+ if (!filePath || typeof event?.baseHash !== 'string') {
29
30
  continue;
30
31
  }
31
32
  const baseHash = event.baseHash.trim();
@@ -73,7 +74,7 @@
73
74
  return { pause: false };
74
75
  }
75
76
 
76
- function canUseOtWarmStart({ enabled, focusFiles, mirrorStatus, now = Date.now() } = {}) {
77
+ function canUseOtWarmStart({ enabled, focusFiles, focusFilesAreLiteral = false, mirrorStatus, now = Date.now() } = {}) {
77
78
  if (!enabled) {
78
79
  return { ok: false, reason: 'disabled' };
79
80
  }
@@ -82,7 +83,10 @@
82
83
  }
83
84
  if (mirrorStatus.dirty === true) return { ok: false, reason: 'mirror_dirty' };
84
85
 
85
- const normalizedFocusFiles = normalizePaths(focusFiles);
86
+ if (focusFilesAreLiteral === true && Array.isArray(focusFiles) && focusFiles.some(path => !normalizeLiteralPath(path))) {
87
+ return { ok: false, reason: 'invalid_focus_paths' };
88
+ }
89
+ const normalizedFocusFiles = normalizePaths(focusFiles, focusFilesAreLiteral === true);
86
90
  if (!normalizedFocusFiles.length) {
87
91
  return { ok: false, reason: 'no_focus_files' };
88
92
  }
@@ -94,7 +98,7 @@
94
98
  }
95
99
  const age = now - Date.parse(file.observedAt ?? file.lastPatchAt ?? '');
96
100
  if (!Number.isFinite(age) || age < 0 || age > OT_FRESHNESS_MAX_AGE_MS) continue;
97
- const filePath = normalizePath(file.path);
101
+ const filePath = normalizeLiteralPath(file.path);
98
102
  if (filePath) {
99
103
  freshFiles.add(filePath);
100
104
  }
@@ -124,9 +128,9 @@
124
128
  let bytes = next.reduce((sum, event) => sum + patchBytes(event), 0);
125
129
  if (next.length > OT_MAX_PENDING_EVENTS || bytes > OT_MAX_PENDING_BYTES) return { ok: false, reason: 'ot_queue_limit' };
126
130
  for (const input of events) {
127
- const path = normalizePath(input?.path);
131
+ const path = normalizeLiteralPath(input?.path);
128
132
  const time = typeof input?.observedAt === 'number' ? input.observedAt : Date.parse(input?.observedAt || '');
129
- if (!isSafeRelativePath(path) || path.length > 1024 || typeof input?.nextContent !== 'string' ||
133
+ if (!path || path.length > 1024 || typeof input?.nextContent !== 'string' ||
130
134
  typeof input.baseHash !== 'string' || !input.baseHash || input.baseHash.length > 128) {
131
135
  return { ok: false, reason: 'ot_event_invalid' };
132
136
  }
@@ -165,7 +169,7 @@
165
169
  }
166
170
  if (!events.length || Number(result.appliedCount) !== events.length || !Array.isArray(result.appliedFiles) ||
167
171
  result.appliedFiles.length !== events.length || !result.appliedFiles.every((file, index) =>
168
- normalizePath(file?.path) === events[index].path && (!events[index].nextHash || file.hash === events[index].nextHash))) {
172
+ normalizeLiteralPath(file?.path) === events[index].path && (!events[index].nextHash || file.hash === events[index].nextHash))) {
169
173
  return { ok: false, reason: 'mirror_patch_invalid_result' };
170
174
  }
171
175
  if (events.some(event => { const time = Date.parse(event.observedAt);
@@ -220,11 +224,11 @@
220
224
  .replace(/^\/+/, '');
221
225
  }
222
226
 
223
- function normalizePaths(value) {
227
+ function normalizePaths(value, literal = false) {
224
228
  const seen = new Set();
225
229
  const paths = [];
226
230
  for (const item of Array.isArray(value) ? value : []) {
227
- const filePath = normalizePath(item);
231
+ const filePath = literal ? normalizeLiteralPath(item) : normalizePath(item);
228
232
  if (!filePath || seen.has(filePath)) {
229
233
  continue;
230
234
  }
@@ -234,13 +238,6 @@
234
238
  return paths;
235
239
  }
236
240
 
237
- function isSafeRelativePath(filePath) {
238
- if (!filePath) {
239
- return false;
240
- }
241
- return !filePath.split('/').some(part => part === '.' || part === '..');
242
- }
243
-
244
241
  function isPrimitiveMetadata(value) {
245
242
  return ['string', 'number', 'boolean'].includes(typeof value);
246
243
  }
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const PAGE_WORLD_SCRIPTS = Object.freeze([
9
- ['src/shared/reviewing.js', 'codex-overleaf-reviewing-script'],
9
+ ['src/shared/reviewing.js', 'codex-overleaf-reviewing-script', true],
10
10
  ['src/shared/projectFiles.js', 'codex-overleaf-project-files-script'],
11
11
  ['src/shared/compatibility.js', 'codex-overleaf-compatibility-page-script'],
12
12
  ['src/shared/pageRpcContract.js', 'codex-overleaf-page-rpc-contract-script', true],
@@ -15,20 +15,23 @@
15
15
  ['src/shared/sensitiveScan.js', 'codex-overleaf-sensitive-scan-script'],
16
16
  ['src/shared/auditRecords.js', 'codex-overleaf-audit-records-script'],
17
17
  ['src/page/saveState.js', 'codex-overleaf-save-state-script', true],
18
- ['src/page/overleafCapabilities.js', 'codex-overleaf-capabilities-script'],
18
+ ['src/page/overleafCapabilities.js', 'codex-overleaf-capabilities-script', true],
19
19
  ['src/page/compileBridge.js', 'codex-overleaf-compile-bridge-script'],
20
20
  ['src/page/overleafEditor.js', 'codex-overleaf-editor-script'],
21
21
  ['src/page/overleafProjectSnapshot.js', 'codex-overleaf-project-snapshot-script'],
22
22
  ['src/page/pageBridgeCapability.js', 'codex-overleaf-page-bridge-capability-script'],
23
23
  ['src/page/treeOperations.js', 'codex-overleaf-tree-operations-script', true],
24
+ ['src/page/ownedDirectoryCleanup.js', 'codex-overleaf-owned-directory-cleanup-script', true],
24
25
  ['src/page/textFileCreator.js', 'codex-overleaf-text-file-creator-script', true],
25
26
  ['src/page/binaryAssetUploader.js', 'codex-overleaf-binary-asset-uploader-script', true],
26
27
  ['src/page/snapshotRouter.js', 'codex-overleaf-snapshot-router-script'],
27
28
  ['src/page/referenceProjects.js', 'codex-overleaf-reference-projects-script', true],
29
+ ['src/page/fileMover.js', 'codex-overleaf-file-mover-script', true],
28
30
  ['src/page/writeGuard.js', 'codex-overleaf-write-guard-script', true],
29
31
  ['src/page/trackedChangeOwnership.js', 'codex-overleaf-tracked-change-ownership-script', true],
30
32
  ['src/page/trackedChangeCapture.js', 'codex-overleaf-tracked-change-capture-script', true],
31
33
  ['src/page/trackedChangeReplay.js', 'codex-overleaf-tracked-change-replay-script', true],
34
+ ['src/page/reviewContext.js', 'codex-overleaf-review-context-script', true],
32
35
  ['src/page/trackedChangesLifecycle.js', 'codex-overleaf-tracked-changes-lifecycle-script', true],
33
36
  ['src/page/writebackRouter.js', 'codex-overleaf-writeback-router-script', true],
34
37
  ['src/page/writebackReceiptJournal.js', 'codex-overleaf-writeback-receipt-script', true],
@@ -75,6 +78,11 @@
75
78
  const requestTimeoutMs = Number.isFinite(Number(policy.timeoutMs))
76
79
  ? Number(policy.timeoutMs)
77
80
  : Contract.resolveTimeoutMs(method, timeoutOverrides);
81
+ // Review preparation and its nested writes share the transport budget.
82
+ if (Contract.getMethod(method)?.timeoutClass === 'lifecycle') {
83
+ params = { ...params, deadlineAt: params?.deadlineAt == null ? now() + requestTimeoutMs
84
+ : Number.isFinite(params.deadlineAt) ? Math.min(params.deadlineAt, now() + requestTimeoutMs) : params.deadlineAt };
85
+ }
78
86
  const remainingMs = params?.deadlineAt == null ? Infinity
79
87
  : Number.isFinite(params.deadlineAt) ? params.deadlineAt - now() : 0;
80
88
  if (remainingMs <= 0) {
@@ -84,7 +92,7 @@
84
92
  }
85
93
  const timeoutMs = Math.min(requestTimeoutMs, remainingMs);
86
94
  const cancellable = policy.cancellation === 'content_abort';
87
- let settled = false;
95
+ let settled = false, dispatched = false;
88
96
  let timeout = null;
89
97
  function cleanup() {
90
98
  if (timeout !== null) windowRef.clearTimeout(timeout);
@@ -107,8 +115,13 @@
107
115
  const error = new Error('Codex run was cancelled by the user');
108
116
  error.code = 'codex_cancelled';
109
117
  error.cancelled = true;
118
+ error.requestDispatched = dispatched;
119
+ error.changedDocument = dispatched && Contract.getMethod(method)?.mutation === 'document';
120
+ if (dispatched && method === 'applyOperations') { error.requestId = id; error.rpcMethod = method; }
110
121
  rejectOnce(error);
111
122
  }
123
+ // Cancellation stops new requests; receipt settlement has its own non-cancellable policy.
124
+ if (cancellable && options.isCancellationRequested?.()) { cancelRequest(); return; }
112
125
  timeout = windowRef.setTimeout(() => {
113
126
  resolveOnce({ ok: false, error: 'Page bridge timed out',
114
127
  ...(method === 'applyOperations' ? { code: 'page_bridge_timeout', rpcMethod: method, requestId: id } : {}) });
@@ -126,11 +139,11 @@
126
139
  }
127
140
  if (cancellable) {
128
141
  activeCancellationHandlers.set(id, cancelRequest);
129
- if (options.isCancellationRequested?.()) windowRef.queueMicrotask(cancelRequest);
130
142
  }
131
143
  windowRef.addEventListener('message', onMessage);
132
144
  // Every low-level request remains one attempt. Higher layers must opt
133
145
  // into replay so a document mutation is never duplicated implicitly.
146
+ dispatched = true;
134
147
  windowRef.postMessage({
135
148
  source: 'codex-overleaf/content',
136
149
  id,
@@ -24,6 +24,7 @@
24
24
  openCustomInstructionsSettings,
25
25
  onHistoryRowJump,
26
26
  resetProjectNameCache,
27
+ clearHistoryComposer,
27
28
  getPanel,
28
29
  getState,
29
30
  setState,
@@ -110,27 +111,54 @@
110
111
  if (!confirmed) {
111
112
  return;
112
113
  }
114
+ if (getCurrentRunView()) {
115
+ showPluginToast(tx('A run started while confirmation was open. Finish or cancel it before clearing history.', '确认期间有任务开始运行,请先完成或取消任务后再清理历史。'));
116
+ return;
117
+ }
118
+ let historyClearedAt;
113
119
  try {
114
- await StorageDb?.clearAllStores?.();
120
+ await StorageDb.clearAllStores();
121
+ historyClearedAt = Date.now();
115
122
  historyView.invalidate();
116
- // v1.8.1: the dashboard's project-name cache lives in
117
- // chrome.storage.local, outside the IndexedDB stores — clear it too so
118
- // no (potentially sensitive) project titles outlive the wipe.
119
- await resetProjectNameCache?.();
120
123
  } catch (error) {
121
124
  showPluginToast(tx(`Could not clear history: ${error.message}`, `清空历史失败:${error.message}`));
122
125
  return;
123
126
  }
124
- // Reset the in-getPanel() getState() to match the emptied store and re-render
125
- // EXPLICITLY. startNewSession() is unusable here: its empty-session reuse
126
- // guard matches the fresh session that normalization mints and returns
127
- // before applyStateToPanel(), leaving the deleted cards on screen.
128
- setState(normalizePanelState({ ...getState(), sessions: [], runs: [], activeSessionId: '' }));
129
- await saveState();
127
+ const cleanupErrors = [];
128
+ try {
129
+ // Project names live outside the history store. Failure here must not
130
+ // leave deleted history in memory for a later autosave to replay.
131
+ await resetProjectNameCache?.();
132
+ } catch (error) {
133
+ cleanupErrors.push(error.message);
134
+ }
135
+ // Clear the compatibility mirror as well as the session list; otherwise
136
+ // normalization can reconstruct old summary runs and the Native thread.
137
+ const clearedState = normalizePanelState({
138
+ ...getState(), session: null, sessions: [], runs: [], activeSessionId: '',
139
+ task: '', focusFiles: [], selectionContext: null, composerAttachments: []
140
+ });
141
+ const freshSession = clearedState.sessions[0];
142
+ // The shared store rejects createdAt <= its clear marker. A new session
143
+ // in the same millisecond needs the next tick, at most 1 ms ahead of now.
144
+ freshSession.createdAt = freshSession.updatedAt = new Date(Math.max(Date.now(), historyClearedAt + 1)).toISOString();
145
+ setState(clearedState);
146
+ try {
147
+ clearHistoryComposer?.();
148
+ if (await saveState({ requireDurable: true }) !== true) {
149
+ throw new Error(tx('The blank session save was not confirmed.', '空白会话的保存结果尚未确认。'));
150
+ }
151
+ } catch (error) {
152
+ cleanupErrors.push(error.message);
153
+ }
154
+ // The store has already been cleared, including when the fresh-session
155
+ // save fails. Always remove old cards and draft inputs from the panel.
130
156
  applyStateToPanel();
131
157
  refreshStorageUsageSummary();
132
158
  void renderAuditHistoryPanel();
133
- showPluginToast(tx('Local history cleared. Overleaf files are unchanged.', '本地历史已清理,Overleaf 文件保持不变。'));
159
+ showPluginToast(cleanupErrors.length
160
+ ? tx(`Saved history was removed, but cleanup or blank-session saving needs attention: ${cleanupErrors.join('; ')}`, `已删除保存的历史,但清理或空白会话保存仍有问题:${cleanupErrors.join(';')}`)
161
+ : tx('Local history cleared. Overleaf files are unchanged.', '本地历史已清理,Overleaf 文件保持不变。'));
134
162
  getPanel()?.querySelector('[data-task]')?.focus();
135
163
  }
136
164
 
@@ -153,6 +181,7 @@
153
181
  // the failed task usually needs a tweak before it is worth resending.
154
182
  function refillComposerForRetry(run, textOverride) {
155
183
  const input = getPanel()?.querySelector('[data-task]');
184
+ if (input?.readOnly) return;
156
185
  const task = typeof textOverride === 'string' && textOverride.trim()
157
186
  ? textOverride
158
187
  : String(run?.task || '');
@@ -282,7 +311,9 @@
282
311
  // Settings history card, expanded and loaded.
283
312
  function openChangeHistory() {
284
313
  openCustomInstructionsSettings();
285
- const card = getSettingsPanelInstance()?.container?.querySelector('[data-history-card]');
314
+ // The workbench shows one page at a time; switch to the page holding the card.
315
+ const card = getSettingsPanelInstance()?.revealGroup?.('history')
316
+ || getSettingsPanelInstance()?.container?.querySelector('[data-history-card]');
286
317
  if (card) {
287
318
  if (!card.open) {
288
319
  card.open = true;
@@ -294,7 +325,8 @@
294
325
 
295
326
  function openStorageSettings() {
296
327
  openCustomInstructionsSettings();
297
- const card = getPanel()?.querySelector('[data-storage-card]');
328
+ const card = getSettingsPanelInstance()?.revealGroup?.('storage')
329
+ || getPanel()?.querySelector('[data-storage-card]');
298
330
  if (card) {
299
331
  card.open = true;
300
332
  card.scrollIntoView({ block: 'center' });