codex-overleaf-link 2.3.6 → 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 (41) hide show
  1. package/README.md +59 -34
  2. package/README.zh-CN.md +543 -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 +53 -50
  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 +583 -112
  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/runResultActions.js +16 -1
  21. package/extension/src/content/runTimelineView.js +1 -1
  22. package/extension/src/content/sessionForkController.js +20 -11
  23. package/extension/src/content/trackedChangeCaptureController.js +153 -0
  24. package/extension/src/content/writebackOrchestrator.js +31 -0
  25. package/extension/src/page/binaryAssetUploader.js +101 -29
  26. package/extension/src/page/overleafRealtimeObserver.js +4 -1
  27. package/extension/src/page/textFileCreator.js +493 -0
  28. package/extension/src/page/trackedChangeCapture.js +342 -0
  29. package/extension/src/page/trackedChangesLifecycle.js +145 -149
  30. package/extension/src/page/treeOperations.js +30 -2
  31. package/extension/src/page/writebackRouter.js +94 -48
  32. package/extension/src/pageBridge.js +62 -25
  33. package/extension/src/shared/compatibility.js +1 -1
  34. package/extension/src/shared/pageRpcContract.js +2 -1
  35. package/extension/src/shared/sessionState.js +6 -2
  36. package/extension/src/shared/storageDb.js +10 -12
  37. package/extension/src/shared/storageRunActions.js +29 -1
  38. package/extension/src/shared/writebackSettlement.js +19 -5
  39. package/extension/styles/panel.css +55 -10
  40. package/native-host/src/taskRunnerRuntime.js +25 -0
  41. 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.6';
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',
@@ -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'),
@@ -636,7 +636,7 @@
636
636
  undoExpectedFiles: normalizeRunFiles(run.undoExpectedFiles),
637
637
  undoStatus: sanitizeAssistantVisibleText(run.undoStatus),
638
638
  nativeRequestId: sanitizeAssistantVisibleText(run.nativeRequestId),
639
- codexTurnId: sanitizeAssistantVisibleText(run.codexTurnId),
639
+ codexThreadId: sanitizeAssistantVisibleText(run.codexThreadId), codexTurnId: sanitizeAssistantVisibleText(run.codexTurnId),
640
640
  forkSnapshot: run.forkSnapshot === true,
641
641
  forkSourceRunId: sanitizeAssistantVisibleText(run.forkSourceRunId),
642
642
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
@@ -656,6 +656,9 @@
656
656
  if (run.changedDocument === true) {
657
657
  normalized.changedDocument = true;
658
658
  }
659
+ if (Array.isArray(run.trackedChangeCaptures) && run.trackedChangeCaptures.length && !run.forkSnapshot) {
660
+ normalized.trackedChangeCaptures = sanitizeAssistantVisibleValue(run.trackedChangeCaptures);
661
+ }
659
662
  applyTrackedChangeStatus(normalized, run.trackedChangeStatus);
660
663
 
661
664
  return normalized;
@@ -1215,7 +1218,7 @@
1215
1218
  undoExpectedFiles: undoPayload.undoExpectedFiles,
1216
1219
  undoStatus: summarizeTextForStorage(run.undoStatus, 'undo status'),
1217
1220
  nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
1218
- codexTurnId: normalizeTextField(run.codexTurnId, 160),
1221
+ codexThreadId: normalizeTextField(run.codexThreadId, 160), codexTurnId: normalizeTextField(run.codexTurnId, 160),
1219
1222
  forkSnapshot: run.forkSnapshot === true,
1220
1223
  forkSourceRunId: normalizeTextField(run.forkSourceRunId, 160),
1221
1224
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
@@ -1627,6 +1630,7 @@
1627
1630
  setActiveSession,
1628
1631
  updateActiveSession,
1629
1632
  normalizeRuns,
1633
+ normalizeProjectReferenceFiles,
1630
1634
  prepareStateForStorage,
1631
1635
  estimateJsonBytes,
1632
1636
  computeSafeTaskSummary
@@ -1,18 +1,19 @@
1
1
  (function initStorageDb(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
3
  module.exports = factory(require('./storageRunActions'), require('./runInputQueue'),
4
- require('./settlementFacts'), require('./storageSessionClaims'));
4
+ require('./settlementFacts'), require('./storageSessionClaims'), require('./sessionState'));
5
5
  } else {
6
6
  root.CodexOverleafModuleRegistry.define('StorageDb', [
7
7
  'StorageRunActions',
8
8
  'RunInputQueue',
9
9
  'SettlementFacts',
10
- 'StorageSessionClaims'
10
+ 'StorageSessionClaims',
11
+ 'SessionState'
11
12
  ], factory);
12
13
  }
13
- })(typeof globalThis !== 'undefined' ? globalThis : window, function storageDbFactory(StorageRunActions, RunInputQueue, SettlementFacts, StorageSessionClaims) {
14
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function storageDbFactory(StorageRunActions, RunInputQueue, SettlementFacts, StorageSessionClaims, SessionState) {
14
15
  'use strict';
15
- if (!StorageRunActions?.compactRunsForStorage || !StorageRunActions?.compactRunActionPayload || !StorageRunActions?.compactProviderSnapshot || !StorageRunActions?.compactStructuredEventValue || !StorageRunActions?.hashString || !RunInputQueue?.compactForStorage || !SettlementFacts?.compactSettlementFacts || !StorageSessionClaims?.createSessionClaimer) {
16
+ if (!StorageRunActions?.compactRunsForStorage || !StorageRunActions?.compactRunActionPayload || !StorageRunActions?.compactProviderSnapshot || !StorageRunActions?.compactRunExecutionSnapshot || !StorageRunActions?.compactStructuredEventValue || !StorageRunActions?.hashString || !RunInputQueue?.compactForStorage || !SettlementFacts?.compactSettlementFacts || !StorageSessionClaims?.createSessionClaimer) {
16
17
  throw new Error('Codex Overleaf storage run-action helpers are unavailable.');
17
18
  }
18
19
  // IndexedDB versions are monotonic; v2.0 RC profiles have already opened v3.
@@ -279,6 +280,7 @@
279
280
  codexThreadId: typeof input.codexThreadId === 'string' ? input.codexThreadId : '',
280
281
  status: typeof input.status === 'string' && input.status ? input.status : 'active',
281
282
  focusFiles: normalizePathList(input.focusFiles),
283
+ projectReferenceFiles: SessionState.normalizeProjectReferenceFiles(input.projectReferenceFiles),
282
284
  pendingInputs: RunInputQueue.compactForStorage(input.pendingInputs, {
283
285
  normalizeField: normalizeTextField,
284
286
  normalizeDisplay: normalizeDisplayTextForStorage,
@@ -650,20 +652,15 @@
650
652
  }
651
653
 
652
654
  function compactRunsForStorage(runs, options) {
653
- return StorageRunActions.compactRunsForStorage(
654
- runs,
655
- options,
656
- SESSION_STORAGE_LIMITS.maxRunsPerSession,
657
- compactRunForStorage
658
- );
655
+ return StorageRunActions.compactRunsForStorage(runs, options, SESSION_STORAGE_LIMITS.maxRunsPerSession, compactRunForStorage);
659
656
  }
660
-
661
657
  function compactRunForStorage(run, keepActionPayload) {
662
658
  var actionPayload = StorageRunActions.compactRunActionPayload(run, keepActionPayload);
663
659
  var compact = {
664
660
  id: run.id,
665
661
  task: normalizeDisplayTextForStorage(run.task || 'untitled task', SESSION_STORAGE_LIMITS.taskChars),
666
662
  mode: typeof run.mode === 'string' ? redactSecretLikeText(run.mode) : '', ...StorageRunActions.compactProviderSnapshot(run),
663
+ ...StorageRunActions.compactRunExecutionSnapshot(run, function (text) { return normalizeTextField(text, 4096); }),
667
664
  model: normalizeTextField(run.model, 80),
668
665
  reasoningEffort: typeof run.reasoningEffort === 'string' ? redactSecretLikeText(run.reasoningEffort) : '',
669
666
  speedTier: typeof run.speedTier === 'string' ? redactSecretLikeText(run.speedTier) : '',
@@ -679,9 +676,10 @@
679
676
  undoBaseFiles: actionPayload.undoBaseFiles,
680
677
  undoTrackedChanges: actionPayload.undoTrackedChanges,
681
678
  undoExpectedFiles: actionPayload.undoExpectedFiles,
679
+ ...(actionPayload.trackedChangeCaptures ? { trackedChangeCaptures: actionPayload.trackedChangeCaptures } : {}),
682
680
  undoStatus: normalizeDisplayTextForStorage(run.undoStatus, SESSION_STORAGE_LIMITS.statusTextChars),
683
681
  nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
684
- codexTurnId: normalizeTextField(run.codexTurnId, 160),
682
+ codexThreadId: normalizeTextField(run.codexThreadId, 160), codexTurnId: normalizeTextField(run.codexTurnId, 160),
685
683
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
686
684
  queueItemId: normalizeTextField(run.queueItemId, 160)
687
685
  };
@@ -70,7 +70,13 @@
70
70
  } catch (_error) {
71
71
  return empty;
72
72
  }
73
- return getUtf8ByteLength(serialized) <= MAX_PERSISTED_ACTION_BYTES_PER_RUN ? payload : empty;
73
+ if (getUtf8ByteLength(serialized) > MAX_PERSISTED_ACTION_BYTES_PER_RUN) return empty;
74
+ const captures = cloneSerializableArray(run.trackedChangeCaptures);
75
+ if (captures.length) {
76
+ const extended = { ...payload, trackedChangeCaptures: captures };
77
+ if (getUtf8ByteLength(JSON.stringify(extended)) <= MAX_PERSISTED_ACTION_BYTES_PER_RUN) return extended;
78
+ }
79
+ return payload; // Optional capture evidence must never evict a mature Undo payload.
74
80
  }
75
81
 
76
82
  function emptyActionPayload() {
@@ -102,6 +108,27 @@
102
108
  return value.length * 2;
103
109
  }
104
110
 
111
+ // Persist only execution metadata; never infer a missing historical setting.
112
+ function compactRunExecutionSnapshot(run, normalizeString) {
113
+ var snapshot = run && run.executionSnapshot;
114
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) return {};
115
+ var normalize = typeof normalizeString === 'function' ? normalizeString : String;
116
+ var compact = {};
117
+ var fields = ['schemaVersion', 'mode', 'providerId', 'providerRevision', 'model',
118
+ 'reasoningEffort', 'speedTier', 'autoRecompile', 'requireReviewing', 'capturedAt', 'source'];
119
+ for (var key of fields) {
120
+ if (!Object.prototype.hasOwnProperty.call(snapshot, key)) continue;
121
+ var value = snapshot[key];
122
+ if (typeof value === 'string') compact[key] = normalize(value);
123
+ else if (value === null || typeof value === 'number' || typeof value === 'boolean') compact[key] = value;
124
+ }
125
+ if (Array.isArray(snapshot.focusFiles)) {
126
+ compact.focusFiles = snapshot.focusFiles.slice(0, 100)
127
+ .filter(function (value) { return typeof value === 'string'; }).map(normalize);
128
+ }
129
+ return { executionSnapshot: compact };
130
+ }
131
+
105
132
  function compactProviderSnapshot(value) {
106
133
  return {
107
134
  providerId: typeof value.providerId === 'string' && value.providerId ? value.providerId : 'builtin',
@@ -147,6 +174,7 @@
147
174
  compactRunsForStorage: compactRunsForStorage,
148
175
  compactRunActionPayload: compactRunActionPayload,
149
176
  compactProviderSnapshot: compactProviderSnapshot,
177
+ compactRunExecutionSnapshot: compactRunExecutionSnapshot,
150
178
  hashString: hashString
151
179
  };
152
180
  });