codex-overleaf-link 2.5.0 → 2.6.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 (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -63,6 +63,7 @@
63
63
  window
64
64
  });
65
65
  snapshotRouter = requirePageModule('CodexOverleafSnapshotRouter').create({
66
+ getActiveEditorIdentity,
66
67
  normalizePath: normalizeSafeProjectPath,
67
68
  readActiveEditorText,
68
69
  treeOperations,
@@ -72,16 +73,22 @@
72
73
  const textFileCreator = requirePageModule('CodexOverleafTextFileCreator').create({
73
74
  window, document, treeOperations, snapshotRouter,
74
75
  collectElements, ensureEditing, ensureReviewing, readActiveEditorText, readWriteCancellationSequence,
76
+ findUpgradeModal: () => window.CodexOverleafReviewing.findUpgradeModal(document),
75
77
  uploadHelpers: requirePageModule('CodexOverleafBinaryAssetUploader')
76
78
  });
77
79
  binaryAssetUploader = requirePageModule('CodexOverleafBinaryAssetUploader').create({
78
80
  prepareUploadParent: textFileCreator.prepareUploadParent,
81
+ cleanupCreatedDirectories: textFileCreator.cleanupCreatedDirectories,
82
+ isUploadParentCurrent: textFileCreator.isUploadParentCurrent,
79
83
  findFolderNode: textFileCreator.findFolderNode,
80
84
  document,
81
85
  snapshotRouter,
82
86
  treeOperations,
83
87
  window
84
88
  });
89
+ const fileMover = requirePageModule('CodexOverleafFileMover').create({
90
+ window, document, treeOperations, snapshotRouter, textFileCreator, normalizeSafeProjectPath
91
+ });
85
92
  writeGuard = requirePageModule('CodexOverleafWriteGuard').create({
86
93
  document,
87
94
  treeOperations,
@@ -102,6 +109,7 @@
102
109
  beginTextCreateBatch: textFileCreator.beginCreateBatch,
103
110
  finishTextCreateBatch: textFileCreator.finishCreateBatch,
104
111
  deleteTextFile: textFileCreator.deleteFile,
112
+ moveFile: fileMover.moveFile,
105
113
  activeEditorIdentityChanged,
106
114
  clickNode,
107
115
  collectElements,
@@ -221,7 +229,8 @@
221
229
 
222
230
  const pageRpcHandlers = {
223
231
  probe,
224
- cancelActiveWrite() {
232
+ cancelActiveWrite(params = {}) {
233
+ if (params.reviewOwnerToken) return { ok: true, cancelled: requirePageModule('CodexOverleafReviewContext').cancelOwner(params.reviewOwnerToken) };
225
234
  const sequence = bumpWriteCancellationSequence();
226
235
  textFileCreator.cancelVerifications();
227
236
  snapshotRouter.cancelWritebackReads();
@@ -391,6 +400,11 @@
391
400
  fallbackUserMessage: 'Target file is active but the editor was not ready before timeout.',
392
401
  fallbackNextAction: 'Wait for Overleaf to finish loading, then retry.'
393
402
  },
403
+ reviewing_unavailable: {
404
+ stage: 'reviewing', severity: 'blocked', defaultRetryable: false,
405
+ fallbackUserMessage: "Track Changes is unavailable for this project. It depends on the project owner's Overleaf plan.",
406
+ fallbackNextAction: 'Turn off Track in the composer to write without tracked changes. Codex still records each change so you can undo it.'
407
+ },
394
408
  reviewing_state_unknown: {
395
409
  stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
396
410
  fallbackUserMessage: 'Codex could not determine whether Reviewing/Track Changes is enabled.',
@@ -480,6 +494,8 @@
480
494
  findFileTreeManager,
481
495
  findHistoryObject,
482
496
  findReviewingActivationControl,
497
+ findUpgradeModal: () => window.CodexOverleafReviewing.findUpgradeModal(document),
498
+ reviewingConfirmed: isReviewingConfirmedForWrite(getReviewingState()),
483
499
  getCodeMirrorEditorView,
484
500
  getDeepActiveElement,
485
501
  isInsideCodexPanel,
@@ -489,7 +505,7 @@
489
505
 
490
506
  async function ensureReviewing(params = {}) {
491
507
  const initial = getReviewingState(params);
492
- if (isReviewingConfirmedForWrite(initial)) {
508
+ if (isReviewingConfirmedForWrite(initial) && !window.CodexOverleafReviewing.findUpgradeModal(document)) {
493
509
  return {
494
510
  ok: true,
495
511
  activated: false,
@@ -498,6 +514,7 @@
498
514
  }
499
515
 
500
516
  const switched = await toggleReviewingMode(true, params);
517
+ if (switched.code === 'reviewing_unavailable') return { ...switched, reason: switched.failure.userMessage };
501
518
  if (switched.ok) {
502
519
  return {
503
520
  ok: true,
@@ -545,6 +562,7 @@
545
562
  }
546
563
 
547
564
  const switched = await toggleReviewingMode(false, params);
565
+ if (switched.code === 'reviewing_unavailable') return { ...switched, reason: switched.failure.userMessage };
548
566
  if (switched.ok) {
549
567
  return {
550
568
  ok: true,
@@ -690,7 +708,14 @@
690
708
  if (to - from > 20000) return { ok: false, code: 'selection_too_large' };
691
709
  const source = editorState.doc.toString();
692
710
  const text = source.slice(from, to);
693
- const digest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(source));
711
+ // UTF-8 replaces isolated surrogates; exclude them so flank hashes prove exact UTF-16 text.
712
+ if (/[\uD800-\uDFFF]/u.test(source) || /[\uD800-\uDFFF]/u.test(text)) return { ok: false, code: 'selection_unavailable' };
713
+ const hashText = async value => Array.from(new Uint8Array(
714
+ await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))
715
+ ), byte => byte.toString(16).padStart(2, '0')).join('');
716
+ const [documentHash, prefixHash, suffixHash] = await Promise.all([
717
+ hashText(source), hashText(source.slice(0, from)), hashText(source.slice(to))
718
+ ]);
694
719
  if (getProjectId() !== projectId || getActiveFilePath() !== path || getCodeMirrorEditorView() !== view
695
720
  || view.state.doc !== editorState.doc || view.state.selection.main.from !== from || view.state.selection.main.to !== to) {
696
721
  return { ok: false, code: 'selection_changed' };
@@ -698,7 +723,8 @@
698
723
  const captured = { projectId, path, from, to, text,
699
724
  lineStart: editorState.doc.lineAt(from).number,
700
725
  lineEnd: editorState.doc.lineAt(Math.max(from, to - 1)).number,
701
- documentHash: Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''),
726
+ documentHash,
727
+ rebaseEvidence: { v: 1, documentLength: source.length, prefixHash, suffixHash },
702
728
  capturedAt: new Date().toISOString() };
703
729
  let anchor = null;
704
730
  try {
@@ -727,9 +753,16 @@
727
753
  const activeFileMatches = getActiveFilePath() === filePath;
728
754
  const text = readActiveEditorText();
729
755
  lastText = text;
756
+ const currentEditorIdentity = getActiveEditorIdentity();
757
+ // A file-tree click can reconfigure or blur the old editor before the
758
+ // target document loads. A new view/state alone does not prove a switch.
759
+ const documentChanged = currentEditorIdentity?.type === 'codemirror-view'
760
+ && previousEditorIdentity?.type === 'codemirror-view'
761
+ ? currentEditorIdentity.doc !== previousEditorIdentity.doc
762
+ : Boolean(currentEditorIdentity) && activeEditorIdentityChanged(previousEditorIdentity);
730
763
  if (activeFileMatches
731
764
  && window.CodexOverleafProjectFiles.isUsableProjectFileContent(text)
732
- && activeEditorIdentityChanged(previousEditorIdentity)) {
765
+ && documentChanged) {
733
766
  return {
734
767
  ok: true,
735
768
  text
@@ -855,6 +888,7 @@
855
888
  }
856
889
 
857
890
  const switched = await toggleReviewingMode(enabled, params);
891
+ if (switched.code === 'reviewing_unavailable') return { ...switched, reason: switched.failure.userMessage };
858
892
  if (switched.ok) {
859
893
  return {
860
894
  ok: true,
@@ -901,67 +935,23 @@
901
935
  }
902
936
 
903
937
  async function toggleReviewingMode(enabled, params = {}) {
904
- const attemptedNodes = uniqueNodes([
905
- findReviewingActivationControl(),
906
- findReviewingModeMenuControl()
907
- ].filter(Boolean));
908
-
909
- for (const control of attemptedNodes) {
910
- clickNode(control);
911
- let after = await waitForReviewingEnabled(enabled, {
912
- ...params,
913
- waitMs: Math.min(resolveWaitMs(params), 180)
914
- });
915
- if (isReviewingTargetStateConfirmed(enabled, after)) {
916
- return {
917
- ok: true,
918
- attempted: true,
919
- reviewing: after.reviewing
920
- };
938
+ const sequence = readWriteCancellationSequence();
939
+ const assertCurrent = () => {
940
+ if (sequence !== readWriteCancellationSequence()) {
941
+ throw Object.assign(new Error('Writeback was cancelled.'), { code: 'codex_cancelled' });
921
942
  }
922
-
923
- const modeOption = findReviewingModeOption(enabled, { requireMenuOption: true });
924
- if (modeOption) {
925
- clickNode(modeOption);
926
- after = await waitForReviewingEnabled(enabled, params);
927
- if (isReviewingTargetStateConfirmed(enabled, after)) {
928
- return {
929
- ok: true,
930
- attempted: true,
931
- reviewing: after.reviewing
932
- };
933
- }
943
+ if (params.runProjectId && writeGuard.getEditorProjectIdPageSide(params.runProjectId) !== params.runProjectId) {
944
+ throw Object.assign(new Error('The active project changed.'), { code: 'aborted_project_changed' });
934
945
  }
935
- }
936
-
937
- const visibleModeOption = findReviewingModeOption(enabled, { requireMenuOption: true });
938
- if (visibleModeOption) {
939
- clickNode(visibleModeOption);
940
- const after = await waitForReviewingEnabled(enabled, params);
941
- return {
942
- ok: isReviewingTargetStateConfirmed(enabled, after),
943
- attempted: true,
944
- reviewing: after.reviewing
945
- };
946
- }
947
-
948
- const after = getReviewingState(params);
949
- return {
950
- ok: false,
951
- attempted: attemptedNodes.length > 0,
952
- reviewing: after.reviewing
953
946
  };
954
- }
955
-
956
- async function waitForReviewingEnabled(enabled, params = {}) {
957
- const waitMs = resolveWaitMs(params);
958
- const deadline = Date.now() + Math.max(0, waitMs);
959
- let current = getReviewingState(params);
960
- while (!isReviewingTargetStateConfirmed(enabled, current) && Date.now() < deadline) {
961
- await delay(120);
962
- current = getReviewingState(params);
963
- }
964
- return current;
947
+ return window.CodexOverleafReviewing.toggleMode(enabled, params, {
948
+ document, assertCurrent, delay,
949
+ getState: () => getReviewingState(params), isConfirmed: isReviewingTargetStateConfirmed,
950
+ click: clickNode,
951
+ findControls: () => findReviewingModeControls(enabled),
952
+ findOption: value => findReviewingModeOption(value, { requireMenuOption: true }),
953
+ buildFailure: buildPageBridgeFailure
954
+ });
965
955
  }
966
956
 
967
957
  function resolveWaitMs(params = {}) {
@@ -1093,7 +1083,72 @@
1093
1083
  return String(value || '').replace(/\s+/g, ' ').trim();
1094
1084
  }
1095
1085
 
1086
+ function isReviewingModeNodeAvailable(node) {
1087
+ if (!node || node.ownerDocument !== document || !node.isConnected || node.disabled
1088
+ || node.getAttribute?.('aria-disabled') === 'true' || !node.getClientRects?.().length) return false;
1089
+ for (let ancestor = node; ancestor; ancestor = ancestor.parentElement || ancestor.getRootNode?.().host) {
1090
+ if (isInsideCodexPanel(ancestor) || ancestor.closest?.(
1091
+ '.review-panel, .review-panel-container, .review-panel-header-slot, .review-sidebar, #review-panel, [data-testid="review-panel"]'
1092
+ ) || ancestor.hidden || ancestor.getAttribute?.('aria-hidden') === 'true' || ancestor.hasAttribute?.('inert')) return false;
1093
+ const style = window.getComputedStyle?.(ancestor);
1094
+ if (style?.display === 'none' || style?.visibility === 'hidden' || style?.visibility === 'collapse'
1095
+ || style?.opacity === '0') return false;
1096
+ }
1097
+ return true;
1098
+ }
1099
+
1100
+ function findReviewingToolbarControl() {
1101
+ return Array.from(document.querySelectorAll(
1102
+ '.toolbar-editor .review-mode-switcher > .review-mode-switcher-toggle-button'
1103
+ )).find(isReviewingModeNodeAvailable) || null;
1104
+ }
1105
+
1106
+ function findReviewingModeControls(enabled) {
1107
+ const toolbarControl = findReviewingToolbarControl();
1108
+ return toolbarControl ? [toolbarControl]
1109
+ : [findReviewingActivationControl(enabled), findReviewingModeMenuControl()];
1110
+ }
1111
+
1112
+ function findReviewingToolbarMenu(control) {
1113
+ const controlledIds = (control.getAttribute('aria-controls') || '').split(/\s+/).filter(Boolean);
1114
+ for (const id of controlledIds) {
1115
+ const menu = document.getElementById(id);
1116
+ if (isReviewingModeNodeAvailable(menu)) return menu;
1117
+ }
1118
+ if (control.getAttribute('aria-expanded') !== 'true') return null;
1119
+ const selector = '[role="menu"], [role="listbox"], .dropdown-menu';
1120
+ // Current Overleaf keeps the menu beside the toggle, inside this switcher.
1121
+ const localMenu = Array.from(control.parentElement?.querySelectorAll(selector) || [])
1122
+ .find(node => node.closest('.review-mode-switcher') === control.parentElement && isReviewingModeNodeAvailable(node));
1123
+ if (localMenu) return localMenu;
1124
+ // A portalled menu must still name this switch, rather than an unrelated dropdown.
1125
+ return control.id ? Array.from(document.querySelectorAll(selector)).find(node =>
1126
+ (node.getAttribute('aria-labelledby') || '').split(/\s+/).includes(control.id)
1127
+ && isReviewingModeNodeAvailable(node)) || null : null;
1128
+ }
1129
+
1130
+ function isReviewingToolbarModeOption(node, enabled) {
1131
+ const mode = enabled ? 'reviewing' : 'editing';
1132
+ const values = [node.value, node.getAttribute('value'), node.getAttribute('data-value'),
1133
+ node.getAttribute('label'), node.getAttribute('aria-label'), ...String(node.className || '').split(/\s+/)];
1134
+ if (values.some(value => normalizeReviewingSignalText(value).toLowerCase() === mode)) return true;
1135
+ // Read a label separately from icon text and the option's explanatory subtitle.
1136
+ return [node, ...node.querySelectorAll('*')].some(label => {
1137
+ if (label.closest('[aria-hidden="true"]')) return false;
1138
+ const directText = Array.from(label.childNodes).filter(child => child.nodeType === 3)
1139
+ .map(child => child.textContent).join(' ');
1140
+ return [label.textContent, directText].some(value => normalizeReviewingSignalText(value).toLowerCase() === mode);
1141
+ });
1142
+ }
1143
+
1096
1144
  function findReviewingModeOption(enabled, options = {}) {
1145
+ const toolbarControl = findReviewingToolbarControl();
1146
+ if (toolbarControl) {
1147
+ const menu = findReviewingToolbarMenu(toolbarControl);
1148
+ return Array.from(menu?.querySelectorAll(
1149
+ '[role="menuitem"], [role="menuitemradio"], [role="option"], .dropdown-item, button, a, option'
1150
+ ) || []).find(node => isReviewingModeNodeAvailable(node) && isReviewingToolbarModeOption(node, enabled)) || null;
1151
+ }
1097
1152
  const candidates = collectReviewingModeOptionNodes()
1098
1153
  .filter(node => isReviewingModeOptionCandidate(node, enabled, options))
1099
1154
  .sort((left, right) => scoreReviewingModeOption(right, enabled) - scoreReviewingModeOption(left, enabled));
@@ -1129,7 +1184,7 @@
1129
1184
  }
1130
1185
 
1131
1186
  function isReviewingModeMenuControlCandidate(node) {
1132
- if (!node || node.disabled || node.getAttribute?.('aria-disabled') === 'true') {
1187
+ if (!isReviewingModeNodeAvailable(node) || isMenuLikeModeOption(node)) {
1133
1188
  return false;
1134
1189
  }
1135
1190
  const text = readNodeSignalText(node);
@@ -1182,7 +1237,7 @@
1182
1237
  }
1183
1238
 
1184
1239
  function isReviewingModeOptionCandidate(node, enabled, options = {}) {
1185
- if (!node || node.disabled || node.getAttribute?.('aria-disabled') === 'true') {
1240
+ if (!isReviewingModeNodeAvailable(node)) {
1186
1241
  return false;
1187
1242
  }
1188
1243
  if (options.requireMenuOption && !isMenuLikeModeOption(node)) {
@@ -1228,10 +1283,10 @@
1228
1283
  && !/\bsyntax highlighting\b/i.test(text);
1229
1284
  }
1230
1285
 
1231
- function findReviewingActivationControl() {
1286
+ function findReviewingActivationControl(enabled = true) {
1232
1287
  const nodes = collectReviewingControlNodes();
1233
1288
  const candidates = nodes
1234
- .filter(node => isReviewingActivationCandidate(node))
1289
+ .filter(node => isReviewingActivationCandidate(node, enabled))
1235
1290
  .sort((left, right) => scoreReviewingControl(right) - scoreReviewingControl(left));
1236
1291
  return candidates[0] || null;
1237
1292
  }
@@ -1255,10 +1310,12 @@
1255
1310
  ]).slice(0, 1500);
1256
1311
  }
1257
1312
 
1258
- function isReviewingActivationCandidate(node) {
1259
- if (!node || node.disabled || node.getAttribute?.('aria-disabled') === 'true') {
1313
+ function isReviewingActivationCandidate(node, enabled) {
1314
+ if (!isReviewingModeNodeAvailable(node) || isMenuLikeModeOption(node)) {
1260
1315
  return false;
1261
1316
  }
1317
+ // A standalone Reviewing command enables tracking; it cannot open Editing.
1318
+ if (!enabled && !node.getAttribute?.('aria-haspopup') && !node.hasAttribute?.('aria-expanded')) return false;
1262
1319
  const text = readNodeSignalText(node);
1263
1320
  return /\breviewing\b|\btrack(?:ed)? changes?\b|\bsuggest(?:ing|ions?)\b/i.test(text)
1264
1321
  && !/\breview panel\b/i.test(text);
@@ -1,10 +1,17 @@
1
1
  (function initPopup() {
2
2
  'use strict';
3
3
 
4
+ // Keep only copy produced by this popup eligible for a language refresh.
5
+ const popupCopyPairs = new Map();
6
+ let newSiteCopyNodes = [];
7
+
4
8
  const popupScriptUrl = document.currentScript?.src || chrome.runtime.getURL('src/popup.js');
5
9
  ensureMinimalPopupShell();
6
10
  const CodexOverleafCompatibility = window.CodexOverleafCompatibility;
7
- const DEFAULT_INSTALL_COMMAND = CodexOverleafCompatibility?.buildInstallCommand?.(
11
+ const isManagedInstall = chrome.runtime.getManifest()?.background?.service_worker === 'bootstrap/background.js'
12
+ || window.__CODEX_OVERLEAF_RUNTIME_PREFIX__ === 'runtime/';
13
+ let installIdentity = { managed: isManagedInstall };
14
+ const DEFAULT_INSTALL_COMMAND = isManagedInstall ? '' : CodexOverleafCompatibility?.buildInstallCommand?.(
8
15
  undefined,
9
16
  undefined,
10
17
  getCurrentExtensionId()
@@ -71,6 +78,7 @@
71
78
  });
72
79
 
73
80
  copyInstallCommand.addEventListener('click', async () => {
81
+ if (!currentInstallCommand) return;
74
82
  copyInstallCommand.disabled = true;
75
83
  copyFeedback.hidden = true;
76
84
  try {
@@ -81,7 +89,7 @@
81
89
  copyFeedback.textContent = utx('Copy failed. Select the command above to copy it.', '复制失败,可选中上方命令复制。');
82
90
  copyFeedback.hidden = false;
83
91
  } finally {
84
- copyInstallCommand.disabled = false;
92
+ copyInstallCommand.disabled = !currentInstallCommand;
85
93
  }
86
94
  });
87
95
 
@@ -157,6 +165,7 @@
157
165
  const granted = await chrome.permissions.request({ origins: missing }).catch(() => false);
158
166
  if (granted) section.remove();
159
167
  });
168
+ newSiteCopyNodes = [title, detail, button];
160
169
  section.append(title, detail, button);
161
170
  document.getElementById('popup-shell')?.append(section);
162
171
  }
@@ -172,10 +181,10 @@
172
181
  connectionDetails.setAttribute('aria-label', utx('Version details', '版本信息'));
173
182
  connectionToggle.title = utx('Show or hide version details', '展开或收起版本信息');
174
183
  copyInstallCommand.textContent = utx('Copy install command', '复制安装命令');
184
+ [status, copyFeedback, ...newSiteCopyNodes].forEach(refreshPopupCopy);
175
185
  if (lastCompatibility) {
176
186
  setVersionStatus(lastCompatibility);
177
- if (!nativeInstall.hidden) nativeStatusMessage.textContent = getNativeStatusMessage(
178
- lastCompatibility.status, getCompatibilityClassification(lastCompatibility));
187
+ if (!nativeInstall.hidden) showNativeInstallGuide(lastCompatibility);
179
188
  }
180
189
  }
181
190
 
@@ -217,6 +226,7 @@
217
226
  }
218
227
 
219
228
  async function checkNativeHost() {
229
+ await readInstallIdentity();
220
230
  try {
221
231
  const response = await chrome.runtime.sendMessage({
222
232
  type: 'codex-overleaf/native-request',
@@ -244,11 +254,29 @@
244
254
  }
245
255
  }
246
256
 
257
+ async function readInstallIdentity() {
258
+ if (!isManagedInstall) return;
259
+ try {
260
+ const response = await fetch(chrome.runtime.getURL('.codex-overleaf-managed-extension.json'));
261
+ const marker = response.ok ? await response.json() : null;
262
+ if (marker?.managedBy === 'codex-overleaf-link' && marker.kind === 'extension'
263
+ && marker.installIdentity?.extensionId === getCurrentExtensionId()) {
264
+ installIdentity = { ...marker.installIdentity, managed: true };
265
+ }
266
+ } catch (_) { /* Older markers cannot prove the original root or browser. */ }
267
+ }
268
+
247
269
  function showNativeInstallGuide(compatibility = {}) {
248
270
  nativeInstall.hidden = false;
249
271
  currentInstallCommand = getCompatibilityUpdateCommand(compatibility);
250
272
  installCommand.textContent = currentInstallCommand;
251
- nativeStatusMessage.textContent = getNativeStatusMessage(compatibility.status, getCompatibilityClassification(compatibility));
273
+ copyInstallCommand.disabled = !currentInstallCommand;
274
+ nativeStatusMessage.textContent = !currentInstallCommand && isManagedInstall
275
+ ? utx('Repair this Managed installation using its original managed root, browser and extension ID. Rerun the original install-managed command with those parameters.',
276
+ '修复此托管安装需要原管理根目录、浏览器和扩展 ID。请使用这些原参数重新运行 install-managed 安装命令。')
277
+ : getNativeStatusMessage(compatibility.status, getCompatibilityClassification(compatibility))
278
+ + ((compatibility.native?.platform || installIdentity.platform) === 'win32' && isManagedInstall
279
+ ? utx(' Run the command in PowerShell.', ' 请在 PowerShell 中运行该命令。') : '');
252
280
  }
253
281
 
254
282
  function evaluateNativeCompatibility(response) {
@@ -303,7 +331,8 @@
303
331
  version: CodexOverleafCompatibility?.BUILD_TARGET_VERSION ||
304
332
  chrome.runtime.getManifest?.().version ||
305
333
  '',
306
- extensionId: getCurrentExtensionId()
334
+ extensionId: getCurrentExtensionId(),
335
+ ...(isManagedInstall ? { installIdentity } : {})
307
336
  };
308
337
  }
309
338
 
@@ -331,8 +360,9 @@
331
360
  return CodexOverleafCompatibility?.buildInstallCommand?.(
332
361
  compatibility.recommendedVersion || CodexOverleafCompatibility.BUILD_TARGET_VERSION,
333
362
  compatibility.native?.platform || compatibility.platform,
334
- getCurrentExtensionId()
335
- ) || compatibility.updateCommand || compatibility.installCommand || DEFAULT_INSTALL_COMMAND;
363
+ getCurrentExtensionId(),
364
+ installIdentity
365
+ ) || (isManagedInstall ? '' : compatibility.updateCommand || compatibility.installCommand || DEFAULT_INSTALL_COMMAND);
336
366
  }
337
367
 
338
368
  function getStatusIconText(classification) {
@@ -558,7 +588,7 @@ async function sendConsentAction(action) {
558
588
  const summary = document.createElement('summary');
559
589
  summary.textContent = utx('Technical details', '技术详情');
560
590
  const code = document.createElement('code');
561
- code.textContent = [state.code, state.message].filter(Boolean).join(': ');
591
+ code.textContent = [state.code, formatUpdateFailure(state)].filter(Boolean).join(': ');
562
592
  details.append(summary, code);
563
593
  card.append(details);
564
594
  }
@@ -668,12 +698,26 @@ async function sendConsentAction(action) {
668
698
  awaiting_health: [utx('Checking updated components', '正在检查更新后的组件'), latest, utx('Restarting and checking both components', '正在重启并检查两个组件')],
669
699
  committed: [utx('Update complete', '更新完成'), latest || current, utx('Extension and Native Host are healthy.', '扩展与 Native Host 均运行正常。')],
670
700
  rolled_back: [utx('Previous version restored', '已恢复上一版本'), current, utx('Health check failed, so the previous coordinated pair was restored.', '健康检查失败,已恢复上一组协调版本。')],
671
- failed: [utx('Update could not continue', '更新无法继续'), latest, state.message || utx('Check the details and retry.', '请查看详情后重试。')]
701
+ failed: [utx('Update could not continue', '更新无法继续'), latest, formatUpdateFailure(state) || utx('Check the details and retry.', '请查看详情后重试。')]
672
702
  };
673
703
  const value = map[state.state] || map.idle;
674
704
  return { title: value[0], version: value[1], detail: value[2], phase: value[2] };
675
705
  }
676
706
 
707
+ function formatUpdateFailure(state = {}) {
708
+ const code = String(state.code || '');
709
+ if (code === 'update_recovery_pending') return utx('The update result is being checked. Recovery has not been confirmed.', '正在确认更新结果,恢复状态尚未确认。');
710
+ if (['update_bootstrap_health_timeout', 'update_bootstrap_reload_failed'].includes(code)) return utx('The updated startup components did not finish restarting and confirming. Check recovery before retrying.', '更新后的启动组件未能完成重启和确认,请先检查恢复状态。');
711
+ if (['update_bootstrap_source_mismatch', 'update_bootstrap_source_version_mismatch'].includes(code)) return utx('The installed startup files do not match a supported update source. Restore the previous version in Settings, or reinstall once.', '已安装的启动文件与支持的升级来源不一致。请在设置中恢复到上一版本,或重新安装一次。');
712
+ if (['update_bootstrap_record_missing', 'update_bootstrap_record_invalid', 'update_bootstrap_backup_mismatch', 'update_bootstrap_already_restoring'].includes(code)) return utx('The startup recovery material could not be confirmed. The update result remains unconfirmed.', '尚未确认启动组件的恢复材料有效,更新结果仍待确认。');
713
+ if (['update_bootstrap_confirmation_pending', 'update_bootstrap_native_restart_required'].includes(code)) return utx('Waiting for the updated startup components and Native Host to finish restarting and confirming.', '正在等待更新后的启动组件和本机服务完成重启及确认。');
714
+ if (code === 'update_bootstrap_monitor_unavailable') return utx('The update recovery monitor could not start. Check recovery before retrying.', '更新恢复监护程序未能启动,请先检查恢复状态。');
715
+ if (['update_bootstrap_transaction_mismatch', 'update_bootstrap_transaction_invalid'].includes(code)) return utx('The startup update transaction could not be matched. Check recovery before retrying.', '未能匹配启动组件的更新事务,请先检查恢复状态。');
716
+ if (code.startsWith('update_bootstrap_')) return utx('The startup update package or installed files could not be verified. Check recovery before retrying.', '无法验证启动组件更新包或已安装文件,请先检查恢复状态。');
717
+ if (['update_health_failed', 'update_request_failed', 'update_failed'].includes(code)) return utx('The updated components could not be confirmed. Check recovery before retrying.', '尚未确认更新后的组件正常,请先检查恢复状态。');
718
+ return state.message || '';
719
+ }
720
+
677
721
  function formatBlocker(blocker) {
678
722
  const labels = {
679
723
  unsaved: utx('Overleaf has not confirmed that this document is saved.', 'Overleaf 尚未确认当前文档已保存。'),
@@ -688,7 +732,15 @@ async function sendConsentAction(action) {
688
732
  return labels[blocker] || utx('Waiting until every Overleaf tab is saved and idle.', '正在等待所有 Overleaf 标签页完成保存并进入空闲状态。');
689
733
  }
690
734
 
735
+ function refreshPopupCopy(element) {
736
+ const pair = popupCopyPairs.get(element?.textContent);
737
+ if (pair) element.textContent = utx(...pair);
738
+ }
739
+
691
740
  function utx(english, chinese) {
741
+ const pair = [english, chinese];
742
+ popupCopyPairs.set(english, pair);
743
+ popupCopyPairs.set(chinese, pair);
692
744
  const locale = document.documentElement?.lang || 'en';
693
745
  return /^zh\b/i.test(locale) ? chinese : english;
694
746
  }