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
@@ -4,6 +4,7 @@
4
4
  const UPDATE_STATE_KEY = 'codex-overleaf-managed-update-state-v1';
5
5
  const CONSENT_STATE_KEY = 'codex-overleaf-update-consent-v1';
6
6
  const UPDATE_RELOAD_TABS_KEY = 'codex-overleaf-managed-update-tabs-v1';
7
+ const MANUAL_RELOAD_TABS_KEY = 'codex-overleaf-manual-runtime-reload-v1';
7
8
  const CHECK_ALARM = 'codex-overleaf-consent-update-check';
8
9
  const IDLE_ALARM = 'codex-overleaf-consent-update-idle';
9
10
  const WATCHDOG_ALARM = 'codex-overleaf-consent-update-watchdog';
@@ -12,7 +13,7 @@
12
13
  const CHECK_INTERVAL_MINUTES = 24 * 60;
13
14
  const SNOOZE_MS = 24 * 60 * 60 * 1000;
14
15
  const CANDIDATE_MAX_AGE_MS = 5 * 60 * 1000;
15
- const CHECK_REQUEST_TIMEOUT_MS = 15 * 1000;
16
+ const CHECK_REQUEST_TIMEOUT_MS = 55 * 1000;
16
17
  const ACTIVATION_TIMEOUT_MS = 20 * 1000;
17
18
  const ACTIVATION_POLL_MS = 250;
18
19
  const FAST_IDLE_RETRY_MS = 4000;
@@ -31,7 +32,8 @@
31
32
  'codex-overleaf/consent-update-check',
32
33
  'codex-overleaf/consent-update-install',
33
34
  'codex-overleaf/consent-update-later',
34
- 'codex-overleaf/consent-update-dismiss'
35
+ 'codex-overleaf/consent-update-dismiss',
36
+ 'codex-overleaf/consent-update-reload'
35
37
  ]);
36
38
 
37
39
  const policy = root.CodexOverleafUpdateConsent;
@@ -41,6 +43,10 @@
41
43
  let policyTail = Promise.resolve();
42
44
  let idleRetryTimer = null;
43
45
  let activationPromise = null;
46
+ let runtimeStatus = null;
47
+ let runtimeStatusAt = 0;
48
+ let runtimeStatusRead = null;
49
+ let runtimeReloadMessage = '';
44
50
 
45
51
  function init(options = {}) {
46
52
  if (initialized || !policy || !revocation) return;
@@ -92,6 +98,7 @@
92
98
  await reconcileRecoveryState();
93
99
  await settleTerminalConsent();
94
100
  await armLegacyGuard();
101
+ await finishManualRuntimeReload();
95
102
  await publishView();
96
103
  const updateState = await getUpdateState();
97
104
  if (['staged', 'waiting_for_idle'].includes(updateState.state)) {
@@ -129,6 +136,8 @@
129
136
  return postponeUpdate();
130
137
  case 'codex-overleaf/consent-update-dismiss':
131
138
  return dismissCompletedUpdate();
139
+ case 'codex-overleaf/consent-update-reload':
140
+ return reloadInstalledRuntime();
132
141
  default:
133
142
  throw codedError('unknown_update_action', 'Unknown update action.');
134
143
  }
@@ -648,11 +657,86 @@
648
657
  }
649
658
 
650
659
  async function getView() {
651
- const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
652
- return policy.deriveViewModel(state, consent, {
660
+ let [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
661
+ const busy = state.state === 'checking' || policy.isExecutionState(state.state) || consent.authorizationId;
662
+ const runtime = busy ? { state: 'transaction_active' } : await readRuntimeStatus();
663
+ if (runtime.state === 'aligned' && ['failed', 'rolled_back'].includes(state.state) &&
664
+ policy.compareStableVersions(state.latestVersion || state.currentVersion, runtime.installedVersion) <= 0) {
665
+ state = await setUpdateState({ ...state, state: 'idle', currentVersion: runtime.installedVersion,
666
+ latestVersion: runtime.installedVersion, code: '', message: '', transactionId: '', blocker: '', blockers: [] });
667
+ }
668
+ const view = policy.deriveViewModel(state, consent, {
653
669
  currentVersion: currentVersion(),
654
670
  now: Date.now()
655
671
  });
672
+ view.runtime = runtime;
673
+ const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
674
+ const pagesPending = runtime.state === 'aligned' && pending?.targetVersion === runtime.installedVersion && pending.tabIds?.length;
675
+ if (runtime.state === 'reload_required' || pagesPending) {
676
+ view.showPanel = true;
677
+ view.state = { ...state, state: pagesPending ? 'reload_tabs_required' : 'reload_required',
678
+ currentVersion: runtime.extensionVersion, latestVersion: runtime.installedVersion,
679
+ code: 'update_reload_required', message: runtimeReloadMessage };
680
+ view.badge = { text: 'UP', color: '#3578bd' };
681
+ }
682
+ return view;
683
+ }
684
+
685
+ async function readRuntimeStatus(force = false) {
686
+ if (!root.CodexOverleafUpdateRuntimeIdentity) return { state: 'unknown' };
687
+ if (!force && runtimeStatus && Date.now() - runtimeStatusAt < 5000) return runtimeStatus;
688
+ if (runtimeStatusRead) return runtimeStatusRead;
689
+ runtimeStatusRead = withTimeout(requestNative('update.status'), 4000,
690
+ codedError('update_status_timeout', 'Installed version inspection timed out.')).then(status => {
691
+ runtimeStatusAt = Date.now();
692
+ runtimeStatus = root.CodexOverleafUpdateRuntimeIdentity.inspectInstalledRuntime(status, {
693
+ extensionVersion: chrome.runtime.getManifest().version, runtimeVersion: currentVersion()
694
+ });
695
+ return runtimeStatus;
696
+ }).catch(() => ({ state: 'unknown' })).finally(() => { runtimeStatusRead = null; });
697
+ return runtimeStatusRead;
698
+ }
699
+
700
+ async function requireReloadSafePoint() {
701
+ const tabs = await chrome.tabs.query({ url: OVERLEAF_MATCHES });
702
+ const probes = await Promise.all(tabs.filter(isUsableEditorTab).map(tab => probeTabIdle(tab.id)));
703
+ const nativeGate = await requestNative('update.canApply').then(result => ({ ok: true, result }))
704
+ .catch(error => ({ ok: false, error: safeError(error) }));
705
+ const blockers = root.CodexOverleafUpdateStatus?.collectBlockers(probes, nativeGate) || ['busy'];
706
+ if (nativeBridge?.getPendingState?.().executionRequests > 0) blockers.push('background_execution_pending');
707
+ if (blockers.length) {
708
+ runtimeReloadMessage = 'Reload is waiting for Overleaf to be saved and idle (' + blockers.join(', ') + ').';
709
+ throw codedError('update_reload_busy', runtimeReloadMessage);
710
+ }
711
+ runtimeReloadMessage = '';
712
+ return tabs.filter(tab => Number.isInteger(tab.id) && !tab.discarded && tab.status !== 'unloaded');
713
+ }
714
+
715
+ async function reloadInstalledRuntime() {
716
+ const runtime = await readRuntimeStatus(true);
717
+ if (runtime.state === 'aligned') { await finishManualRuntimeReload(); return getView(); }
718
+ if (runtime.state !== 'reload_required') throw codedError('update_reload_unavailable', 'No verified installed update is awaiting reload.');
719
+ const tabs = await requireReloadSafePoint();
720
+ await chrome.storage.local.set({ [MANUAL_RELOAD_TABS_KEY]: {
721
+ targetVersion: runtime.installedVersion, tabIds: tabs.map(tab => tab.id)
722
+ } });
723
+ setTimeout(() => chrome.runtime.reload(), 0);
724
+ return getView();
725
+ }
726
+
727
+ async function finishManualRuntimeReload() {
728
+ const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
729
+ if (!pending || !Array.isArray(pending.tabIds)) return;
730
+ const runtime = await readRuntimeStatus(true);
731
+ if (runtime.state !== 'aligned' || runtime.installedVersion !== pending.targetVersion) return;
732
+ let tabs;
733
+ try { tabs = await requireReloadSafePoint(); } catch (_error) { return; }
734
+ const remaining = [];
735
+ for (const tab of tabs.filter(tab => pending.tabIds.includes(tab.id))) {
736
+ try { await chrome.tabs.reload(tab.id); } catch (_error) { remaining.push(tab.id); }
737
+ }
738
+ if (remaining.length) await chrome.storage.local.set({ [MANUAL_RELOAD_TABS_KEY]: { ...pending, tabIds: remaining } });
739
+ else await chrome.storage.local.remove(MANUAL_RELOAD_TABS_KEY);
656
740
  }
657
741
 
658
742
  async function publishView() {
@@ -123,6 +123,7 @@
123
123
  const raw = entry?.event || {};
124
124
  if (raw.type === 'codex.turn.bound') {
125
125
  session.codexThreadId = String(raw.detail?.threadId || session.codexThreadId || '');
126
+ record.codexThreadId = String(raw.detail?.threadId || record.codexThreadId || '');
126
127
  record.codexTurnId = String(raw.detail?.turnId || record.codexTurnId || '');
127
128
  record.nativeEventSeq = sequence;
128
129
  continue;
@@ -139,8 +139,10 @@
139
139
  // recordUndoFromApply classify a successful Track write as legacy Undo,
140
140
  // which hides Accept and prevents the lifecycle from surviving reload.
141
141
  merged.trackedChanges.push(...(Array.isArray(result?.trackedChanges) ? result.trackedChanges : []));
142
+ if (result?.trackedChangeCaptures?.length) (merged.trackedChangeCaptures ||= []).push(...result.trackedChangeCaptures);
142
143
  if (result?.ok !== true) merged.ok = false;
143
144
  }
145
+ if (merged.trackedChangeCaptures?.some(capture => capture.state !== 'observed')) merged.trackedChanges = [];
144
146
  if (merged.skipped.length) merged.ok = false;
145
147
  return merged;
146
148
  }
@@ -51,8 +51,7 @@
51
51
  'codex.providers.delete',
52
52
  'task.run',
53
53
  'mirror.sync',
54
- 'mirror.patchFiles',
55
- 'mirror.confirmWriteback',
54
+ 'mirror.patchFiles', 'mirror.confirmWriteback', 'mirror.invalidate',
56
55
  'mirror.scanSensitive',
57
56
  'codex.history.clearPlugin',
58
57
  'skills.list',
@@ -418,6 +417,8 @@
418
417
  const runTimelineView = Modules.RunTimelineView.create({
419
418
  RunGuidanceView: Modules.RunGuidanceView,
420
419
  RunResultActions: runResultActions,
420
+ RunScrollLayout: Modules.RunScrollLayout,
421
+ RunFailureNotice: Modules.RunFailureNotice,
421
422
  tr,
422
423
  tx,
423
424
  getLocale,
@@ -581,6 +582,7 @@
581
582
  RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
582
583
  getState: () => state,
583
584
  getCurrentRunView: () => currentRunView,
585
+ appendRunRecordEvent,
584
586
  onMirrorRefreshSettled: runSettlementPersistence.settleMirrorRefresh
585
587
  });
586
588
  const {
@@ -788,6 +790,7 @@
788
790
  },
789
791
  saveState,
790
792
  saveStateSoon,
793
+ onProjectFilesReady: refreshHistoricalProjectReferences,
791
794
  updateActiveSession,
792
795
  callPageBridge,
793
796
  getCurrentProjectId,
@@ -835,6 +838,7 @@
835
838
  });
836
839
 
837
840
  let panel = null;
841
+ let probeStatusSnapshot = null;
838
842
  let panelRendererInstance = null;
839
843
  let sessionPanelInstance = null;
840
844
  let settingsPanelInstance = null;
@@ -1031,7 +1035,7 @@
1031
1035
  providerSettingsCoordinator.syncSessionProvider().catch(() => {});
1032
1036
  syncOtWarmMirrorController().catch(error => {
1033
1037
  updateOtStatusDisplay('unavailable');
1034
- appendPlainLog(tx(`Failed to sync experimental OT warm mirror: ${error.message}`, `同步实验性 OT 预热镜像失败:${error.message}`));
1038
+ appendPlainLog(tx(`Failed to sync OT warm mirror: ${error.message}`, `同步 OT 预热镜像失败:${error.message}`));
1035
1039
  });
1036
1040
  await refreshProbe({ quiet: true });
1037
1041
  }
@@ -1274,6 +1278,7 @@
1274
1278
  onProvidersOpen: () => providerSettingsCoordinator.open(),
1275
1279
  onClearAllHistory: () => clearAllHistoryWithConfirm(),
1276
1280
  onHistoryOpen: () => renderAuditHistoryPanel(),
1281
+ onStorageOpen: () => refreshStorageUsageSummary(),
1277
1282
  onHistoryFilter: () => applyAuditHistoryFilter(),
1278
1283
  // Experimental OT mirror: a single visible switch whose click is
1279
1284
  // intercepted so the confirm-before-enable flow still runs.
@@ -1444,9 +1449,6 @@
1444
1449
  function renderAttachmentPreviewList(attachments = [], container, options = {}) {
1445
1450
  return composerAttachmentController.renderAttachmentPreviewList(attachments, container, options);
1446
1451
  }
1447
-
1448
-
1449
-
1450
1452
  function applyLocaleToPanel() {
1451
1453
  if (!panel) {
1452
1454
  return;
@@ -1535,6 +1537,17 @@
1535
1537
  updateModelDisplay();
1536
1538
  renderSessionList();
1537
1539
  renderContextSelection();
1540
+ const probeStatus = panel.querySelector('[data-probe-status]');
1541
+ if (probeStatus?.dataset.refreshing === 'true') probeStatus.textContent = tr('refreshProbeLoading');
1542
+ else if (probeStatus && probeStatusSnapshot?.projectId === getCurrentProjectId()) {
1543
+ const { failed, probe, userInitiated } = probeStatusSnapshot;
1544
+ const text = failed ? tr('refreshProbeFailed') : formatProbeStatusBar(probe);
1545
+ probeStatus.textContent = userInitiated && !failed ? tr('refreshProbeDone', { status: text }) : text;
1546
+ if (!failed) updateExistingProbeNotice(probe);
1547
+ }
1548
+ updateSkillsEntrySummary();
1549
+ providerSettingsCoordinator.renderSummary();
1550
+ if (settingsPanelInstance?.container?.querySelector('[data-storage-card]')?.open) refreshStorageUsageSummary();
1538
1551
  }
1539
1552
 
1540
1553
  function setElementTitleAndAria(selector, title, ariaLabel) {
@@ -1764,9 +1777,7 @@
1764
1777
  return contextTrayController.setContextStatus(text);
1765
1778
  }
1766
1779
 
1767
- function resetContextProject() {
1768
- return contextTrayController.resetContextProject();
1769
- }
1780
+ function resetContextProject() { return contextTrayController.resetContextProject(); }
1770
1781
 
1771
1782
  async function runTask(options = {}) {
1772
1783
  // Barrier on a still-flying background mirror refresh from the previous
@@ -2393,7 +2404,7 @@
2393
2404
  }
2394
2405
 
2395
2406
  await saveState();
2396
- applyStateToPanel();
2407
+ applyStateToPanel({ preserveScroll: true });
2397
2408
  } catch (persistenceError) {
2398
2409
  appendRunEvent({
2399
2410
  title: tx('Codex result was generated, but saving local session history failed.', 'Codex 结果已生成,但保存本地会话记录失败。'),
@@ -2473,14 +2484,15 @@
2473
2484
  setRunning(false);
2474
2485
  nativeChannel.clearActiveRequest();
2475
2486
  stopRunElapsedTick();
2476
- if (isExperimentalOtEnabled()) {
2477
- await resumeOtWarmMirror('run-settled');
2478
- }
2479
2487
  await flushQueuedSaveState().catch(() => {});
2480
2488
  currentRunView = null;
2481
2489
  runCancellationRequested = false;
2482
2490
  runCancellationController = null;
2483
2491
  activeTurnControl.release(settlement.requestId);
2492
+ if (isExperimentalOtEnabled()) {
2493
+ try { await resumeOtWarmMirror('run-settled'); }
2494
+ catch (_error) { updateOtStatusDisplay('unavailable'); }
2495
+ }
2484
2496
  if (settlement.requestId) {
2485
2497
  activeTurnControl.acknowledge(settlement.requestId).catch(() => {});
2486
2498
  }
@@ -4943,6 +4955,7 @@
4943
4955
  }
4944
4956
 
4945
4957
  async function refreshProbe(options = {}) {
4958
+ const projectId = getCurrentProjectId();
4946
4959
  const userInitiated = options.userInitiated === true;
4947
4960
  if (userInitiated) {
4948
4961
  setRefreshProbeLoading(true);
@@ -4958,6 +4971,8 @@
4958
4971
  const probe = await callPageBridge('probe', {
4959
4972
  manualOverride: state?.requireReviewing === false
4960
4973
  });
4974
+ if (projectId !== getCurrentProjectId()) return null;
4975
+ probeStatusSnapshot = { projectId, probe, userInitiated };
4961
4976
  const status = panel?.querySelector('[data-probe-status]');
4962
4977
  if (status) {
4963
4978
  status.textContent = userInitiated
@@ -4976,6 +4991,8 @@
4976
4991
  if (!userInitiated) {
4977
4992
  throw error;
4978
4993
  }
4994
+ if (projectId !== getCurrentProjectId()) return null;
4995
+ probeStatusSnapshot = { projectId, failed: true };
4979
4996
  const status = panel?.querySelector('[data-probe-status]');
4980
4997
  if (status) {
4981
4998
  status.textContent = tr('refreshProbeFailed');
@@ -5553,6 +5570,7 @@
5553
5570
  title: session.title || '',
5554
5571
  titleSource: session.titleSource || 'auto',
5555
5572
  focusFiles: session.focusFiles || [],
5573
+ projectReferenceFiles: session.projectReferenceFiles || [],
5556
5574
  codexThreadId: session.codexThreadId || '',
5557
5575
  createdAt: session.createdAt,
5558
5576
  updatedAt: session.updatedAt,
@@ -5960,15 +5978,7 @@
5960
5978
  }
5961
5979
 
5962
5980
  function getRenderedModelEntries() {
5963
- return Array.from(panel?.querySelector('[data-model]')?.options || []).map(option => ({
5964
- id: option.value,
5965
- label: option.textContent,
5966
- reasoningEfforts: (option.dataset.reasoningEfforts || '').split(',').filter(Boolean),
5967
- defaultReasoningEffort: option.dataset.defaultReasoningEffort || '',
5968
- reasoningPresentation: option.dataset.reasoningPresentation || '',
5969
- speedTiers: (option.dataset.speedTiers || 'standard').split(',').filter(Boolean),
5970
- defaultSpeedTier: option.dataset.defaultSpeedTier || 'standard'
5971
- }));
5981
+ return Modules.ModelPickerSupport.getRenderedModelEntries(panel);
5972
5982
  }
5973
5983
 
5974
5984
  async function selectMode(mode) {
@@ -6069,7 +6079,7 @@
6069
6079
  saveStateSoon();
6070
6080
  }
6071
6081
 
6072
- function applyStateToPanel() {
6082
+ function applyStateToPanel(options = {}) {
6073
6083
  applyPanelTheme(getThemePreference());
6074
6084
  if ((state.providerId || 'builtin') === 'builtin' && !modelSelectHasOption(state.model)) {
6075
6085
  renderModelOptions(getModelCatalog().FALLBACK_MODELS, state.model);
@@ -6105,7 +6115,8 @@
6105
6115
  syncModeControls();
6106
6116
  applySessionLabel();
6107
6117
  renderSessionList();
6108
- renderRunHistory();
6118
+ renderRunHistory(options);
6119
+ getTrackedChangeCaptureController().schedule();
6109
6120
  renderContextSelection();
6110
6121
  renderContextSummary();
6111
6122
  // v1.8.0 C4: refresh recovery — small attachments persisted in state are
@@ -6195,7 +6206,7 @@
6195
6206
  undoExpectedFiles: [],
6196
6207
  undoStatus: '',
6197
6208
  queueItemId,
6198
- nativeRequestId: '',
6209
+ nativeRequestId: '', codexThreadId: '',
6199
6210
  codexTurnId: '',
6200
6211
  nativeEventSeq: 0,
6201
6212
  executionSnapshot: executionSnapshot
@@ -6362,7 +6373,7 @@
6362
6373
  currentRunView.activeTurn = { threadId, turnId };
6363
6374
  const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
6364
6375
  if (record) {
6365
- record.codexTurnId = turnId;
6376
+ record.codexThreadId = threadId; record.codexTurnId = turnId;
6366
6377
  record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(journalSeq || 0));
6367
6378
  }
6368
6379
  if (threadId) {
@@ -6736,6 +6747,13 @@
6736
6747
  return files;
6737
6748
  }
6738
6749
 
6750
+ function refreshHistoricalProjectReferences(project) {
6751
+ if (!project?.id || String(project.id) !== getCurrentProjectId()) return;
6752
+ state = updateActiveSession(state, { projectReferenceFiles: captureProjectReferenceFiles(project) });
6753
+ saveStateSoon();
6754
+ if (!currentRunView && !trackedChangeInFlight.size) renderRunHistory({ preserveScroll: true });
6755
+ }
6756
+
6739
6757
  function persistCurrentProjectReferenceFiles(project) {
6740
6758
  const projectFiles = captureProjectReferenceFiles(project);
6741
6759
  if (currentRunView) {
@@ -6867,6 +6885,7 @@
6867
6885
  runProjectId: getRunProjectIdForWriteback(run)
6868
6886
  });
6869
6887
  const undoApplied = isUndoResultEffectivelyApplied(run, result);
6888
+ writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
6870
6889
  appendUndoReviewingPolicyEvent(runId, result.reviewingPolicy);
6871
6890
  appendRunRecordEvent(runId, {
6872
6891
  title: tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
@@ -6929,11 +6948,16 @@
6929
6948
  });
6930
6949
 
6931
6950
  let result;
6951
+ let trackedUndoPostFilesAtDispatch;
6932
6952
  try {
6953
+ trackedUndoPostFilesAtDispatch = buildTrackedUndoPostFiles(run);
6933
6954
  result = await callPageBridge('rejectTrackedChanges', {
6934
6955
  trackedChanges: run.undoTrackedChanges || [],
6935
6956
  expectedFiles: run.undoExpectedFiles || [],
6936
- postFiles: buildTrackedUndoPostFiles(run),
6957
+ postFiles: trackedUndoPostFilesAtDispatch,
6958
+ untrackedUndo: run.executionSnapshot
6959
+ ? run.executionSnapshot.requireReviewing === false && ['submitted', 'legacy-captured'].includes(run.executionSnapshot.source)
6960
+ : getTrackedChangeCaptureController().hasLegacyUntrackedCheckpoint(run),
6937
6961
  // Welcome-panel + write-guard:
6938
6962
  // bind the reject to the run's original project. If the user has
6939
6963
  // navigated away the page-side guard refuses with
@@ -6945,6 +6969,7 @@
6945
6969
  trackedChangeInFlight.delete(runId);
6946
6970
  }
6947
6971
  }
6972
+ writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
6948
6973
  appendRunRecordEvent(runId, {
6949
6974
  title: trackedUndo
6950
6975
  ? tr('undoTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 })
@@ -6964,7 +6989,8 @@
6964
6989
  });
6965
6990
  if (lifecycleReject) {
6966
6991
  WritebackSettlement.attachUndoNotVerifiedFailure(run, result, {
6967
- buildFailure: buildContentFailure
6992
+ buildFailure: buildContentFailure,
6993
+ postFiles: trackedUndoPostFilesAtDispatch
6968
6994
  });
6969
6995
  applyTrackedChangeSettlement(runId, 'reject', result);
6970
6996
  return;
@@ -7178,6 +7204,25 @@
7178
7204
  return WritebackSettlement.attachVerifiedContentToOperation(operation, result);
7179
7205
  }
7180
7206
 
7207
+ let trackedChangeCaptureController = null;
7208
+ function getTrackedChangeCaptureController() {
7209
+ return trackedChangeCaptureController ||= Modules.TrackedChangeCaptureController.create({
7210
+ getRuns: () => (state.sessions || []).flatMap(session => session.runs || []),
7211
+ getProjectId: () => window.location.pathname.match(/^\/project\/([^/]+)/)?.[1] || '',
7212
+ buildExpectedFilesAfterOperations, normalizeTrackedChanges: normalizeApplyTrackedChanges, callPageBridge,
7213
+ persist: () => flushQueuedSaveState({ preserveRunActionPayload: true }),
7214
+ refresh: run => refreshRunCardControls(run.id),
7215
+ notify: (run, kind, detail) => appendRunRecordEvent(run.id, {
7216
+ title: {
7217
+ pending: tx('Files written; confirming tracked changes.', '文件已写入,正在确认留痕记录。'),
7218
+ observed: tx('Tracked changes confirmed; Accept is available.', '留痕记录已确认,可以接受改动。'),
7219
+ needs_review: tx('Tracked changes could not be safely attributed. Review them in Overleaf; Undo is preserved.', '留痕归属尚未确认,请在 Overleaf 审阅;原有撤销能力已保留。')
7220
+ }[kind], status: 'info', detail
7221
+ }),
7222
+ onError: error => appendPlainLog(tx('Tracked-change confirmation unavailable: ', '留痕确认暂不可用:') + error.message)
7223
+ });
7224
+ }
7225
+
7181
7226
  function recordUndoFromApply(project, applyResult) {
7182
7227
  const appliedEntries = getAppliedEntries(applyResult);
7183
7228
  if (!currentRunView?.recordId || !appliedEntries.length) {
@@ -7221,6 +7266,7 @@
7221
7266
  if (combinedTrackedChanges.length) {
7222
7267
  record.trackedChangeStatus = 'pending';
7223
7268
  }
7269
+ getTrackedChangeCaptureController().record(record, applyResult?.trackedChangeCaptures);
7224
7270
  refreshRunCardControls(record.id);
7225
7271
  if (combinedTrackedChanges.length) {
7226
7272
  appendRunEvent({
@@ -7285,38 +7331,12 @@
7285
7331
  return WritebackSettlement.normalizeApplyTrackedChanges(changes);
7286
7332
  }
7287
7333
 
7288
- function selectExpectedFilesForTrackedUndo(
7289
- project,
7290
- operations = [],
7291
- trackedChanges = [],
7292
- previousExpectedFiles = []
7293
- ) {
7294
- return WritebackSettlement.selectExpectedFilesForTrackedUndo(
7295
- project,
7296
- operations,
7297
- trackedChanges,
7298
- previousExpectedFiles
7299
- );
7334
+ function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = [], previousExpectedFiles = []) {
7335
+ return WritebackSettlement.selectExpectedFilesForTrackedUndo(project, operations, trackedChanges, previousExpectedFiles);
7300
7336
  }
7301
7337
 
7302
7338
  function buildTrackedUndoPostFiles(run) {
7303
- const expectedFiles = Array.isArray(run?.undoExpectedFiles) ? run.undoExpectedFiles : [];
7304
- const appliedOperations = Array.isArray(run?.appliedOperations) ? run.appliedOperations : [];
7305
- if (!expectedFiles.length || !appliedOperations.length) {
7306
- return [];
7307
- }
7308
-
7309
- const postFilesByPath = buildExpectedFilesAfterOperations(
7310
- { files: expectedFiles },
7311
- appliedOperations
7312
- );
7313
- const expectedPaths = new Set(expectedFiles.map(file => file?.path).filter(Boolean));
7314
- return Array.from(postFilesByPath.entries())
7315
- .filter(([path, content]) => expectedPaths.has(path) && typeof content === 'string')
7316
- .map(([path, content]) => ({
7317
- path,
7318
- content
7319
- }));
7339
+ return getTrackedChangeCaptureController().buildPostFiles(run);
7320
7340
  }
7321
7341
 
7322
7342
  function hasTrackedEditorUndo(run) {
@@ -22,6 +22,7 @@
22
22
  setState = () => {},
23
23
  saveState = null,
24
24
  saveStateSoon = () => {},
25
+ onProjectFilesReady = () => {},
25
26
  updateActiveSession = defaultUpdateActiveSession,
26
27
  callPageBridge = () => Promise.resolve({ ok: false }),
27
28
  getCurrentProjectId = () => '',
@@ -201,6 +202,7 @@
201
202
  renderContextFiles(contextProject);
202
203
  if (isExactContextFileListProject(contextProject)) {
203
204
  setContextSyncState('exact-ready');
205
+ onProjectFilesReady(contextProject);
204
206
  } else if (contextSyncState !== 'partial') {
205
207
  setContextSyncState('exact-loading');
206
208
  void enhanceContextFilesFromExactSnapshot({ loadId: contextLoadId });
@@ -224,6 +226,7 @@
224
226
  contextProject = project;
225
227
  setContextSyncState(isExactContextFileListProject(project) ? 'exact-ready' : 'exact-loading');
226
228
  renderContextFiles(project);
229
+ if (isExactContextFileListProject(project)) onProjectFilesReady(project);
227
230
  if (!isExactContextFileListProject(project)) {
228
231
  void enhanceContextFilesFromExactSnapshot({
229
232
  force: Boolean(options.force),
@@ -292,6 +295,7 @@
292
295
  contextProject = project;
293
296
  setContextSyncState('exact-ready');
294
297
  renderContextFiles(project);
298
+ onProjectFilesReady(project);
295
299
  } catch (_error) {
296
300
  // The page file tree remains usable when the optional exact ZIP list is unavailable.
297
301
  if (loadId === contextLoadId) {
@@ -359,7 +359,7 @@
359
359
  if (!options.collectOnly) {
360
360
  showDiagnosticsLoading(tr('diagnosticsOtTitle'), tr('diagnosticsLoading'));
361
361
  }
362
- // OT warm mirror is experimental and off by default. When it is disabled
362
+ // OT warm mirror is opt-in and off by default. When it is disabled
363
363
  // there is nothing live to probe, so report a clean, healthy "disabled"
364
364
  // state instead of querying (then warning about) absent OT metadata — or
365
365
  // degrading to the generic "could not run" error if a probe throws. A
@@ -374,6 +374,14 @@
374
374
  nextStep: '',
375
375
  technical: ''
376
376
  };
377
+ const mirrorState = getOtWarmMirrorState?.() || {};
378
+ if (mirrorState.failureActive && mirrorState.lastFailure) {
379
+ const failure = mirrorState.lastFailure;
380
+ disabledResult.status = 'warning';
381
+ disabledResult.summary = tr(otWarmMirrorController.getFailureMessageKey(failure.code));
382
+ disabledResult.nextStep = tr('otFailureNextStep');
383
+ disabledResult.technical = `lastErrorCode: ${failure.code}\nphase: ${failure.phase}\nfailedAt: ${new Date(failure.at).toISOString()}`;
384
+ }
377
385
  if (options.collectOnly) {
378
386
  return disabledResult;
379
387
  }
@@ -434,6 +442,7 @@
434
442
  const queuedEventCount = normalizeOtDiagnosticsCount(otStatus?.queuedEventCount);
435
443
  const lastEventAt = formatOtDiagnosticValue(otStatus?.lastEventAt, tr('noneValue'));
436
444
  const warmMirrorState = getOtWarmMirrorState() || {};
445
+ const failureActive = warmMirrorState.failureActive === true;
437
446
  const lastOtPatchAt = formatOtDiagnosticValue(mirrorStatus?.lastOtPatchAt || warmMirrorState.lastPatchAt, tr('noneValue'));
438
447
  const lastOtErrorCode = formatOtDiagnosticValue(mirrorStatus?.lastOtErrorCode || warmMirrorState.lastErrorCode, tr('noneValue'));
439
448
  const lastErrorCode = formatOtDiagnosticValue(otStatus?.lastErrorCode || otStatus?.reason || otStatus?.error, tr('noneValue'));
@@ -445,13 +454,14 @@
445
454
  const bridgeFailed = otStatus?.ok === false;
446
455
  const focusFiles = getActiveFocusFiles();
447
456
  const otWarmStart = otWarmMirrorController?.canUseOtWarmStart?.({ enabled, focusFiles, mirrorStatus }) || { ok: false };
448
- const fallback = !enabled || statusValue !== 'observing' || bridgeFailed || otWarmStart?.ok !== true;
457
+ const fallback = !enabled || failureActive || statusValue !== 'observing' || bridgeFailed || otWarmStart?.ok !== true;
449
458
 
450
459
  return {
451
460
  title: tr('diagnosticsOtTitle'),
452
461
  subtitle: tr('diagnosticsOtSubtitle'),
453
462
  status: enabled && !fallback && !bridgeFailed ? 'completed' : (enabled || bridgeFailed ? 'warning' : 'completed'),
454
- summary: tr(enabled ? 'diagnosticsOtSummaryEnabled' : 'diagnosticsOtSummaryDisabled'),
463
+ summary: tr(failureActive ? otWarmMirrorController.getFailureMessageKey(warmMirrorState.lastErrorCode)
464
+ : enabled ? 'diagnosticsOtSummaryEnabled' : 'diagnosticsOtSummaryDisabled'),
455
465
  bullets: [
456
466
  `${tr('otStatus')}: ${formatOtStatusLabel(statusValue)}`,
457
467
  `${tr('otFreshFiles')}: ${otFreshFileCount}`,
@@ -631,6 +641,10 @@
631
641
  lines.push(`CodeMirror: docLength=${view.docLength || 0}; dispatch=${Boolean(view.hasDispatch)}; source=${view.source || 'unknown'}`);
632
642
  }
633
643
  }
644
+ const capture = probe?.trackedChangeCapture;
645
+ if (capture) lines.push(tx('Tracked ledger: ', '留痕记录:')
646
+ + 'source=' + capture.source + '; ready=' + capture.ready + '; count=' + capture.count
647
+ + '; doc=' + (capture.docId || 'none') + '; adapter=' + capture.adapter + '; reason=' + (capture.reason || 'none'));
634
648
  const projectDiagnostics = probe?.projectDiagnostics;
635
649
  if (projectDiagnostics) {
636
650
  lines.push(`Project records: ${projectDiagnostics.docRecordCount || 0}; roots=${(projectDiagnostics.internalRootKeys || []).slice(0, 6).join(', ') || 'none'}`);