codex-overleaf-link 2.3.6 → 2.4.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 (52) hide show
  1. package/README.md +62 -34
  2. package/README.zh-CN.md +546 -0
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +3 -0
  5. package/extension/src/content/activeTurnControl.js +1 -0
  6. package/extension/src/content/assetTransferBroker.js +2 -0
  7. package/extension/src/content/contentRuntime.js +122 -118
  8. package/extension/src/content/contextTray.js +4 -0
  9. package/extension/src/content/diagnosticsController.js +4 -0
  10. package/extension/src/content/diagnosticsPanel.js +128 -4
  11. package/extension/src/content/generated/content.bundle.js +1233 -550
  12. package/extension/src/content/generated/content.bundle.meta.json +4 -3
  13. package/extension/src/content/legacyGlobalRegistry.js +1 -0
  14. package/extension/src/content/modelPicker.js +21 -9
  15. package/extension/src/content/modelPickerSupport.js +38 -1
  16. package/extension/src/content/nativeCompatibilityController.js +4 -1
  17. package/extension/src/content/otWarmMirror.js +34 -30
  18. package/extension/src/content/otWarmMirrorController.js +20 -0
  19. package/extension/src/content/pageBridgeClient.js +2 -0
  20. package/extension/src/content/panelMaintenance.js +8 -4
  21. package/extension/src/content/recentProjects.js +354 -355
  22. package/extension/src/content/runResultActions.js +16 -1
  23. package/extension/src/content/runTimelineView.js +9 -11
  24. package/extension/src/content/scopedPersistenceCoordinator.js +2 -1
  25. package/extension/src/content/scopedPersistencePanelState.js +45 -2
  26. package/extension/src/content/sessionForkController.js +20 -11
  27. package/extension/src/content/sessionPersistence.js +4 -1
  28. package/extension/src/content/settingsPanel.js +16 -2
  29. package/extension/src/content/trackedChangeCaptureController.js +153 -0
  30. package/extension/src/content/writebackOrchestrator.js +31 -0
  31. package/extension/src/page/binaryAssetUploader.js +101 -29
  32. package/extension/src/page/overleafRealtimeObserver.js +4 -1
  33. package/extension/src/page/saveState.js +44 -1
  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 +149 -150
  37. package/extension/src/page/treeOperations.js +40 -29
  38. package/extension/src/page/writebackRouter.js +95 -49
  39. package/extension/src/pageBridge.js +65 -25
  40. package/extension/src/shared/compatibility.js +1 -1
  41. package/extension/src/shared/i18n.js +94 -40
  42. package/extension/src/shared/pageRpcContract.js +2 -1
  43. package/extension/src/shared/sessionState.js +6 -2
  44. package/extension/src/shared/storageDb.js +11 -12
  45. package/extension/src/shared/storageRunActions.js +73 -1
  46. package/extension/src/shared/writebackSettlement.js +19 -5
  47. package/extension/styles/panel.css +254 -405
  48. package/native-host/src/codexSessionRunner.js +41 -38
  49. package/native-host/src/codexSessionTiming.js +68 -0
  50. package/native-host/src/readProgressGuard.js +84 -17
  51. package/native-host/src/taskRunnerRuntime.js +25 -0
  52. 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,9 +1886,11 @@
1847
1886
  acceptTrackedChanges,
1848
1887
  rejectTrackedChanges,
1849
1888
  collectTrackedChangeRefsForPaths,
1889
+ prepareTrackedChangeCapture, getTrackedChangeCaptureStatus,
1890
+ captureTrackedWrite, reconcileTrackedChangeCapture,
1850
1891
  waitForTrackedChangeDiff
1851
1892
  } = trackedChangesLifecycleModule.create({
1852
- window,
1893
+ window, confirmReviewWriteback: deps.confirmReviewWriteback,
1853
1894
  checkWritebackRunProjectId,
1854
1895
  buildNoTraceUndoBlockedResult,
1855
1896
  buildReviewingRequiredBlockedResult,
@@ -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,11 @@
86
93
  window
87
94
  });
88
95
  writebackRouter = requirePageModule('CodexOverleafWritebackRouter').create({
96
+ confirmReviewWriteback: (params, result) => requirePageModule('CodexOverleafSaveState').confirmReviewWriteback({
97
+ params, result, getProjectId, delay, readSnapshot: options => snapshotRouter.fetchProjectZipSnapshot(options)
98
+ }),
99
+ createTextFile: textFileCreator.createFile,
100
+ deleteTextFile: textFileCreator.deleteFile,
89
101
  activeEditorIdentityChanged,
90
102
  clickNode,
91
103
  collectElements,
@@ -93,8 +105,14 @@
93
105
  compileBridge,
94
106
  delay,
95
107
  diagnosticsRevision: PAGE_BRIDGE_INSTALL_REVISION,
96
- ensureEditing,
97
- ensureReviewing,
108
+ getCodeMirrorEditorView,
109
+ getTrackedChangeDocumentId: filePath => {
110
+ const ids = new Set(collectDocRecords({ includeWindowGlobals: true })
111
+ .filter(record => record.path === filePath && typeof record.id === 'string' && record.id).map(record => record.id));
112
+ return ids.size === 1 ? Array.from(ids)[0] : '';
113
+ },
114
+ ensureEditing: params => textFileCreator.ensureWriteMode(false, params),
115
+ ensureReviewing: params => textFileCreator.ensureWriteMode(true, params),
98
116
  // Cross-world cancel: the router's per-op loop polls this each iteration
99
117
  // and aborts remaining ops when the value bumps. content-side cancel
100
118
  // invokes pageBridge.cancelActiveWrite which calls bumpWriteCancellationSequence.
@@ -205,20 +223,28 @@
205
223
  return { ok: true };
206
224
  },
207
225
  createCheckpoint: params => createCheckpoint(params.label),
208
- ensureReviewing,
209
- ensureEditing,
210
- applyOperations: params => applyOperations(params.operations || [], {
226
+ ensureReviewing: params => textFileCreator.ensureWriteMode(true, params),
227
+ ensureEditing: params => textFileCreator.ensureWriteMode(false, params),
228
+ applyOperations: async params => {
229
+ if (params.reviewingPolicy === 'no-trace-undo') {
230
+ const prepared = await textFileCreator.prepareEditor(params);
231
+ if (prepared.ok !== true) return { applied: [], changedDocument: false,
232
+ skipped: (params.operations || []).map(operation => ({ operation, result: prepared })) };
233
+ }
234
+ return applyOperations(params.operations || [], {
211
235
  baseFiles: params.baseFiles || null,
212
236
  reviewingPolicy: params.reviewingPolicy || '',
213
237
  requireReviewing: params.requireReviewing === true,
214
238
  requireEditing: params.requireEditing === true,
215
239
  runProjectId: typeof params.runProjectId === 'string' ? params.runProjectId : ''
216
- }),
240
+ });
241
+ },
217
242
  binaryUploadBegin: params => binaryAssetUploader.begin(params),
218
243
  binaryUploadAppend: params => binaryAssetUploader.append(params),
219
244
  binaryUploadCommit: params => binaryAssetUploader.commit(params),
220
245
  binaryUploadAbort: params => binaryAssetUploader.abort(params),
221
246
  jumpToPosition,
247
+ reconcileTrackedChangeCapture: params => writebackRouter.reconcileTrackedChangeCapture(params),
222
248
  rejectTrackedChanges,
223
249
  acceptTrackedChanges,
224
250
  triggerCompile: params => compileBridge.triggerCompile(params),
@@ -430,6 +456,7 @@
430
456
  editor,
431
457
  capabilities: pageRpcContract.withCapabilityReport(collectPageCapabilities(editor)),
432
458
  editorDiagnostics: buildEditorDiagnostics(editor),
459
+ trackedChangeCapture: writebackRouter.getTrackedChangeCaptureStatus?.(),
433
460
  projectDiagnostics: {
434
461
  internalRootKeys: collectInternalRootKeys(),
435
462
  docRecordCount: docRecords.length,
@@ -559,40 +586,26 @@
559
586
  reason: 'jumpToPosition requires a non-empty file path'
560
587
  };
561
588
  }
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
589
  const navigating = getActiveFilePath() !== filePath;
579
590
  const previousEditorIdentity = navigating ? getActiveEditorIdentity() : null;
580
591
 
581
592
  if (navigating) {
582
593
  const opened = await openFileByPath(filePath);
583
594
  if (!opened.ok) {
595
+ const missing = !projectPathExists(filePath);
596
+ const code = missing ? 'path_not_found' : 'file_open_failed';
584
597
  return {
585
598
  ok: false,
586
- code: 'file_open_failed',
599
+ code,
587
600
  reason: opened.reason || `Could not open ${filePath}`,
588
601
  path: filePath,
589
- failure: buildPageBridgeFailure('target_file_open_failed', {
602
+ failure: buildPageBridgeFailure(missing ? 'target_file_not_found' : 'target_file_open_failed', {
590
603
  file: filePath,
591
604
  operationType: 'jump',
592
605
  changedDocument: false,
593
606
  userMessage: `Codex could not open ${filePath} in Overleaf.`,
594
607
  technicalMessage: opened.reason || '',
595
- evidence: { originalCode: 'file_open_failed', writeStarted: false }
608
+ evidence: { originalCode: code, writeStarted: false }
596
609
  })
597
610
  };
598
611
  }
@@ -830,10 +843,35 @@
830
843
  return current.node !== previous.node;
831
844
  }
832
845
 
846
+ function readCurrentEditorMode() {
847
+ const selector = '.toolbar-editor .review-mode-switcher > .review-mode-switcher-toggle-button';
848
+ const controls = uniqueNodes(collectElements(selector, 16)).filter(node =>
849
+ String(node.className || '').split(/\s+/).includes('review-mode-switcher-toggle-button')
850
+ && node.ownerDocument === document && !isInsideCodexPanel(node));
851
+ if (!controls.length) return '';
852
+ const modes = controls.filter(node => {
853
+ const style = window.getComputedStyle?.(node);
854
+ return node.isConnected !== false && !node.hidden && !node.closest?.('[hidden],[aria-hidden="true"]')
855
+ && (node.getClientRects?.().length || 0) > 0 && style?.visibility !== 'hidden'
856
+ && style?.display !== 'none' && style?.opacity !== '0';
857
+ }).map(node => {
858
+ const classes = String(node.className || '').split(/\s+/);
859
+ const values = [node.getAttribute('aria-label'),
860
+ node.querySelector?.('.review-mode-switcher-toggle-label')?.textContent]
861
+ .map(value => normalizeReviewingSignalText(value).toLowerCase())
862
+ .filter(value => value === 'editing' || value === 'reviewing');
863
+ for (const mode of ['editing', 'reviewing']) if (classes.includes(mode)) values.push(mode);
864
+ return values.length && values.every(mode => mode === values[0]) ? values[0] : 'unknown';
865
+ });
866
+ return modes.length && modes.every(mode => mode === modes[0]) ? modes[0] : 'unknown';
867
+ }
868
+
833
869
  function getReviewingState(params = {}) {
834
870
  const signals = collectReviewingSignals({ ...params, manualOverride: false });
871
+ const editorMode = readCurrentEditorMode();
835
872
  return {
836
873
  signals,
874
+ ...(editorMode ? { editorMode } : {}),
837
875
  reviewing: window.CodexOverleafReviewing.detectReviewingFromSignals(signals)
838
876
  };
839
877
  }
@@ -986,6 +1024,7 @@
986
1024
  }
987
1025
 
988
1026
  function isReviewingConfirmedForWrite(state = {}) {
1027
+ if (state.editorMode) return state.editorMode === 'reviewing';
989
1028
  const reviewing = state.reviewing || {};
990
1029
  if (!reviewing.ok || reviewing.status === 'manual-override') {
991
1030
  return false;
@@ -1015,6 +1054,7 @@
1015
1054
  }
1016
1055
 
1017
1056
  function isEditingConfirmedForNoTraceUndo(state = {}) {
1057
+ if (state.editorMode) return state.editorMode === 'editing';
1018
1058
  if (isReviewingConfirmedForWrite(state)) {
1019
1059
  return false;
1020
1060
  }
@@ -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.6';
15
+ const BUILD_TARGET_VERSION = '2.4.1';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -508,14 +508,39 @@
508
508
  failureReason_editor_project_id_unavailable_next: 'Refresh the Overleaf tab and retry; if it persists, reload the extension.',
509
509
  // Welcome-panel + write-guard: Recent-projects
510
510
  // variant copy. Spec §5.3–§5.5, §5.8, §5.10 are the source of truth.
511
- recentProjects_welcome: 'Codex Overleaf Link',
512
- recentProjects_welcome_subtitle: 'Open a project from the Overleaf list on the left to start.',
513
- recentProjects_empty: "You haven't run any Codex sessions yet. Open a project from the Overleaf list on the left to start.",
514
- recentProjects_sessions_empty: 'No sessions in this project yet',
515
- recentProjects_sessions_toggle: "Show / manage this project's sessions",
516
- recentProjects_sessions_action: 'View runs',
517
- recentProjects_sessions_action_open: 'Hide runs',
518
- recentProjects_clearProject: 'Delete all sessions for this project',
511
+ recentProjects_welcome: 'Continue working',
512
+ recentProjects_focusEyebrow: 'Last conversation',
513
+ recentProjects_historyHeading: 'Other recent conversations',
514
+ recentProjects_historyHint: 'Select to continue',
515
+ recentProjects_projectConversations: "View this project's conversations",
516
+ recentProjects_allProjects: 'All projects',
517
+ recentProjects_showAllConversations: 'View all {count} conversations',
518
+ recentProjects_unstarted: 'Not started \u00b7 {count}',
519
+ recentProjects_unstartedHint: 'Empty conversations are kept here, without deleting their records.',
520
+ recentProjects_noStarted: 'No conversation has been started yet. Empty conversations are available below.',
521
+ recentProjects_savedDraft: 'Saved draft, ready to continue.',
522
+ recentProjects_attention: 'Needs attention',
523
+ recentProjects_unavailableRecords: 'Records with unavailable project links',
524
+ recentProjects_loadingHistory: 'Loading conversations...',
525
+ recentProjects_loadFailed: 'Conversation history could not be read. Please retry.',
526
+ recentProjects_continueHint: 'Open this conversation without automatically starting a task.',
527
+ recentProjects_more: 'More options',
528
+ recentProjects_continue: 'Continue',
529
+ recentProjects_openProject: 'Open project',
530
+ recentProjects_loadingConversation: 'Loading conversation...',
531
+ recentProjects_noConversation: 'No saved conversation',
532
+ recentProjects_noRuns: 'No runs yet',
533
+ recentProjects_previewUnavailable: 'Conversation unavailable',
534
+ recentProjects_previewUnavailableHint: 'Saved conversation could not be loaded.',
535
+ recentProjects_conversationGone: 'This conversation is no longer available. Choose another conversation from the refreshed list.',
536
+ recentProjects_continueFailed: 'Could not open the conversation. Please try again.',
537
+ recentProjects_welcome_subtitle: 'Conversations saved in this browser',
538
+ recentProjects_empty: 'No conversations saved in this browser yet. Open an Overleaf project and start a Codex conversation.',
539
+ recentProjects_sessions_empty: 'No saved conversations in this project',
540
+ recentProjects_sessions_toggle: "Show or manage this project's conversations",
541
+ recentProjects_sessions_action: 'All conversations',
542
+ recentProjects_sessions_action_open: 'Hide conversations',
543
+ recentProjects_clearProject: 'Delete this project\'s conversations',
519
544
  recentProjects_clearProject_title: 'Delete all Codex sessions for {project}?',
520
545
  recentProjects_clearProject_message: 'This permanently deletes all {count} plugin sessions, run history, and matching plugin-local Codex thread history for {project}. It will not modify Overleaf files or global Codex history.',
521
546
  recentProjects_clearProject_confirm: 'Delete all',
@@ -531,15 +556,17 @@
531
556
  recentProjects_degraded: 'Sign in to Overleaf to see your recent Codex projects.',
532
557
  recentProjects_row_projectLinkUnavailable: 'Project link unavailable.',
533
558
  recentProjects_settings_entry: 'Settings & Diagnostics',
534
- recentProjects_badge_pending: 'pending',
535
- recentProjects_badge_accepted: 'accepted',
536
- recentProjects_badge_rejected: 'rejected',
537
- recentProjects_badge_needs_review: 'pending',
538
- recentProjects_badge_running: 'running',
539
- recentProjects_badge_completed: 'completed',
540
- recentProjects_badge_failed: 'failed',
541
- recentProjects_badge_interrupted: 'interrupted',
542
- recentProjects_badge_stale: 'stale',
559
+ recentProjects_settingsTitle: 'General Settings',
560
+ recentProjects_settingsSubtitle: 'Manage Codex preferences and local history across all projects.',
561
+ recentProjects_badge_pending: 'Last run pending',
562
+ recentProjects_badge_accepted: 'Last changes accepted',
563
+ recentProjects_badge_rejected: 'Last changes undone',
564
+ recentProjects_badge_needs_review: 'Last run needs review',
565
+ recentProjects_badge_running: 'Last recorded as running',
566
+ recentProjects_badge_completed: 'Last run completed',
567
+ recentProjects_badge_failed: 'Last run failed',
568
+ recentProjects_badge_interrupted: 'Last run may have been interrupted',
569
+ recentProjects_badge_stale: 'Last run status unconfirmed',
543
570
  recentProjects_unnamed: 'Project \u00b7 {prefix}',
544
571
  recentProjects_zombieRunningNote: 'Marked as running, but it may be a leftover from an interrupted run.',
545
572
  recentProjects_zombieDeleteConfirm: 'This session is marked as running. If no other tab is actually running it, you can delete it anyway.',
@@ -547,9 +574,9 @@
547
574
  recentProjects_degraded_hint: 'Could not identify the Overleaf account on this page, so the project list is hidden to avoid mixing accounts. This usually resolves on a reload.',
548
575
  recentProjects_sessionsCount: '{count} session(s)',
549
576
  recentProjects_welcome_stats: '{projects} projects \u00b7 {sessions} Codex sessions',
550
- recentProjects_badge_background_completed: 'completed in background',
551
- recentProjects_badge_needs_review_after_navigation: 'pending',
552
- recentProjects_badge_abandoned_after_navigation: 'abandoned'
577
+ recentProjects_badge_background_completed: 'Last run completed in background',
578
+ recentProjects_badge_needs_review_after_navigation: 'Last run needs review',
579
+ recentProjects_badge_abandoned_after_navigation: 'Last run stopped after navigation'
553
580
  },
554
581
  zh: {
555
582
  resizePanel: '拖动调整 Codex 面板宽度,双击恢复默认宽度',
@@ -1047,14 +1074,39 @@
1047
1074
  failureReason_editor_project_id_unavailable_next: '请刷新 Overleaf 页面后重试;如果仍然失败,请重新加载扩展。',
1048
1075
  // Welcome-panel + write-guard: Recent-projects
1049
1076
  // variant copy. 见 spec §5.3–§5.5、§5.8、§5.10。
1050
- recentProjects_welcome: 'Codex Overleaf Link',
1051
- recentProjects_welcome_subtitle: '从左侧 Overleaf 项目列表中打开任意项目即可开始。',
1052
- recentProjects_empty: '你还没有运行过 Codex 会话。从左侧 Overleaf 项目列表中打开任意项目即可开始。',
1053
- recentProjects_sessions_empty: '该项目还没有会话',
1054
- recentProjects_sessions_toggle: '查看/管理该项目的会话',
1055
- recentProjects_sessions_action: '查看会话',
1056
- recentProjects_sessions_action_open: '收起会话',
1057
- recentProjects_clearProject: '删除该项目的全部会话',
1077
+ recentProjects_welcome: '继续工作',
1078
+ recentProjects_focusEyebrow: '上次对话',
1079
+ recentProjects_historyHeading: '其他最近对话',
1080
+ recentProjects_historyHint: '点击条目继续',
1081
+ recentProjects_projectConversations: '查看该项目的全部对话',
1082
+ recentProjects_allProjects: '所有项目',
1083
+ recentProjects_showAllConversations: '查看全部 {count} 段对话',
1084
+ recentProjects_unstarted: '未开始 · {count}',
1085
+ recentProjects_unstartedHint: '空白对话收纳在这里,记录不会被删除。',
1086
+ recentProjects_noStarted: '尚无已开始的对话,空白对话可在下方展开。',
1087
+ recentProjects_savedDraft: '草稿已保存,可继续补充。',
1088
+ recentProjects_attention: '需要查看',
1089
+ recentProjects_unavailableRecords: '项目链接不可用的记录',
1090
+ recentProjects_loadingHistory: '正在加载对话…',
1091
+ recentProjects_loadFailed: '暂时无法读取对话历史,请重试。',
1092
+ recentProjects_continueHint: '仅打开这段对话,不会自动启动任务。',
1093
+ recentProjects_more: '更多选项',
1094
+ recentProjects_continue: '继续',
1095
+ recentProjects_openProject: '打开项目',
1096
+ recentProjects_loadingConversation: '正在加载对话…',
1097
+ recentProjects_noConversation: '暂无保存的对话',
1098
+ recentProjects_noRuns: '尚未运行任务',
1099
+ recentProjects_previewUnavailable: '暂时无法读取对话',
1100
+ recentProjects_previewUnavailableHint: '已保存的对话暂时无法读取。',
1101
+ recentProjects_conversationGone: '该对话已不可用,请从更新后的列表中选择其他对话。',
1102
+ recentProjects_continueFailed: '暂时无法打开对话,请重试。',
1103
+ recentProjects_welcome_subtitle: '当前浏览器保存的对话',
1104
+ recentProjects_empty: '当前浏览器还没有保存的对话。打开一个 Overleaf 项目,与 Codex 开始对话即可。',
1105
+ recentProjects_sessions_empty: '该项目还没有保存的对话',
1106
+ recentProjects_sessions_toggle: '查看或管理该项目的对话',
1107
+ recentProjects_sessions_action: '全部对话',
1108
+ recentProjects_sessions_action_open: '收起对话',
1109
+ recentProjects_clearProject: '删除该项目的全部对话',
1058
1110
  recentProjects_clearProject_title: '删除 {project} 的全部 Codex 会话?',
1059
1111
  recentProjects_clearProject_message: '这会永久删除 {project} 的全部 {count} 个插件会话、运行历史及对应的插件本地 Codex 线程历史。不会修改 Overleaf 文件或全局 Codex 历史。',
1060
1112
  recentProjects_clearProject_confirm: '全部删除',
@@ -1070,15 +1122,17 @@
1070
1122
  recentProjects_degraded: '请登录 Overleaf 后查看你最近的 Codex 项目。',
1071
1123
  recentProjects_row_projectLinkUnavailable: '项目链接不可用。',
1072
1124
  recentProjects_settings_entry: '设置与诊断',
1073
- recentProjects_badge_pending: '待处理',
1074
- recentProjects_badge_accepted: '已接受',
1075
- recentProjects_badge_rejected: '已撤回',
1076
- recentProjects_badge_needs_review: '待处理',
1077
- recentProjects_badge_running: '运行中',
1078
- recentProjects_badge_completed: '已完成',
1079
- recentProjects_badge_failed: '失败',
1080
- recentProjects_badge_interrupted: '已中断',
1081
- recentProjects_badge_stale: '久未处理',
1125
+ recentProjects_settingsTitle: '通用设置',
1126
+ recentProjects_settingsSubtitle: '管理适用于所有项目的 Codex 偏好设置与本地历史记录。',
1127
+ recentProjects_badge_pending: '上次运行待处理',
1128
+ recentProjects_badge_accepted: '上次修改已接受',
1129
+ recentProjects_badge_rejected: '上次修改已撤回',
1130
+ recentProjects_badge_needs_review: '上次运行需要检查',
1131
+ recentProjects_badge_running: '上次记录为运行中',
1132
+ recentProjects_badge_completed: '上次运行已完成',
1133
+ recentProjects_badge_failed: '上次运行失败',
1134
+ recentProjects_badge_interrupted: '上次运行可能已中断',
1135
+ recentProjects_badge_stale: '上次运行状态未确认',
1082
1136
  recentProjects_unnamed: '项目 · {prefix}',
1083
1137
  recentProjects_zombieRunningNote: '标记为运行中,但可能是被中断后的残留。',
1084
1138
  recentProjects_zombieDeleteConfirm: '该会话标记为运行中。如果确认没有其他标签页在运行它,可以直接删除。',
@@ -1086,9 +1140,9 @@
1086
1140
  recentProjects_degraded_hint: '无法识别本页的 Overleaf 账号,为避免账号间串数据已隐藏项目列表。通常刷新即可恢复。',
1087
1141
  recentProjects_sessionsCount: '{count} 个会话',
1088
1142
  recentProjects_welcome_stats: '{projects} 个项目 · {sessions} 个 Codex 会话',
1089
- recentProjects_badge_background_completed: '后台已完成',
1090
- recentProjects_badge_needs_review_after_navigation: '待处理',
1091
- recentProjects_badge_abandoned_after_navigation: '已中止'
1143
+ recentProjects_badge_background_completed: '上次运行已在后台完成',
1144
+ recentProjects_badge_needs_review_after_navigation: '上次运行需要检查',
1145
+ recentProjects_badge_abandoned_after_navigation: '上次运行已在离开页面后停止'
1092
1146
  }
1093
1147
  };
1094
1148
 
@@ -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'),