codex-overleaf-link 2.2.1 → 2.3.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 (79) hide show
  1. package/README.md +25 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/runtime-manifest.json +5 -71
  4. package/extension/src/background.js +67 -63
  5. package/extension/src/backgroundUpdateCoordinator.js +128 -66
  6. package/extension/src/content/applyResultFormatters.js +2 -3
  7. package/extension/src/content/composerPanel.js +0 -2
  8. package/extension/src/content/contentRuntime.js +757 -1558
  9. package/extension/src/content/contextTray.js +4 -6
  10. package/extension/src/content/diagnosticsController.js +1 -2
  11. package/extension/src/content/diffReviewPanel.js +14 -13
  12. package/extension/src/content/generated/content.bundle.js +41605 -0
  13. package/extension/src/content/generated/content.bundle.meta.json +103 -0
  14. package/extension/src/content/legacyGlobalRegistry.js +129 -0
  15. package/extension/src/content/markdownText.js +17 -17
  16. package/extension/src/content/mathText.js +155 -9
  17. package/extension/src/content/modelPicker.js +1 -1
  18. package/extension/src/content/modelPickerSupport.js +10 -5
  19. package/extension/src/content/moduleRegistryKernel.js +125 -0
  20. package/extension/src/content/nativeCompatibilityController.js +313 -0
  21. package/extension/src/content/otWarmMirror.js +8 -6
  22. package/extension/src/content/pageBridgeClient.js +224 -0
  23. package/extension/src/content/panelMaintenance.js +8 -3
  24. package/extension/src/content/postNavigationSettlementPersistence.js +100 -0
  25. package/extension/src/content/projectProviderSelection.js +4 -12
  26. package/extension/src/content/projectSessionCleanup.js +3 -3
  27. package/extension/src/content/providerSettingsCoordinator.js +59 -26
  28. package/extension/src/content/providerSettingsDialog.js +30 -18
  29. package/extension/src/content/recentProjects.js +15 -23
  30. package/extension/src/content/runGuidanceController.js +1 -1
  31. package/extension/src/content/runQueueScheduler.js +129 -29
  32. package/extension/src/content/runSettlementPersistence.js +51 -0
  33. package/extension/src/content/runTimelineView.js +17 -8
  34. package/extension/src/content/scopedPersistenceCoordinator.js +100 -0
  35. package/extension/src/content/scopedPersistenceDetachedCommit.js +47 -0
  36. package/extension/src/content/scopedPersistencePanelState.js +111 -0
  37. package/extension/src/content/sessionManager.js +15 -12
  38. package/extension/src/content/sessionPersistence.js +67 -8
  39. package/extension/src/content/updateNotice.js +10 -4
  40. package/extension/src/content/writebackOrchestrator.js +89 -68
  41. package/extension/src/contentScript.js +5 -1
  42. package/extension/src/page/trackedChangesLifecycle.js +16 -5
  43. package/extension/src/page/writebackRouter.js +6 -2
  44. package/extension/src/pageBridge.js +47 -98
  45. package/extension/src/shared/agentTranscript.js +18 -8
  46. package/extension/src/shared/compatibility.js +1 -1
  47. package/extension/src/shared/governanceRules.js +1 -1
  48. package/extension/src/shared/i18n.js +2 -4
  49. package/extension/src/shared/managedUpdateProjection.js +337 -0
  50. package/extension/src/shared/nativeRequestIdentity.js +60 -0
  51. package/extension/src/shared/otText.js +0 -19
  52. package/extension/src/shared/pageRpcContract.js +136 -0
  53. package/extension/src/shared/runExecutionSnapshot.js +187 -0
  54. package/extension/src/shared/runExecutionSnapshotCodec.js +171 -0
  55. package/extension/src/shared/runInputQueue.js +81 -34
  56. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +79 -0
  57. package/extension/src/shared/scopedPersistenceQueuePolicy.js +200 -0
  58. package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
  59. package/extension/src/shared/scopedPersistenceTransaction.js +241 -0
  60. package/extension/src/shared/sessionState.js +110 -59
  61. package/extension/src/shared/settlementFacts.js +351 -0
  62. package/extension/src/shared/storageDb.js +25 -10
  63. package/extension/src/shared/storageMigration.js +275 -61
  64. package/extension/src/shared/storageSessionClaims.js +60 -0
  65. package/extension/src/shared/updateConsent.js +4 -0
  66. package/extension/src/shared/updateRevocationIntent.js +109 -0
  67. package/extension/src/shared/writebackEvidenceProjection.js +150 -0
  68. package/extension/src/shared/writebackOperationEvidence.js +153 -0
  69. package/extension/src/shared/writebackSettlement.js +727 -0
  70. package/extension/styles/panel.css +18 -8
  71. package/native-host/src/codexPromptAssembly.js +1 -1
  72. package/native-host/src/codexRuntimeIdentity.js +1 -1
  73. package/native-host/src/codexSessionRunner.js +7 -13
  74. package/native-host/src/index.js +3 -69
  75. package/native-host/src/managedInstall.js +50 -8
  76. package/native-host/src/nativeTransportEnvelope.js +148 -0
  77. package/native-host/src/taskRunnerRuntime.js +14 -84
  78. package/native-host/src/updateManager.js +36 -1
  79. package/package.json +13 -5
@@ -25,7 +25,6 @@
25
25
  let appendOperationsPreview;
26
26
  let appendPartialWritebackWarning;
27
27
  let appendApplyResult;
28
- let renderDiffReview;
29
28
  let renderReadOnlyDiffReview;
30
29
  let showPluginConfirm;
31
30
  let callPageBridge;
@@ -53,11 +52,21 @@
53
52
  let buildReviewingBlockedApplyResult;
54
53
  let ensureReviewingBeforeWrite;
55
54
  let confirmBinaryOperations;
55
+ let compileAdapter;
56
56
  let filterSyncChangesByOperations;
57
57
  let writebackController;
58
58
  let RUN_SNAPSHOT_ZIP_TIMEOUT_MS;
59
59
  let getState;
60
60
  let getCurrentRunView;
61
+ let onMirrorRefreshSettled;
62
+ let writebackSettlement;
63
+ let injectedWritebackSettlement;
64
+
65
+ function buildSettlement(input = {}) {
66
+ return writebackSettlement?.settle instanceof Function
67
+ ? writebackSettlement.settle(input)
68
+ : null;
69
+ }
61
70
 
62
71
  async function applySyncChangesToOverleaf(syncChanges = [], project = {}, options = {}) {
63
72
  const runMode = options.mode || getState().mode;
@@ -86,13 +95,17 @@
86
95
  applyResults: [],
87
96
  mode: runMode,
88
97
  nextStep: tx(
89
- 'Switch to Suggest or Auto only when you want Codex to edit files.',
90
- '只有希望 Codex 修改文件时,才需要切换到建议修改或自动写入。'
98
+ 'Switch to Auto only when you want Codex to edit files.',
99
+ '只有希望 Codex 修改文件时,才需要切换到自动写入。'
91
100
  )
92
101
  });
93
102
  return {
94
103
  summaryLine: assistantMessage || tx('Ask mode completed without Overleaf changes', 'Ask 模式已完成,未修改 Overleaf'),
95
104
  hasSkippedOperations: false,
105
+ settlement: buildSettlement({
106
+ operations: [],
107
+ applyResult: { ok: true, applied: [], skipped: [] }
108
+ }),
96
109
  audit: buildAuditSummaryFromApply({
97
110
  operations: [],
98
111
  resultStatus: 'ask_ignored_local_changes',
@@ -138,16 +151,23 @@
138
151
  conclusion: assistantMessage || tx('Codex finished locally. There are no changes to sync back to Overleaf.', 'Codex 已完成本地处理,没有需要同步回 Overleaf 的改动。'),
139
152
  status: runMode === 'ask' ? tr('modeAsk') : 'completed',
140
153
  operations: [],
141
- applyResults: [],
142
- unchangedReason: assistantMessage
143
- ? tx('No file changes need to sync back to Overleaf.', '没有产生需要同步回 Overleaf 的文件改动。')
144
- : formatUnsupportedLocalChangeSummary(unsupportedChanges),
154
+ applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
155
+ unchangedReason: additionalSkippedEntries.find(item => item?.result?.reason)?.result.reason
156
+ || (assistantMessage ? tx('No file changes need to sync back to Overleaf.', '没有产生需要同步回 Overleaf 的文件改动。') : formatUnsupportedLocalChangeSummary(unsupportedChanges)),
145
157
  mode: runMode,
146
- nextStep: tx('You can continue the conversation, or adjust @context and run again.', '可以继续追问,或调整 @context 后重新运行。')
158
+ nextStep: additionalSkippedEntries.length ? formatWritebackSkippedNextStep({ ok: false, applied: [], skipped: additionalSkippedEntries }) : tx('You can continue the conversation, or adjust @context and run again.', '可以继续追问,或调整 @context 后重新运行。')
147
159
  });
148
160
  return {
149
161
  summaryLine: assistantMessage || tx('No changes to sync', '没有需要同步的改动'),
150
162
  hasSkippedOperations: additionalSkippedEntries.length > 0,
163
+ settlement: buildSettlement({
164
+ operations: buildSyncApplyOperations(syncChanges, project),
165
+ applyResult: {
166
+ ok: additionalSkippedEntries.length === 0,
167
+ applied: [],
168
+ skipped: additionalSkippedEntries
169
+ }
170
+ }),
151
171
  audit: buildAuditSummaryFromApply({
152
172
  operations: buildSyncApplyOperations(syncChanges, project),
153
173
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
@@ -157,47 +177,6 @@
157
177
  };
158
178
  }
159
179
 
160
- if (runMode === 'confirm') {
161
- appendRunEvent({
162
- title: tx(
163
- `Local Codex produced ${operations.length} change(s). Review the diff before applying.`,
164
- `本地 Codex 产生了 ${operations.length} 项改动,请查看差异后确认。`
165
- ),
166
- status: 'running'
167
- });
168
- const accepted = await renderDiffReview(visibleSyncChanges);
169
- if (!accepted.length) {
170
- appendRunEvent({
171
- title: tx('Sync cancelled: Overleaf was not modified.', '已取消同步:Overleaf 没有被修改。'),
172
- status: 'completed'
173
- });
174
- appendCompletionReport({
175
- conclusion: tx('You cancelled syncing. Local Codex changes were not written back to Overleaf.', '你取消了同步,本地 Codex 改动没有写回 Overleaf。'),
176
- status: 'rejected',
177
- operations: [],
178
- applyResults: [],
179
- mode: runMode,
180
- nextStep: tx('Run the task again, or switch to Auto if you want changes synced directly.', '可以重新运行任务,或切换到自动写入后再同步。')
181
- });
182
- return {
183
- summaryLine: tx('Sync cancelled', '已取消同步'),
184
- hasSkippedOperations: additionalSkippedEntries.length > 0,
185
- audit: buildAuditSummaryFromApply({
186
- operations,
187
- applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
188
- blockedFiles: additionalSkippedEntries.map(item => summarizeOperationForAudit(item.operation, item.result, 'blocked')),
189
- resultStatus: 'rejected'
190
- })
191
- };
192
- }
193
- operations = buildSyncApplyOperations(accepted, project);
194
- const acceptedPathSafety = partitionUnsafeProjectPathOperations(operations);
195
- if (acceptedPathSafety.skipped.length) {
196
- additionalSkippedEntries.push(...acceptedPathSafety.skipped);
197
- operations = acceptedPathSafety.safe;
198
- }
199
- }
200
-
201
180
  const deleteOperations = operations.filter(operation => operation.type === 'delete');
202
181
  if (deleteOperations.length) {
203
182
  const approved = await showPluginConfirm({
@@ -239,14 +218,23 @@
239
218
  conclusion: assistantMessage || tx('No approved changes were written back to Overleaf.', '没有已确认的改动写回 Overleaf。'),
240
219
  status: runMode === 'ask' ? tr('modeAsk') : 'completed',
241
220
  operations: [],
242
- applyResults: [],
243
- unchangedReason: tx('No approved file changes remain to sync back to Overleaf.', '没有剩余已确认的文件改动需要同步回 Overleaf。'),
221
+ applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
222
+ unchangedReason: additionalSkippedEntries.find(item => item?.result?.reason)?.result.reason
223
+ || tx('No approved file changes remain to sync back to Overleaf.', '没有剩余已确认的文件改动需要同步回 Overleaf。'),
244
224
  mode: runMode,
245
- nextStep: tx('You can continue the conversation, or adjust @context and run again.', '可以继续追问,或调整 @context 后重新运行。')
225
+ nextStep: additionalSkippedEntries.length ? formatWritebackSkippedNextStep({ ok: false, applied: [], skipped: additionalSkippedEntries }) : tx('You can continue the conversation, or adjust @context and run again.', '可以继续追问,或调整 @context 后重新运行。')
246
226
  });
247
227
  return {
248
228
  summaryLine: tx('No changes applied', '没有应用改动'),
249
229
  hasSkippedOperations: additionalSkippedEntries.length > 0,
230
+ settlement: buildSettlement({
231
+ operations: buildSyncApplyOperations(syncChanges, project),
232
+ applyResult: {
233
+ ok: additionalSkippedEntries.length === 0,
234
+ applied: [],
235
+ skipped: additionalSkippedEntries
236
+ }
237
+ }),
250
238
  audit: buildAuditSummaryFromApply({
251
239
  operations: buildSyncApplyOperations(syncChanges, project),
252
240
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
@@ -283,6 +271,7 @@
283
271
  '已阻止写入:未确认 Overleaf Reviewing/Track Changes'
284
272
  ),
285
273
  hasSkippedOperations: true,
274
+ settlement: buildSettlement({ operations, applyResult: blocked }),
286
275
  audit: buildAuditSummaryFromApply({
287
276
  operations,
288
277
  applyResults: [blocked],
@@ -342,6 +331,7 @@
342
331
  // pending promise is exposed so undoRun and the next runTask barrier on
343
332
  // it — a mirror.sync landing AFTER an undo would otherwise push the
344
333
  // pre-undo snapshot as the local baseline.
334
+ const mirrorSettlementTarget = getCurrentRunView?.();
345
335
  pendingMirrorRefresh = refreshProjectMirrorAfterWriteback(project, applied, saveVerification)
346
336
  .catch(error => {
347
337
  appendRunEvent({
@@ -351,12 +341,25 @@
351
341
  ),
352
342
  status: 'failed'
353
343
  });
344
+ return 'failed';
345
+ })
346
+ .then(state => {
347
+ if (typeof onMirrorRefreshSettled !== 'function') return;
348
+ return onMirrorRefreshSettled({
349
+ recordId: mirrorSettlementTarget?.recordId || '',
350
+ sessionId: mirrorSettlementTarget?.sessionId || '',
351
+ runProjectId: mirrorSettlementTarget?.runProjectId || '',
352
+ state: state || 'not-attempted'
353
+ });
354
354
  })
355
355
  .finally(() => {
356
356
  pendingMirrorRefresh = null;
357
357
  });
358
358
  const compileSummary = appliedPaths.length
359
- ? await autoRecompileAfterWriteback(appliedPaths, saveVerification).catch(error => {
359
+ ? await autoRecompileAfterWriteback(appliedPaths, saveVerification, {
360
+ autoRecompile: options.autoRecompile,
361
+ mode: runMode
362
+ }).catch(error => {
360
363
  appendRunEvent({
361
364
  title: tx(`Post-write compile failed: ${error.message}`, `写后编译出错:${error.message}`),
362
365
  status: 'failed'
@@ -419,6 +422,17 @@
419
422
  // Existing call sites that only read `summaryLine` / `hasSkippedOperations`
420
423
  // / `audit` are unaffected.
421
424
  applied,
425
+ settlement: buildSettlement({
426
+ operations,
427
+ applyResult: applied,
428
+ saveVerification,
429
+ mirror: {
430
+ state: appliedPaths.length && saveVerification?.state === 'verified_saved'
431
+ ? 'pending'
432
+ : 'not_started'
433
+ },
434
+ compile: compileSummary
435
+ }),
422
436
  audit: buildAuditSummaryFromApply({
423
437
  operations,
424
438
  applyResults: [applied],
@@ -447,8 +461,8 @@
447
461
  if (result?.ok === true && !result?.state) {
448
462
  return {
449
463
  ...result,
450
- ok: true,
451
- state: 'verified_saved'
464
+ ok: false,
465
+ state: 'verified_quiet'
452
466
  };
453
467
  }
454
468
  if (result?.state === 'unknown_timeout' || result?.state === 'unavailable') {
@@ -522,10 +536,10 @@
522
536
 
523
537
  async function refreshProjectMirrorAfterWriteback(project = {}, applied = {}, saveVerification = {}) {
524
538
  if (saveVerification?.state !== 'verified_saved') {
525
- return;
539
+ return 'not-attempted';
526
540
  }
527
541
  if (!Array.isArray(applied?.applied) || !applied.applied.length) {
528
- return;
542
+ return 'not-attempted';
529
543
  }
530
544
 
531
545
  if (await tryConfirmMirrorWriteback(applied)) {
@@ -536,7 +550,7 @@
536
550
  ),
537
551
  status: 'completed'
538
552
  });
539
- return;
553
+ return 'complete';
540
554
  }
541
555
 
542
556
  appendRunEvent({
@@ -567,7 +581,7 @@
567
581
  ),
568
582
  status: 'failed'
569
583
  });
570
- return;
584
+ return 'failed';
571
585
  }
572
586
 
573
587
  const syncedProject = mergeVerifiedAppliedFiles(freshProject, project, applied);
@@ -584,7 +598,7 @@
584
598
  title: tx('Local Codex workspace refreshed. The next run will start from the latest Overleaf content.', '已刷新本地 Codex workspace,下一轮会从最新 Overleaf 内容开始。'),
585
599
  status: 'completed'
586
600
  });
587
- return;
601
+ return 'complete';
588
602
  }
589
603
 
590
604
  const message = response?.error?.message || tx('native host did not respond', 'native host 没有响应');
@@ -595,6 +609,7 @@
595
609
  ),
596
610
  status: 'failed'
597
611
  });
612
+ return 'failed';
598
613
  }
599
614
 
600
615
  function mergeVerifiedAppliedFiles(freshProject = {}, originalProject = {}, applied = {}) {
@@ -615,11 +630,12 @@
615
630
  return writebackController.formatUnsupportedLocalChangeSummary(changes, getLocale());
616
631
  }
617
632
 
618
- async function autoRecompileAfterWriteback(writtenPaths = [], saveVerification = {}) {
619
- if (getState().autoRecompile === false) return null;
620
- if (getState().mode === 'ask') return null;
633
+ async function autoRecompileAfterWriteback(writtenPaths = [], saveVerification = {}, options = {}) {
634
+ if (options.autoRecompile === false
635
+ || (options.autoRecompile === undefined && getState().autoRecompile === false)) return null;
636
+ if ((options.mode || getState().mode) === 'ask') return null;
621
637
 
622
- const CompileAdapter = window.CodexOverleafCompileAdapter;
638
+ const CompileAdapter = compileAdapter;
623
639
  if (!CompileAdapter) return null;
624
640
 
625
641
  const hasCompilableFile = writtenPaths.some(filePath => CompileAdapter.isCompilableFile(filePath));
@@ -642,7 +658,8 @@
642
658
  const result = await callPageBridge('triggerCompile', {
643
659
  preferUiClick: true,
644
660
  waitForSaveMs: 5000,
645
- requireVerifiedSave: saveVerification?.state === 'verified_saved'
661
+ requireVerifiedSave: saveVerification?.state === 'verified_saved',
662
+ runProjectId: getCurrentRunView()?.runProjectId || getCurrentProjectId()
646
663
  });
647
664
  if (result?.ok) {
648
665
  let logResult = null;
@@ -650,7 +667,8 @@
650
667
  logResult = await callPageBridge('getCompileLog', {
651
668
  triggerIfStale: false,
652
669
  maxAgeMs: 30000,
653
- waitForSaveMs: 0
670
+ waitForSaveMs: 0,
671
+ runProjectId: getCurrentRunView()?.runProjectId || getCurrentProjectId()
654
672
  });
655
673
  } catch (_error) {
656
674
  logResult = null;
@@ -775,7 +793,8 @@
775
793
  const result = await callPageBridge('getCompileLog', {
776
794
  triggerIfStale: true,
777
795
  maxAgeMs: 30000,
778
- waitForSaveMs: 5000
796
+ waitForSaveMs: 5000,
797
+ runProjectId: getCurrentRunView()?.runProjectId || getCurrentProjectId()
779
798
  });
780
799
 
781
800
  if (!result?.ok) {
@@ -807,7 +826,6 @@
807
826
  appendOperationsPreview,
808
827
  appendPartialWritebackWarning,
809
828
  appendApplyResult,
810
- renderDiffReview,
811
829
  renderReadOnlyDiffReview,
812
830
  showPluginConfirm,
813
831
  callPageBridge,
@@ -841,6 +859,9 @@
841
859
  getState,
842
860
  getCurrentRunView,
843
861
  } = deps);
862
+ onMirrorRefreshSettled = deps.onMirrorRefreshSettled;
863
+ writebackSettlement = deps.writebackSettlement;
864
+ compileAdapter = deps.compileAdapter;
844
865
  return {
845
866
  applySyncChangesToOverleaf,
846
867
  resolveCompileLogContext,
@@ -4,5 +4,9 @@
4
4
  if (!window.CodexOverleafContentRuntime || typeof window.CodexOverleafContentRuntime.init !== 'function') {
5
5
  throw new Error('Codex Overleaf content runtime did not load.');
6
6
  }
7
- window.CodexOverleafContentRuntime.init();
7
+ const registry = window.CodexOverleafLegacyGlobalRegistry;
8
+ if (!registry || typeof registry.create !== 'function') {
9
+ throw new Error('Codex Overleaf dependency registry did not load.');
10
+ }
11
+ window.CodexOverleafContentRuntime.init(registry.create(window));
8
12
  })();
@@ -971,8 +971,15 @@
971
971
  }
972
972
 
973
973
  for (const path of paths) {
974
- if (path && getActiveFilePath() !== path) {
975
- const opened = await openFileByPath(path);
974
+ const postContent = postByPath.get(path);
975
+ const activePathMatches = getActiveFilePath() === path;
976
+ const activeContentMatches = readActiveEditorText() === postContent;
977
+ // Overleaf can update the selected tree path before CodeMirror swaps its
978
+ // document. In that state getActiveFilePath() already names `path`, while
979
+ // readActiveEditorText() still belongs to the previously viewed file.
980
+ // Force-reopen the target whenever either identity signal disagrees.
981
+ if (path && (!activePathMatches || !activeContentMatches)) {
982
+ const opened = await openFileByPath(path, { force: true });
976
983
  if (!opened.ok) {
977
984
  return {
978
985
  ok: false,
@@ -983,8 +990,7 @@
983
990
  }
984
991
  }
985
992
 
986
- const postContent = postByPath.get(path);
987
- const postReady = await waitForActiveEditorExpectedText(path, postContent, 2500);
993
+ const postReady = await waitForActiveEditorExpectedText(path, postContent, 9000);
988
994
  if (!postReady.ok) {
989
995
  return {
990
996
  ok: false,
@@ -1008,7 +1014,12 @@
1008
1014
  result: {
1009
1015
  ok: true,
1010
1016
  method: 'overleaf-editor-undo',
1011
- undoClicks: result.clicks
1017
+ undoClicks: result.clicks,
1018
+ // undoEditorHistoryUntilContent returns ok only after the active
1019
+ // editor exactly matches this run's pre-write checkpoint. Preserve
1020
+ // that proof for the content-side lifecycle settlement.
1021
+ verified: true,
1022
+ verifiedContent: expectedByPath.get(path)
1012
1023
  }
1013
1024
  });
1014
1025
  }
@@ -1667,13 +1667,17 @@
1667
1667
  continue;
1668
1668
  }
1669
1669
  const key = typeof ref.key === 'string' ? ref.key : '';
1670
- if (!key || seen.has(key)) {
1670
+ if (!key) {
1671
1671
  continue;
1672
1672
  }
1673
- seen.add(key);
1674
1673
  const hasPath = typeof ref.path === 'string';
1675
1674
  const hasExplicitPath = hasPath && ref.path.trim() !== '';
1676
1675
  const path = hasPath ? normalizeSafeProjectPath(ref.path) : '';
1676
+ const identity = `${path}\u0000${key}`;
1677
+ if (seen.has(identity)) {
1678
+ continue;
1679
+ }
1680
+ seen.add(identity);
1677
1681
  normalized.push({
1678
1682
  key,
1679
1683
  id: typeof ref.id === 'string' ? ref.id : '',
@@ -2,7 +2,8 @@
2
2
  'use strict';
3
3
 
4
4
  const PAGE_BRIDGE_INSTALL_VERSION = window.CodexOverleafCompatibility?.BUILD_TARGET_VERSION || 'dev';
5
- const PAGE_BRIDGE_INSTALL_REVISION = '2026-05-21-editor-readiness-v11';
5
+ const pageRpcContract = window.CodexOverleafPageRpcContract;
6
+ const PAGE_BRIDGE_INSTALL_REVISION = pageRpcContract.REVISION;
6
7
  if (window.__codexOverleafPageBridgeInstalledVersion === PAGE_BRIDGE_INSTALL_VERSION
7
8
  && window.__codexOverleafPageBridgeInstalledRevision === PAGE_BRIDGE_INSTALL_REVISION) {
8
9
  return;
@@ -152,10 +153,7 @@
152
153
  result = await dispatch(method, params || {});
153
154
  }
154
155
  } catch (error) {
155
- result = {
156
- ok: false,
157
- error: error.message
158
- };
156
+ result = pageRpcContract.normalizeFailure(method, error);
159
157
  }
160
158
 
161
159
  window.postMessage({
@@ -187,111 +185,62 @@
187
185
  return writeCancellationSequence;
188
186
  }
189
187
 
190
- async function dispatch(method, params) {
191
- if (method === 'probe') {
192
- return probe(params);
193
- }
194
- if (method === 'cancelActiveWrite') {
188
+ const pageRpcHandlers = {
189
+ probe,
190
+ cancelActiveWrite() {
195
191
  const sequence = bumpWriteCancellationSequence();
196
192
  return { ok: true, cancelled: true, sequence };
197
- }
198
- if (method === 'getProjectSnapshot') {
199
- return projectSnapshotBridge.getProjectSnapshot(withSnapshotCacheIdentity(params));
200
- }
201
- if (method === 'getProjectFileList') {
202
- return projectSnapshotBridge.getProjectFileList(params);
203
- }
204
- if (method === 'invalidateProjectSnapshot') {
193
+ },
194
+ getProjectSnapshot: params => projectSnapshotBridge.getProjectSnapshot(withSnapshotCacheIdentity(params)),
195
+ getProjectFileList: params => projectSnapshotBridge.getProjectFileList(params),
196
+ invalidateProjectSnapshot(params) {
205
197
  projectSnapshotBridge.invalidateProjectSnapshot(params);
206
198
  return { ok: true };
207
- }
208
- if (method === 'createCheckpoint') {
209
- return createCheckpoint(params.label);
210
- }
211
- if (method === 'ensureReviewing') {
212
- // Gate the Reviewing-mode toggle with the same writeGuard the per-write
213
- // path uses. Without this, a mid-flight SPA navigation could leave the
214
- // pre-flight toggle flipping Track Changes in a different Overleaf
215
- // project than the run was bound to. params.runProjectId is set by
216
- // the content-side caller (preflightWriteSafety / ensureReviewingBeforeWrite);
217
- // when absent (defensive), the guard short-circuits via its own
218
- // missing-runProjectId branch.
219
- if (typeof params?.runProjectId === 'string' && params.runProjectId) {
220
- const blocked = await writeGuard.runWriteGuard(params);
221
- if (blocked) return blocked;
222
- }
223
- return ensureReviewing(params);
224
- }
225
- if (method === 'ensureEditing') {
226
- if (typeof params?.runProjectId === 'string' && params.runProjectId) {
227
- const blocked = await writeGuard.runWriteGuard(params);
228
- if (blocked) return blocked;
229
- }
230
- return ensureEditing(params);
231
- }
232
- if (method === 'applyOperations') {
233
- // Welcome-panel + write-guard:
234
- // First gate: runProjectId vs page-side editorProjectId. Runs before
235
- // any reviewing / save-state / open-file readiness check so a
236
- // mid-run navigation cannot write into a different project. runWriteGuard
237
- // is async because it retries the page-side editorProjectId reader with
238
- // backoff to ride out Overleaf's hydration window.
239
- const blocked = await writeGuard.runWriteGuard(params);
240
- if (blocked) return blocked;
241
- return applyOperations(params.operations || [], {
199
+ },
200
+ createCheckpoint: params => createCheckpoint(params.label),
201
+ ensureReviewing,
202
+ ensureEditing,
203
+ applyOperations: params => applyOperations(params.operations || [], {
242
204
  baseFiles: params.baseFiles || null,
243
205
  reviewingPolicy: params.reviewingPolicy || '',
244
206
  requireReviewing: params.requireReviewing === true,
245
207
  requireEditing: params.requireEditing === true,
246
- // Propagate runProjectId so the writeback router's defense-in-depth
247
- // missing-runProjectId check sees it on this in-bridge dispatch path.
248
208
  runProjectId: typeof params.runProjectId === 'string' ? params.runProjectId : ''
249
- });
250
- }
251
- if (method === 'jumpToPosition') {
252
- return jumpToPosition(params);
253
- }
254
- if (method === 'rejectTrackedChanges') {
255
- const blocked = await writeGuard.runWriteGuard(params);
256
- if (blocked) return blocked;
257
- return rejectTrackedChanges(params);
209
+ }),
210
+ jumpToPosition,
211
+ rejectTrackedChanges,
212
+ acceptTrackedChanges,
213
+ triggerCompile: params => compileBridge.triggerCompile(params),
214
+ getCompileLog: params => compileBridge.getCompileLog(params),
215
+ getCompileState: () => compileBridge.getCompileState(),
216
+ waitForSaveState,
217
+ startOtObserver: params => otObserver.start(params),
218
+ stopOtObserver: () => otObserver.stop(),
219
+ getOtStatus: () => otObserver.getStatus(),
220
+ drainOtEvents() {
221
+ return { ok: true, events: otObserver.drainEvents().map(sanitizeOtEventForContent) };
258
222
  }
259
- if (method === 'acceptTrackedChanges') {
223
+ };
224
+
225
+ async function dispatch(method, params) {
226
+ const contract = pageRpcContract.getMethod(method);
227
+ if (!contract || method === PAGE_BRIDGE_CAPABILITY_METHOD) {
228
+ return { ok: false, error: `Unknown page bridge method: ${method}` };
229
+ }
230
+ const handler = pageRpcHandlers[method];
231
+ if (typeof handler !== 'function') {
232
+ return { ok: false, error: `Page bridge handler unavailable: ${method}` };
233
+ }
234
+ const hasRunProjectId = typeof params?.runProjectId === 'string'
235
+ && params.runProjectId.trim().length > 0;
236
+ if (
237
+ contract.projectIdentity === 'required'
238
+ || (contract.projectIdentity === 'optional' && hasRunProjectId)
239
+ ) {
260
240
  const blocked = await writeGuard.runWriteGuard(params);
261
241
  if (blocked) return blocked;
262
- return acceptTrackedChanges(params);
263
- }
264
- if (method === 'triggerCompile') {
265
- return compileBridge.triggerCompile(params);
266
- }
267
- if (method === 'getCompileLog') {
268
- return compileBridge.getCompileLog(params);
269
- }
270
- if (method === 'getCompileState') {
271
- return compileBridge.getCompileState();
272
- }
273
- if (method === 'waitForSaveState') {
274
- return waitForSaveState(params);
275
242
  }
276
- if (method === 'startOtObserver') {
277
- return otObserver.start(params);
278
- }
279
- if (method === 'stopOtObserver') {
280
- return otObserver.stop();
281
- }
282
- if (method === 'getOtStatus') {
283
- return otObserver.getStatus();
284
- }
285
- if (method === 'drainOtEvents') {
286
- return {
287
- ok: true,
288
- events: otObserver.drainEvents().map(sanitizeOtEventForContent)
289
- };
290
- }
291
- return {
292
- ok: false,
293
- error: `Unknown page bridge method: ${method}`
294
- };
243
+ return handler(params);
295
244
  }
296
245
 
297
246
  function sanitizeOtEventForContent(event = {}) {
@@ -468,7 +417,7 @@
468
417
  reviewing: window.CodexOverleafReviewing.detectReviewingFromSignals(reviewingSignals),
469
418
  reviewingDiagnostics: reviewingSignals.diagnostics,
470
419
  editor,
471
- capabilities: collectPageCapabilities(editor),
420
+ capabilities: pageRpcContract.withCapabilityReport(collectPageCapabilities(editor)),
472
421
  editorDiagnostics: buildEditorDiagnostics(editor),
473
422
  projectDiagnostics: {
474
423
  internalRootKeys: collectInternalRootKeys(),
@@ -5,11 +5,14 @@
5
5
  getI18n: function getI18nCjs() { return require('./i18n.js'); }
6
6
  });
7
7
  } else {
8
- // Browser script-tag load order is determined by manifest.json content_scripts;
9
- // resolve dependencies lazily so this module can be listed before its deps.
10
- root.CodexOverleafAgentTranscript = factory({
11
- getFailureReasons: function getFailureReasonsWindow() { return root.CodexOverleafFailureReasons || null; },
12
- getI18n: function getI18nWindow() { return root.CodexOverleafI18n || null; }
8
+ root.CodexOverleafModuleRegistry.define('AgentTranscript', [
9
+ 'FailureReasons',
10
+ 'I18n'
11
+ ], function createAgentTranscript(FailureReasons, I18n) {
12
+ return factory({
13
+ getFailureReasons: function getFailureReasonsWindow() { return FailureReasons; },
14
+ getI18n: function getI18nWindow() { return I18n; }
15
+ });
13
16
  });
14
17
  }
15
18
  })(typeof globalThis !== 'undefined' ? globalThis : window, function agentTranscriptFactory(deps) {
@@ -724,10 +727,17 @@
724
727
  if (providerError) {
725
728
  return providerError;
726
729
  }
727
- if (/Mode must be "confirm" or "auto"/i.test(text)) {
730
+ if (/suggest_mode_removed|Suggest mode has been removed/i.test(text)) {
731
+ return {
732
+ conclusion: textFor(locale, '建议修改模式已经移除。', 'Suggest mode has been removed.'),
733
+ nextStep: textFor(locale, '请刷新扩展,然后选择“只问不改”或“自动写入”。', 'Reload the extension, then choose Ask or Auto.'),
734
+ failureCode: 'suggest_mode_removed'
735
+ };
736
+ }
737
+ if (/Mode must be "(?:confirm|ask)" or "auto"/i.test(text)) {
728
738
  return {
729
739
  conclusion: textFor(locale, '这轮没有写入:当前是“只问不改”,但这个任务需要写入权限。', 'No files were written: this task needs write access, but the current mode is Ask.'),
730
- nextStep: textFor(locale, '请切换到“建议修改”或“自动写入”后重新运行。', 'Switch to Suggest or Auto and run the task again.'),
740
+ nextStep: textFor(locale, '请切换到“自动写入”后重新运行。', 'Switch to Auto and run the task again.'),
731
741
  failureCode: null // no direct catalog match — preflight / governance
732
742
  };
733
743
  }
@@ -794,7 +804,7 @@
794
804
  if (/checkpoint/i.test(text)) {
795
805
  return {
796
806
  conclusion: textFor(locale, '这轮没有自动写入:Codex 没有拿到可恢复版本。', 'This run did not auto-write: Codex did not get a recoverable version.'),
797
- nextStep: textFor(locale, '请切换到“建议修改”,或确认 Overleaf Reviewing 已开启后再用“自动写入”。', 'Switch to Suggest, or confirm Overleaf Reviewing is enabled before using Auto.'),
807
+ nextStep: textFor(locale, '请确认 Overleaf Reviewing 已开启后再用“自动写入”。', 'Confirm that Overleaf Reviewing is enabled before using Auto.'),
798
808
  failureCode: null // no current catalog code; preflight reviewing issue
799
809
  };
800
810
  }
@@ -12,7 +12,7 @@
12
12
  const MIN_NATIVE_VERSION = '1.0.0';
13
13
  const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
14
14
  const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
15
- const BUILD_TARGET_VERSION = '2.2.1';
15
+ const BUILD_TARGET_VERSION = '2.3.1';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -8,7 +8,7 @@
8
8
  }
9
9
  module.exports = factory(projectFiles);
10
10
  } else {
11
- root.CodexOverleafGovernanceRules = factory(root.CodexOverleafProjectFiles);
11
+ root.CodexOverleafModuleRegistry.define('GovernanceRules', ['ProjectFiles'], factory);
12
12
  }
13
13
  })(typeof globalThis !== 'undefined' ? globalThis : window, function governanceRulesFactory(projectFiles) {
14
14
  'use strict';