codex-overleaf-link 2.3.5 → 2.4.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 (57) hide show
  1. package/README.md +63 -31
  2. package/README.zh-CN.md +543 -0
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +7 -0
  5. package/extension/src/backgroundUpdateCoordinator.js +88 -4
  6. package/extension/src/content/activeTurnControl.js +1 -0
  7. package/extension/src/content/assetTransferBroker.js +2 -0
  8. package/extension/src/content/contentRuntime.js +77 -57
  9. package/extension/src/content/contextTray.js +4 -0
  10. package/extension/src/content/diagnosticsController.js +17 -3
  11. package/extension/src/content/diagnosticsPanel.js +129 -5
  12. package/extension/src/content/generated/content.bundle.js +1422 -348
  13. package/extension/src/content/generated/content.bundle.meta.json +7 -3
  14. package/extension/src/content/legacyGlobalRegistry.js +3 -0
  15. package/extension/src/content/modelPicker.js +21 -9
  16. package/extension/src/content/modelPickerSupport.js +38 -1
  17. package/extension/src/content/nativeCompatibilityController.js +4 -1
  18. package/extension/src/content/otWarmMirror.js +126 -92
  19. package/extension/src/content/otWarmMirrorController.js +122 -1
  20. package/extension/src/content/pageBridgeClient.js +2 -0
  21. package/extension/src/content/panelMaintenance.js +26 -15
  22. package/extension/src/content/providerSettingsCoordinator.js +1 -0
  23. package/extension/src/content/runFailureNotice.js +104 -0
  24. package/extension/src/content/runResultActions.js +16 -1
  25. package/extension/src/content/runScrollLayout.js +124 -0
  26. package/extension/src/content/runTimelineView.js +37 -11
  27. package/extension/src/content/sessionForkController.js +20 -11
  28. package/extension/src/content/settingsPanel.js +28 -11
  29. package/extension/src/content/trackedChangeCaptureController.js +153 -0
  30. package/extension/src/content/updateNotice.js +24 -4
  31. package/extension/src/content/writebackOrchestrator.js +31 -0
  32. package/extension/src/page/binaryAssetUploader.js +101 -29
  33. package/extension/src/page/overleafRealtimeObserver.js +120 -25
  34. package/extension/src/page/textFileCreator.js +493 -0
  35. package/extension/src/page/trackedChangeCapture.js +342 -0
  36. package/extension/src/page/trackedChangesLifecycle.js +145 -149
  37. package/extension/src/page/treeOperations.js +30 -2
  38. package/extension/src/page/writebackRouter.js +94 -48
  39. package/extension/src/pageBridge.js +62 -25
  40. package/extension/src/shared/compatibility.js +1 -1
  41. package/extension/src/shared/i18n.js +77 -37
  42. package/extension/src/shared/managedUpdateProjection.js +1 -1
  43. package/extension/src/shared/otText.js +0 -1
  44. package/extension/src/shared/pageRpcContract.js +2 -1
  45. package/extension/src/shared/sessionState.js +23 -23
  46. package/extension/src/shared/sessionTitle.js +120 -0
  47. package/extension/src/shared/storageDb.js +10 -12
  48. package/extension/src/shared/storageRunActions.js +29 -1
  49. package/extension/src/shared/updateRuntimeIdentity.js +32 -0
  50. package/extension/src/shared/writebackSettlement.js +19 -5
  51. package/extension/styles/panel.css +128 -24
  52. package/native-host/src/mirrorWorkspace.js +20 -1
  53. package/native-host/src/releaseProxy.js +177 -0
  54. package/native-host/src/releaseTransport.js +154 -0
  55. package/native-host/src/taskRunnerRuntime.js +25 -0
  56. package/native-host/src/updateManager.js +40 -39
  57. package/package.json +5 -1
@@ -392,54 +392,45 @@
392
392
  : result;
393
393
  }
394
394
 
395
- async function applyOperationsWithNoTraceUndo(operations, options = {}) {
396
- const initial = getReviewingState({});
397
- if (isEditingConfirmedForNoTraceUndo(initial)) {
398
- const result = await applyOperationsCore(operations, options);
399
- return {
400
- ...result,
401
- reviewingPolicy: {
402
- policy: 'no-trace-undo',
403
- disabled: false,
404
- restored: false,
405
- reason: 'editing_already_confirmed'
406
- }
407
- };
408
- }
409
-
410
- const disabled = await setReviewingEnabled(false, { waitMs: 1800 });
411
- if (!disabled.ok) {
412
- return buildNoTraceUndoBlockedResult(operations, disabled);
413
- }
395
+ async function confirmNoTraceUndoEditing() {
396
+ let changed = false;
397
+ let toggle = { ok: true, changed: false, enabled: false };
398
+ for (let attempt = 0; attempt < 2; attempt++) {
399
+ if (!isEditingConfirmedForNoTraceUndo(getReviewingState({}))) {
400
+ toggle = await setReviewingEnabled(false, { waitMs: 1800 });
401
+ if (!toggle.ok) return toggle;
402
+ changed = changed || toggle.changed === true;
403
+ }
404
+ await delay(180);
405
+ if (!isEditingConfirmedForNoTraceUndo(getReviewingState({}))) continue;
406
+ await delay(180);
407
+ if (isEditingConfirmedForNoTraceUndo(getReviewingState({})))
408
+ return { ...toggle, ok: true, changed, enabled: false };
409
+ }
410
+ return { ok: false, code: 'editing_not_confirmed',
411
+ reason: 'Overleaf Editing mode did not remain confirmed before undo; no undo write was started.' };
412
+ }
414
413
 
415
- let result;
416
- let applyError = null;
417
- try {
418
- result = await applyOperationsCore(operations, options);
419
- } catch (error) {
420
- applyError = error;
421
- }
422
- if (applyError) {
423
- throw applyError;
414
+ async function applyOperationsWithNoTraceUndo(operations, options = {}) {
415
+ const disabled = await confirmNoTraceUndoEditing();
416
+ if (!disabled.ok) return buildNoTraceUndoBlockedResult(operations, disabled);
417
+ const result = await applyOperationsCore(operations, {
418
+ ...options, noTraceUndo: true, trackReviewingChanges: false
419
+ });
420
+ const reviewingPolicy = { policy: 'no-trace-undo', disabled: disabled.changed === true, restored: false,
421
+ reason: disabled.changed ? 'left_editing_after_undo' : 'editing_already_confirmed' };
422
+ if (disabled.changed) {
423
+ reviewingPolicy.leftEditing = true;
424
+ reviewingPolicy.disable = summarizeReviewingToggleResult(disabled);
424
425
  }
425
-
426
- return {
427
- ...result,
428
- reviewingPolicy: {
429
- policy: 'no-trace-undo',
430
- disabled: true,
431
- restored: false,
432
- leftEditing: true,
433
- reason: 'left_editing_after_undo',
434
- disable: summarizeReviewingToggleResult(disabled)
435
- }
436
- };
426
+ return { ...result, reviewingPolicy };
437
427
  }
438
428
 
439
429
  async function applyOperationsCore(operations, options = {}) {
440
430
  const applied = [];
441
431
  const skipped = [];
442
432
  const trackedChanges = [];
433
+ const trackedChangeCaptures = [];
443
434
  const safeBaseFiles = normalizeBaseFilesForSafety(options.baseFiles);
444
435
  const baseFileLookup = window.CodexOverleafStaleGuard?.buildBaseFileLookup(safeBaseFiles);
445
436
  const baseBinaryFileLookup = buildBaseBinaryFileLookup(safeBaseFiles);
@@ -574,8 +565,10 @@
574
565
  let raceResult;
575
566
  if (operation.type === 'edit') {
576
567
  raceResult = await raceOpAgainstCancellation(applyEditOperation(operation, {
577
- baseFileLookup,
578
- trackReviewingChanges: options.trackReviewingChanges === true
568
+ baseFileLookup, runProjectId,
569
+ recheckWriteProject: writeGuardSurface ? () => writeGuardSurface.runWriteGuard({ runProjectId }) : null,
570
+ trackReviewingChanges: options.trackReviewingChanges === true,
571
+ noTraceUndo: options.noTraceUndo === true
579
572
  }));
580
573
  } else if (['binary-create', 'overwrite-binary'].includes(operation.type)) {
581
574
  raceResult = await raceOpAgainstCancellation(applyBinaryAssetOperation(operation, { baseFileLookup, baseBinaryFileLookup }));
@@ -604,6 +597,7 @@
604
597
  break;
605
598
  }
606
599
  const result = raceResult;
600
+ if (result.trackedChangeCapture) trackedChangeCaptures.push(result.trackedChangeCapture);
607
601
  if (operation.type === 'edit' && result.ok && Array.isArray(result.trackedChanges)) {
608
602
  trackedChanges.push(...result.trackedChanges);
609
603
  }
@@ -618,7 +612,8 @@
618
612
  ok: skipped.length === 0,
619
613
  applied,
620
614
  skipped,
621
- trackedChanges: mergeTrackedChangeRefs(trackedChanges)
615
+ trackedChanges: trackedChangeCaptures.some(capture => capture.state !== 'observed') ? [] : mergeTrackedChangeRefs(trackedChanges),
616
+ trackedChangeCaptures
622
617
  };
623
618
  }
624
619
 
@@ -653,12 +648,17 @@
653
648
  return editorReady;
654
649
  }
655
650
 
651
+ if (options.noTraceUndo === true) {
652
+ const editing = await confirmNoTraceUndoEditing();
653
+ if (!editing.ok) return buildNoTraceUndoBlockedResult([operation], editing).skipped[0].result;
654
+ }
656
655
  const trackReviewingChanges = options.trackReviewingChanges === true;
657
656
  const forbidTrackedChanges = options.forbidTrackedChanges === true;
658
657
  const observeTrackedChanges = trackReviewingChanges || forbidTrackedChanges;
659
658
  const operationPaths = collectOperationPaths([operation]);
659
+ const captureBaseline = trackReviewingChanges ? prepareTrackedChangeCapture(operation.path) : null;
660
660
  const trackedBefore = observeTrackedChanges
661
- ? collectTrackedChangeRefsForPaths(operationPaths)
661
+ ? (captureBaseline?.refs || collectTrackedChangeRefsForPaths(operationPaths))
662
662
  : [];
663
663
  let current = editorReady.text;
664
664
  let freshness = window.CodexOverleafStaleGuard?.checkOperationFreshness(
@@ -752,6 +752,25 @@
752
752
  };
753
753
  }
754
754
 
755
+ if (options.noTraceUndo === true) {
756
+ const blocked = typeof options.recheckWriteProject === 'function'
757
+ ? await options.recheckWriteProject()
758
+ : { ok: false, code: 'editor_project_id_unavailable',
759
+ failure: buildPageFailure('editor_project_id_unavailable', { file: operation.path,
760
+ operationType: 'undo', changedDocument: false,
761
+ userMessage: 'The project identity guard is unavailable; the undo write was not applied.' }) };
762
+ if (blocked) return blocked.skipped?.[0]?.result || blocked;
763
+ if (getActiveFilePath() !== operation.path || readActiveEditorText() !== current) {
764
+ const reason = 'The editor changed while confirming Editing mode; the undo write was not applied.';
765
+ return { ok: false, code: 'stale_source_changed', reason,
766
+ failure: buildPageFailure('stale_source_changed', { file: operation.path,
767
+ operationType: 'undo', changedDocument: false, userMessage: reason }) };
768
+ }
769
+ if (!isEditingConfirmedForNoTraceUndo(getReviewingState({})))
770
+ return buildNoTraceUndoBlockedResult([operation], { ok: false, code: 'editing_not_confirmed',
771
+ reason: 'Overleaf left Editing mode before the undo write.' }).skipped[0].result;
772
+ }
773
+
755
774
  const result = Array.isArray(operation.patches) && operation.patches.length
756
775
  ? replaceActiveEditorPatches(operation.patches, nextContent)
757
776
  : replaceActiveEditorText(nextContent);
@@ -784,6 +803,7 @@
784
803
  return decorated;
785
804
  }
786
805
  const trackedChanges = [];
806
+ let trackedChangeCapture = null;
787
807
  if (observeTrackedChanges) {
788
808
  // Overleaf renders review markers asynchronously after the editor text
789
809
  // has already changed. A single 120 ms snapshot intermittently missed
@@ -791,10 +811,13 @@
791
811
  // Poll both paths: normal Reviewing writes get a shorter capture window,
792
812
  // while Accept replay keeps the longer safety window used to prove that
793
813
  // it did not create fresh tracked changes.
794
- const trackedDiff = await waitForTrackedChangeDiff(trackedBefore, operationPaths, {
795
- waitMs: forbidTrackedChanges ? 3600 : 1800,
796
- intervalMs: 180
797
- });
814
+ const trackedDiff = trackReviewingChanges
815
+ ? await captureTrackedWrite({ trackedBefore, captureBaseline, operation, beforeContent: current,
816
+ postContent: nextContent, runProjectId: options.runProjectId })
817
+ : await waitForTrackedChangeDiff(trackedBefore, operationPaths, {
818
+ waitMs: 3600, intervalMs: 180, stopOnFirst: true
819
+ });
820
+ trackedChangeCapture = trackedDiff.capture || null;
798
821
  trackedChanges.push(...trackedDiff.trackedChanges);
799
822
  if (forbidTrackedChanges && trackedChanges.length > 0) {
800
823
  return {
@@ -831,6 +854,7 @@
831
854
  ...result,
832
855
  verified: true,
833
856
  verifiedContent: nextContent,
857
+ trackedChangeCapture,
834
858
  trackedChanges: mergeTrackedChangeRefs(trackedChanges)
835
859
  };
836
860
  }
@@ -1352,6 +1376,8 @@
1352
1376
  }
1353
1377
 
1354
1378
  async function applyFileTreeOperation(operation, options = {}) {
1379
+ const initialProjectId = treeOperations.getProjectId?.();
1380
+ const initialCancellationSequence = readWriteCancellationSequence();
1355
1381
  const freshness = await checkFileTreeOperationFreshness(operation, options.baseFileLookup);
1356
1382
  if (!freshness.ok) {
1357
1383
  return freshness;
@@ -1400,8 +1426,21 @@
1400
1426
  }
1401
1427
  }
1402
1428
 
1429
+ const nativeOperation = { create: deps.createTextFile, delete: deps.deleteTextFile }[operation.type];
1430
+ if (typeof nativeOperation === 'function' && (operation.type === 'delete' || typeof operation.content === 'string')) {
1431
+ const isCurrent = () => Boolean(initialProjectId) && treeOperations.getProjectId?.() === initialProjectId
1432
+ && readWriteCancellationSequence() === initialCancellationSequence;
1433
+ const created = await nativeOperation(operation, { isCurrent, expectedContent: options.baseFileLookup?.get(operation.path) });
1434
+ if (!created.ok) return created;
1435
+ if (!isCurrent() || created.verified !== true || created.verification !== 'overleaf-zip') {
1436
+ return { ...fileTreeVerificationFailed(operation, 'Server receipt is missing or stale.'), changedDocument: true };
1437
+ }
1438
+ recordFileTreeOperationSuccess(operation, options.baseFileLookup);
1439
+ return { ok: true, method: created.method, verified: true };
1440
+ }
1403
1441
  return {
1404
1442
  ok: false,
1443
+ code: 'file_tree_controls_unavailable',
1405
1444
  reason: 'No supported Overleaf file-tree method was detected'
1406
1445
  };
1407
1446
  }
@@ -1847,6 +1886,8 @@
1847
1886
  acceptTrackedChanges,
1848
1887
  rejectTrackedChanges,
1849
1888
  collectTrackedChangeRefsForPaths,
1889
+ prepareTrackedChangeCapture, getTrackedChangeCaptureStatus,
1890
+ captureTrackedWrite, reconcileTrackedChangeCapture,
1850
1891
  waitForTrackedChangeDiff
1851
1892
  } = trackedChangesLifecycleModule.create({
1852
1893
  window,
@@ -1868,6 +1909,9 @@
1868
1909
  buildPageFailure,
1869
1910
  delay,
1870
1911
  getActiveFilePath,
1912
+ getProjectId: () => treeOperations.getProjectId?.() || '',
1913
+ getCodeMirrorEditorView: deps.getCodeMirrorEditorView,
1914
+ getTrackedChangeDocumentId: deps.getTrackedChangeDocumentId,
1871
1915
  getReviewingState,
1872
1916
  isEditorUndoControl,
1873
1917
  isInsideCodexPanel,
@@ -1885,6 +1929,8 @@
1885
1929
 
1886
1930
  return {
1887
1931
  applyOperations: applyOperationsForBridge,
1932
+ getTrackedChangeCaptureStatus,
1933
+ reconcileTrackedChangeCapture,
1888
1934
  rejectTrackedChanges,
1889
1935
  acceptTrackedChanges,
1890
1936
  verifySaveState
@@ -67,7 +67,14 @@
67
67
  treeOperations,
68
68
  window
69
69
  });
70
+ const textFileCreator = requirePageModule('CodexOverleafTextFileCreator').create({
71
+ window, document, treeOperations, snapshotRouter,
72
+ collectElements, ensureEditing, ensureReviewing, readActiveEditorText, readWriteCancellationSequence,
73
+ uploadHelpers: requirePageModule('CodexOverleafBinaryAssetUploader')
74
+ });
70
75
  binaryAssetUploader = requirePageModule('CodexOverleafBinaryAssetUploader').create({
76
+ prepareUploadParent: textFileCreator.prepareUploadParent,
77
+ findFolderNode: textFileCreator.findFolderNode,
71
78
  document,
72
79
  snapshotRouter,
73
80
  treeOperations,
@@ -86,6 +93,8 @@
86
93
  window
87
94
  });
88
95
  writebackRouter = requirePageModule('CodexOverleafWritebackRouter').create({
96
+ createTextFile: textFileCreator.createFile,
97
+ deleteTextFile: textFileCreator.deleteFile,
89
98
  activeEditorIdentityChanged,
90
99
  clickNode,
91
100
  collectElements,
@@ -93,8 +102,14 @@
93
102
  compileBridge,
94
103
  delay,
95
104
  diagnosticsRevision: PAGE_BRIDGE_INSTALL_REVISION,
96
- ensureEditing,
97
- ensureReviewing,
105
+ getCodeMirrorEditorView,
106
+ getTrackedChangeDocumentId: filePath => {
107
+ const ids = new Set(collectDocRecords({ includeWindowGlobals: true })
108
+ .filter(record => record.path === filePath && typeof record.id === 'string' && record.id).map(record => record.id));
109
+ return ids.size === 1 ? Array.from(ids)[0] : '';
110
+ },
111
+ ensureEditing: params => textFileCreator.ensureWriteMode(false, params),
112
+ ensureReviewing: params => textFileCreator.ensureWriteMode(true, params),
98
113
  // Cross-world cancel: the router's per-op loop polls this each iteration
99
114
  // and aborts remaining ops when the value bumps. content-side cancel
100
115
  // invokes pageBridge.cancelActiveWrite which calls bumpWriteCancellationSequence.
@@ -205,20 +220,28 @@
205
220
  return { ok: true };
206
221
  },
207
222
  createCheckpoint: params => createCheckpoint(params.label),
208
- ensureReviewing,
209
- ensureEditing,
210
- applyOperations: params => applyOperations(params.operations || [], {
223
+ ensureReviewing: params => textFileCreator.ensureWriteMode(true, params),
224
+ ensureEditing: params => textFileCreator.ensureWriteMode(false, params),
225
+ applyOperations: async params => {
226
+ if (params.reviewingPolicy === 'no-trace-undo') {
227
+ const prepared = await textFileCreator.prepareEditor(params);
228
+ if (prepared.ok !== true) return { applied: [], changedDocument: false,
229
+ skipped: (params.operations || []).map(operation => ({ operation, result: prepared })) };
230
+ }
231
+ return applyOperations(params.operations || [], {
211
232
  baseFiles: params.baseFiles || null,
212
233
  reviewingPolicy: params.reviewingPolicy || '',
213
234
  requireReviewing: params.requireReviewing === true,
214
235
  requireEditing: params.requireEditing === true,
215
236
  runProjectId: typeof params.runProjectId === 'string' ? params.runProjectId : ''
216
- }),
237
+ });
238
+ },
217
239
  binaryUploadBegin: params => binaryAssetUploader.begin(params),
218
240
  binaryUploadAppend: params => binaryAssetUploader.append(params),
219
241
  binaryUploadCommit: params => binaryAssetUploader.commit(params),
220
242
  binaryUploadAbort: params => binaryAssetUploader.abort(params),
221
243
  jumpToPosition,
244
+ reconcileTrackedChangeCapture: params => writebackRouter.reconcileTrackedChangeCapture(params),
222
245
  rejectTrackedChanges,
223
246
  acceptTrackedChanges,
224
247
  triggerCompile: params => compileBridge.triggerCompile(params),
@@ -430,6 +453,7 @@
430
453
  editor,
431
454
  capabilities: pageRpcContract.withCapabilityReport(collectPageCapabilities(editor)),
432
455
  editorDiagnostics: buildEditorDiagnostics(editor),
456
+ trackedChangeCapture: writebackRouter.getTrackedChangeCaptureStatus?.(),
433
457
  projectDiagnostics: {
434
458
  internalRootKeys: collectInternalRootKeys(),
435
459
  docRecordCount: docRecords.length,
@@ -559,40 +583,26 @@
559
583
  reason: 'jumpToPosition requires a non-empty file path'
560
584
  };
561
585
  }
562
- if (!projectPathExists(filePath)) {
563
- return {
564
- ok: false,
565
- code: 'path_not_found',
566
- reason: `Could not find ${filePath} in the Overleaf project`,
567
- path: filePath,
568
- failure: buildPageBridgeFailure('target_file_not_found', {
569
- file: filePath,
570
- operationType: 'jump',
571
- changedDocument: false,
572
- userMessage: `Codex could not find ${filePath} in this Overleaf project.`,
573
- evidence: { originalCode: 'path_not_found', writeStarted: false }
574
- })
575
- };
576
- }
577
-
578
586
  const navigating = getActiveFilePath() !== filePath;
579
587
  const previousEditorIdentity = navigating ? getActiveEditorIdentity() : null;
580
588
 
581
589
  if (navigating) {
582
590
  const opened = await openFileByPath(filePath);
583
591
  if (!opened.ok) {
592
+ const missing = !projectPathExists(filePath);
593
+ const code = missing ? 'path_not_found' : 'file_open_failed';
584
594
  return {
585
595
  ok: false,
586
- code: 'file_open_failed',
596
+ code,
587
597
  reason: opened.reason || `Could not open ${filePath}`,
588
598
  path: filePath,
589
- failure: buildPageBridgeFailure('target_file_open_failed', {
599
+ failure: buildPageBridgeFailure(missing ? 'target_file_not_found' : 'target_file_open_failed', {
590
600
  file: filePath,
591
601
  operationType: 'jump',
592
602
  changedDocument: false,
593
603
  userMessage: `Codex could not open ${filePath} in Overleaf.`,
594
604
  technicalMessage: opened.reason || '',
595
- evidence: { originalCode: 'file_open_failed', writeStarted: false }
605
+ evidence: { originalCode: code, writeStarted: false }
596
606
  })
597
607
  };
598
608
  }
@@ -830,10 +840,35 @@
830
840
  return current.node !== previous.node;
831
841
  }
832
842
 
843
+ function readCurrentEditorMode() {
844
+ const selector = '.toolbar-editor .review-mode-switcher > .review-mode-switcher-toggle-button';
845
+ const controls = uniqueNodes(collectElements(selector, 16)).filter(node =>
846
+ String(node.className || '').split(/\s+/).includes('review-mode-switcher-toggle-button')
847
+ && node.ownerDocument === document && !isInsideCodexPanel(node));
848
+ if (!controls.length) return '';
849
+ const modes = controls.filter(node => {
850
+ const style = window.getComputedStyle?.(node);
851
+ return node.isConnected !== false && !node.hidden && !node.closest?.('[hidden],[aria-hidden="true"]')
852
+ && (node.getClientRects?.().length || 0) > 0 && style?.visibility !== 'hidden'
853
+ && style?.display !== 'none' && style?.opacity !== '0';
854
+ }).map(node => {
855
+ const classes = String(node.className || '').split(/\s+/);
856
+ const values = [node.getAttribute('aria-label'),
857
+ node.querySelector?.('.review-mode-switcher-toggle-label')?.textContent]
858
+ .map(value => normalizeReviewingSignalText(value).toLowerCase())
859
+ .filter(value => value === 'editing' || value === 'reviewing');
860
+ for (const mode of ['editing', 'reviewing']) if (classes.includes(mode)) values.push(mode);
861
+ return values.length && values.every(mode => mode === values[0]) ? values[0] : 'unknown';
862
+ });
863
+ return modes.length && modes.every(mode => mode === modes[0]) ? modes[0] : 'unknown';
864
+ }
865
+
833
866
  function getReviewingState(params = {}) {
834
867
  const signals = collectReviewingSignals({ ...params, manualOverride: false });
868
+ const editorMode = readCurrentEditorMode();
835
869
  return {
836
870
  signals,
871
+ ...(editorMode ? { editorMode } : {}),
837
872
  reviewing: window.CodexOverleafReviewing.detectReviewingFromSignals(signals)
838
873
  };
839
874
  }
@@ -986,6 +1021,7 @@
986
1021
  }
987
1022
 
988
1023
  function isReviewingConfirmedForWrite(state = {}) {
1024
+ if (state.editorMode) return state.editorMode === 'reviewing';
989
1025
  const reviewing = state.reviewing || {};
990
1026
  if (!reviewing.ok || reviewing.status === 'manual-override') {
991
1027
  return false;
@@ -1015,6 +1051,7 @@
1015
1051
  }
1016
1052
 
1017
1053
  function isEditingConfirmedForNoTraceUndo(state = {}) {
1054
+ if (state.editorMode) return state.editorMode === 'editing';
1018
1055
  if (isReviewingConfirmedForWrite(state)) {
1019
1056
  return false;
1020
1057
  }
@@ -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.3.5';
15
+ const BUILD_TARGET_VERSION = '2.4.0';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -40,20 +40,31 @@
40
40
  diagnosticsPageSubtitle: 'Current file, write access, track changes',
41
41
  diagnosticsSnapshotTitle: 'Check Project Read',
42
42
  diagnosticsSnapshotSubtitle: 'Full project, assets, read source',
43
- diagnosticsOtTitle: 'Check Experimental OT Mirror',
43
+ diagnosticsOtTitle: 'Check OT Mirror',
44
44
  diagnosticsOtSubtitle: 'Status, fresh files, fallback',
45
45
  diagnosticsExportTitle: 'Export Diagnostics',
46
46
  diagnosticsExportSubtitle: 'Redacted audit and environment bundle',
47
47
  diagnosticsExportDone: 'Diagnostics bundle exported.',
48
- diagnosticsOtSummaryEnabled: 'Experimental OT is enabled. Codex will use the OT-warmed mirror only when matching files are fresh; otherwise it falls back to the normal project read path.',
49
- diagnosticsOtSummaryDisabled: 'Experimental OT is disabled, so Codex is using the normal project read path.',
50
- diagnosticsOtNextStep: 'If you expected OT warm reads, turn on OT and wait for the status to show Live before relying on it.',
51
- experimentalOtMenuTitle: 'Experimental OT Mirror',
52
- experimentalOtMenuSubtitle: 'Experimental — speeds up reading the file you are editing; Codex falls back to normal reading whenever unsure.',
53
- experimentalOtConfirmTitle: 'Experimental OT Mirror',
54
- experimentalOtConfirmMessage: 'Experimental: tracks the current Overleaf editor and warms the local mirror so Codex can read recent focused-file edits faster. Codex uses it only when file scope is clear and the mirror is fresh; otherwise it falls back to normal project reading.',
48
+ diagnosticsOtSummaryEnabled: 'OT Mirror is enabled. Codex will use the OT-warmed mirror only when matching files are fresh; otherwise it falls back to the normal project read path.',
49
+ diagnosticsOtSummaryDisabled: 'OT Mirror is disabled, so Codex is using the normal project read path.',
50
+ diagnosticsOtNextStep: 'Observer status describes warming availability. File scope and freshness are checked again before each run.',
51
+ experimentalOtMenuTitle: 'OT Mirror',
52
+ otSettingsTitle: 'Project cache',
53
+ contextLoadingTitle: 'Context loading',
54
+ runFeedbackFailedTitle: 'Needs attention',
55
+ runFeedbackPartialTitle: 'Partially completed',
56
+ runFeedbackUnconfirmedTitle: 'Write status needs review',
57
+ runFeedbackGeneric: 'Open the run details for the reported cause and the next step.',
58
+ runFeedbackUnconfirmed: 'Some changes may already be in Overleaf. Check the document before retrying.',
59
+ runFeedbackUnconfirmedResult: 'Not fully confirmed. Review Overleaf before retrying.',
60
+ runFeedbackReviewFile: 'Review {file} in Overleaf',
61
+ runFeedbackReviewDetails: 'Review run details',
62
+ runFeedbackDetailsUnavailable: 'Detailed steps are unavailable for this older run.',
63
+ experimentalOtMenuSubtitle: 'Warms recent edits to the active text file locally. File scope and freshness are checked before reuse.',
64
+ experimentalOtConfirmTitle: 'Enable OT Mirror?',
65
+ experimentalOtConfirmMessage: 'Observes the active text editor and warms its local mirror. This is read-only with respect to Overleaf and does not upload assets. Each run checks file scope and freshness before reuse; normal project reading remains the fallback.',
55
66
  experimentalOtConfirmEnable: 'Turn on',
56
- experimentalOtEnabledToast: 'Experimental OT Mirror is on. Use @file for the cleanest warm-mirror test.',
67
+ experimentalOtEnabledToast: 'OT Mirror requested for this project. Its status shows whether warming is available.',
57
68
  diagnosticsSnapshotLoading: 'Checking whether the extension can read the full Overleaf project.',
58
69
  diagnosticsSnapshotErrorSummary: 'Overleaf may still be loading, or the page API did not return project files yet.',
59
70
  diagnosticsNativeLoading: 'Checking whether local Codex, Native Host, and LaTeX tools are available.',
@@ -326,18 +337,27 @@
326
337
  requireReviewingTitle: 'When enabled, Codex checks or switches Overleaf Reviewing/Track Changes before writing. Deletes still require confirmation.',
327
338
  autoCompile: 'Compile',
328
339
  autoCompileTitle: 'After Codex writes, click Overleaf Recompile and record the compile result for this task. Ask mode will not trigger it.',
329
- experimentalOtWarmMirror: 'Experimental OT warm mirror',
340
+ experimentalOtWarmMirror: 'OT warm mirror',
330
341
  experimentalOtWarmMirrorShort: 'OT',
331
- experimentalOtWarmMirrorTitle: 'Experimental read-only OT mirror warming. If unavailable, stale, or inconsistent, Codex falls back to the normal project read path.',
342
+ experimentalOtWarmMirrorTitle: 'Read-only local text warming. File scope and freshness are checked before reuse; normal project reading remains the fallback.',
332
343
  otStatus: 'OT status',
333
- otFreshFiles: 'OT fresh files',
334
- otFallback: 'Fallback',
344
+ otFreshFiles: 'Files with recent OT evidence',
345
+ otFallback: 'Read fallback',
335
346
  otStatusOff: 'Off',
336
- otStatusStarting: 'Start',
337
- otStatusObserving: 'Live',
338
- otStatusUnavailable: 'N/A',
339
- otStatusStale: 'Stale',
340
- otStatusInconsistent: 'Check',
347
+ otStatusStarting: 'Connecting',
348
+ otStatusObserving: 'Observing current editor',
349
+ otStatusUnavailable: 'Unavailable',
350
+ otStatusStale: 'Warming paused; awaiting verification',
351
+ otStatusInconsistent: 'Needs verification',
352
+ otFailureBridge: 'The page connection needs to be restored. Reload the extension and Overleaf before retrying.',
353
+ otFailureObserver: 'The editor observer is unavailable. The next run will try normal project reading.',
354
+ otFailureEditor: 'The active editor could not be read. Warming is paused; the next run will try normal project reading.',
355
+ otFailureFileLimit: 'This file exceeds the 1 MiB warming limit. The next run will try normal project reading.',
356
+ otFailureQueueLimit: 'The mirror queue reached its limit. Warming is paused; the next run will try normal project reading.',
357
+ otFailureExpired: 'The observation is too old to reuse. The next run will try normal project reading.',
358
+ otFailureMirror: 'The mirror update was not fully confirmed. The next run will try normal project reading.',
359
+ otFailureGeneric: 'Mirror warming could not continue. The next run will try normal project reading.',
360
+ otFailureNextStep: 'Resolve the reported cause, then enable OT again. If the page connection is unavailable, reload the extension and Overleaf first.',
341
361
  modelSourceFallback: 'fallback',
342
362
  modelSourceDiscovered: 'discovered',
343
363
  modelSourceFailed: 'failed',
@@ -560,20 +580,31 @@
560
580
  diagnosticsPageSubtitle: '当前文件、写入能力、留痕状态',
561
581
  diagnosticsSnapshotTitle: '检查项目读取',
562
582
  diagnosticsSnapshotSubtitle: '完整项目、资源文件、读取来源',
563
- diagnosticsOtTitle: '检查实验性 OT Mirror',
583
+ diagnosticsOtTitle: '检查 OT Mirror',
564
584
  diagnosticsOtSubtitle: '状态、新鲜文件、回退路径',
565
585
  diagnosticsExportTitle: '导出诊断信息',
566
586
  diagnosticsExportSubtitle: '已脱敏的审计和环境信息包',
567
587
  diagnosticsExportDone: '诊断信息已导出。',
568
- diagnosticsOtSummaryEnabled: '实验性 OT 已开启。只有匹配文件是新鲜状态时,Codex 才会使用 OT 预热 mirror;否则会回退到常规项目读取路径。',
569
- diagnosticsOtSummaryDisabled: '实验性 OT 已关闭,Codex 会使用常规项目读取路径。',
570
- diagnosticsOtNextStep: '如果你希望使用 OT 预热读取,请开启 OT,并等状态显示为“实时”后再依赖它。',
571
- experimentalOtMenuTitle: '实验性 OT Mirror',
572
- experimentalOtMenuSubtitle: '实验性——加快读取你正在编辑的文件;拿不准时自动回退到普通读取。',
573
- experimentalOtConfirmTitle: '实验性 OT Mirror',
574
- experimentalOtConfirmMessage: '实验性:实时跟踪当前 Overleaf 编辑器,把改动预热到本地 mirror,让 Codex 更快读到最近的当前文件改动。只有文件范围明确且 mirror 新鲜时才会使用;否则自动回退到普通项目读取。',
588
+ diagnosticsOtSummaryEnabled: 'OT Mirror 已开启。只有匹配文件是新鲜状态时,Codex 才会使用 OT 预热镜像;否则会回退到常规项目读取路径。',
589
+ diagnosticsOtSummaryDisabled: 'OT Mirror 已关闭,Codex 会使用常规项目读取路径。',
590
+ diagnosticsOtNextStep: '观测状态说明预热是否可用;每次任务开始前仍会重新校验文件范围和新鲜度。',
591
+ experimentalOtMenuTitle: 'OT Mirror',
592
+ otSettingsTitle: '项目缓存',
593
+ contextLoadingTitle: '上下文加载',
594
+ runFeedbackFailedTitle: '需要处理',
595
+ runFeedbackPartialTitle: '部分完成',
596
+ runFeedbackUnconfirmedTitle: '写入状态待确认',
597
+ runFeedbackGeneric: '可在本轮详情中查看具体原因和后续处理方式。',
598
+ runFeedbackUnconfirmed: '部分改动可能已经写入 Overleaf,重试前请先检查文档。',
599
+ runFeedbackUnconfirmedResult: '结果尚未完全确认,请检查 Overleaf 后再决定是否重试。',
600
+ runFeedbackReviewFile: '在 Overleaf 检查 {file}',
601
+ runFeedbackReviewDetails: '查看运行步骤',
602
+ runFeedbackDetailsUnavailable: '这条历史记录没有可展开的详细步骤。',
603
+ experimentalOtMenuSubtitle: '在本地预热当前文本文件的最近改动,复用前重新校验文件范围和新鲜度。',
604
+ experimentalOtConfirmTitle: '开启 OT Mirror?',
605
+ experimentalOtConfirmMessage: '观测当前文本编辑器并预热本地镜像。此功能不会写回 Overleaf,也不负责上传资产。每次任务复用前都会校验文件范围和新鲜度,不满足条件时尝试常规项目读取。',
575
606
  experimentalOtConfirmEnable: '开启',
576
- experimentalOtEnabledToast: '实验性 OT Mirror 已开启。使用 @file 可以最清楚地测试 warm mirror。',
607
+ experimentalOtEnabledToast: '已为当前项目请求开启 OT Mirror,可用情况见状态提示。',
577
608
  diagnosticsSnapshotLoading: '确认插件能否读取完整 Overleaf 项目。',
578
609
  diagnosticsSnapshotErrorSummary: '这通常是 Overleaf 页面还没加载完成,或者当前页面接口暂时没有返回项目文件。',
579
610
  diagnosticsNativeLoading: '确认本机 Codex、Native Host 和 LaTeX 工具是否可用。',
@@ -846,18 +877,27 @@
846
877
  requireReviewingTitle: '开启后,写入前会确认并尝试切到 Overleaf Reviewing/Track Changes;删除仍需确认。',
847
878
  autoCompile: '编译',
848
879
  autoCompileTitle: 'Codex 写入后自动点击 Overleaf Recompile,并把编译结果记录到本轮任务。只问不改时不会触发。',
849
- experimentalOtWarmMirror: '实验性 OT 预热镜像',
880
+ experimentalOtWarmMirror: 'OT 预热镜像',
850
881
  experimentalOtWarmMirrorShort: 'OT',
851
- experimentalOtWarmMirrorTitle: '实验性只读 OT 镜像预热;不可用、过期或不一致时会回退到常规项目读取。',
882
+ experimentalOtWarmMirrorTitle: '只读本地文本预热;复用前校验文件范围和新鲜度,常规项目读取作为回退路径。',
852
883
  otStatus: 'OT 状态',
853
- otFreshFiles: 'OT 新鲜文件',
854
- otFallback: '回退',
855
- otStatusOff: '关',
856
- otStatusStarting: '启动',
857
- otStatusObserving: '实时',
858
- otStatusUnavailable: '不可用',
859
- otStatusStale: '过期',
860
- otStatusInconsistent: '需检查',
884
+ otFreshFiles: '近期有 OT 校验证据的文件',
885
+ otFallback: '读取回退',
886
+ otStatusOff: '未开启',
887
+ otStatusStarting: '正在连接',
888
+ otStatusObserving: '正在观测当前编辑器',
889
+ otStatusUnavailable: '暂不可用',
890
+ otStatusStale: '预热暂缓,等待重新校验',
891
+ otStatusInconsistent: '需重新校验',
892
+ otFailureBridge: '页面连接需要恢复,请先重新加载扩展并刷新 Overleaf,再重试。',
893
+ otFailureObserver: '编辑器观察器暂不可用,下次任务将尝试常规项目读取。',
894
+ otFailureEditor: '当前编辑器内容尚不可读,预热已暂停;下次任务将尝试常规项目读取。',
895
+ otFailureFileLimit: '文件超过 1 MiB 预热上限,下次任务将尝试常规项目读取。',
896
+ otFailureQueueLimit: '镜像队列达到容量上限,预热已暂停;下次任务将尝试常规项目读取。',
897
+ otFailureExpired: '观测记录已过期,下次任务将尝试常规项目读取。',
898
+ otFailureMirror: '镜像更新尚未完整确认,下次任务将尝试常规项目读取。',
899
+ otFailureGeneric: '镜像预热未能继续,下次任务将尝试常规项目读取。',
900
+ otFailureNextStep: '处理所显示的原因后重新开启 OT;若页面连接不可用,请先重新加载扩展并刷新 Overleaf。',
861
901
  modelSourceFallback: '备用列表',
862
902
  modelSourceDiscovered: '已发现',
863
903
  modelSourceFailed: '获取失败',
@@ -26,7 +26,7 @@
26
26
  ]);
27
27
 
28
28
  const PHASE_TIMEOUTS = Object.freeze({
29
- checking: 30 * 1000,
29
+ checking: 60 * 1000,
30
30
  downloading: 2 * 60 * 1000,
31
31
  applying: 90 * 1000,
32
32
  awaiting_health: 2 * 60 * 1000,
@@ -279,7 +279,6 @@
279
279
 
280
280
  function normalizePath(value) {
281
281
  return String(value || '')
282
- .replace(/\s+/g, ' ')
283
282
  .replace(/\\/g, '/')
284
283
  .trim()
285
284
  .replace(/^\/+/, '');
@@ -5,7 +5,7 @@
5
5
  })(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this), function () {
6
6
  'use strict';
7
7
 
8
- var REVISION = '2026-08-04-page-rpc-contract-v15';
8
+ var REVISION = '2026-09-14-native-text-upload-v28';
9
9
  var METHODS = freezeCatalog({
10
10
  initializeCapability: entry('bridge.initialize', 'control', 'none', 'default', false, 'none'),
11
11
  probe: entry('bridge.probe', 'read', 'none', 'default', false, 'safe'),
@@ -22,6 +22,7 @@
22
22
  binaryUploadCommit: entry('asset.write', 'document', 'required', 'writeback', true, 'no_retry'),
23
23
  binaryUploadAbort: entry('asset.stage', 'cache', 'none', 'default', false, 'idempotent'),
24
24
  jumpToPosition: entry('editor.navigate', 'navigation', 'required', 'default', false, 'idempotent'),
25
+ reconcileTrackedChangeCapture: entry('tracked_changes.read', 'read', 'required', 'default', false, 'safe'),
25
26
  rejectTrackedChanges: entry('tracked_changes.reject', 'document', 'required', 'lifecycle', true, 'no_retry'),
26
27
  acceptTrackedChanges: entry('tracked_changes.accept', 'document', 'required', 'lifecycle', true, 'no_retry'),
27
28
  triggerCompile: entry('compile.trigger', 'page_state', 'required', 'compile', false, 'no_retry'),