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
@@ -8,13 +8,21 @@
8
8
  'use strict';
9
9
 
10
10
  const COMPILE_TEMPLATE_TTL_MS = 5 * 60 * 1000;
11
+ const MAX_RECENT_COMPILE_CAPTURES = 8;
12
+ const COMPILE_LOG_MAX_AGE_MS = 30000;
11
13
  const GLOBAL_STATE_KEY = '__codexOverleafCompileBridgeState';
14
+ const FETCH_WRAPPER_REVISION = '2026-10-17-compile-capture-v4';
12
15
 
13
16
  function create(deps = {}) {
14
17
  const pageWindow = deps.window || window;
15
18
  const pageDocument = deps.document || document;
16
19
  let installed = false;
17
- const state = pageWindow[GLOBAL_STATE_KEY] || {
20
+ const previousState = pageWindow[GLOBAL_STATE_KEY];
21
+ // A buried wrapper still closes over its old state. Isolate its counters and
22
+ // late responses while keeping the other script's fetch chain intact.
23
+ const replaceWrapper = previousState?.wrappedFetch
24
+ && (previousState.wrapperRevision !== FETCH_WRAPPER_REVISION || pageWindow.fetch !== previousState.wrappedFetch);
25
+ const state = replaceWrapper ? { ...previousState, inputListenerInstalled: false } : previousState || {
18
26
  capturedRequestTemplate: null,
19
27
  lastCompileResponse: null,
20
28
  lastCompileAt: 0,
@@ -24,6 +32,15 @@
24
32
  wrappedFetch: null,
25
33
  inputListenerInstalled: false
26
34
  };
35
+ if (replaceWrapper && previousState.inputListener) {
36
+ pageDocument.removeEventListener?.('input', previousState.inputListener, true);
37
+ }
38
+ state.compileRequestSequence ||= 0;
39
+ // Keep completed captures isolated from a detached older wrapper.
40
+ state.recentCompileCaptures = (state.recentCompileCaptures || []).slice(0, MAX_RECENT_COMPILE_CAPTURES);
41
+ // Busy tokens alone survive reinjection: old requests can release their own
42
+ // tokens without advancing the new wrapper's capture counters or results.
43
+ state.inFlightCompileRequests ||= new Set();
27
44
  pageWindow[GLOBAL_STATE_KEY] = state;
28
45
 
29
46
  function install() {
@@ -34,24 +51,28 @@
34
51
  interceptCompileRequests();
35
52
  if (typeof pageDocument.addEventListener === 'function' && state.inputListenerInstalled !== true) {
36
53
  pageDocument.addEventListener('input', markSourceEditFromInput, true);
54
+ state.inputListener = markSourceEditFromInput;
37
55
  state.inputListenerInstalled = true;
38
56
  }
39
57
  }
40
58
 
41
59
  function interceptCompileRequests() {
42
- if (state.wrappedFetch && pageWindow.fetch === state.wrappedFetch) {
60
+ if (state.wrapperRevision === FETCH_WRAPPER_REVISION && state.wrappedFetch && pageWindow.fetch === state.wrappedFetch) {
43
61
  return;
44
62
  }
45
63
  if (!state.originalFetch || pageWindow.fetch !== state.wrappedFetch) {
46
64
  state.originalFetch = pageWindow.fetch;
47
65
  }
48
66
  const originalFetch = state.originalFetch;
67
+ state.wrapperRevision = FETCH_WRAPPER_REVISION;
49
68
  state.wrappedFetch = async function codexOverleafCompileFetchWrapper(...args) {
50
69
  const [resource, init] = args;
51
70
  const url = typeof resource === 'string' ? resource : (resource?.url || '');
52
- const method = (init?.method || 'GET').toUpperCase();
53
- const isCompileRequest = /\/project\/[^/]+\/compile\b/.test(url) && method === 'POST';
71
+ const method = (init?.method || resource?.method || 'GET').toUpperCase();
72
+ const isCompileRequest = /\/project\/[^/]+\/compile(?:[?#]|$)/.test(url) && method === 'POST';
73
+ const captureId = isCompileRequest ? ++state.compileRequestSequence : 0;
54
74
  const compileSourceChangeTimestamp = isCompileRequest ? state.lastKnownSourceEditTimestamp : 0;
75
+ const pendingRequest = isCompileRequest ? {} : null;
55
76
 
56
77
  if (isCompileRequest) {
57
78
  state.capturedRequestTemplate = {
@@ -67,29 +88,46 @@
67
88
  };
68
89
  }
69
90
 
70
- const response = await originalFetch.apply(this, args);
71
-
72
- if (isCompileRequest && response.ok) {
73
- try {
74
- const clone = response.clone();
75
- const json = await clone.json();
76
- const CompileAdapter = pageWindow.CodexOverleafCompileAdapter;
77
- if (CompileAdapter) {
78
- state.lastCompileResponse = CompileAdapter.parseCompileResponse(json);
79
- state.lastCompileAt = Date.now();
80
- state.lastCompileSourceChangeTimestamp = compileSourceChangeTimestamp;
81
- }
82
- } catch (_e) { /* ignore parse errors */ }
91
+ if (pendingRequest) state.inFlightCompileRequests.add(pendingRequest);
92
+ try {
93
+ const response = await originalFetch.apply(this, args);
94
+
95
+ if (isCompileRequest && response.ok) {
96
+ try {
97
+ const clone = response.clone();
98
+ const json = await clone.json();
99
+ const CompileAdapter = pageWindow.CodexOverleafCompileAdapter;
100
+ if (CompileAdapter) {
101
+ const capture = {
102
+ compile: { ...CompileAdapter.parseCompileResponse(json), captureId },
103
+ compiledAt: Date.now(),
104
+ sourceChangeTimestamp: compileSourceChangeTimestamp
105
+ };
106
+ state.recentCompileCaptures = [capture, ...state.recentCompileCaptures]
107
+ .sort((a, b) => b.compile.captureId - a.compile.captureId).slice(0, MAX_RECENT_COMPILE_CAPTURES);
108
+ // Preserve request order even if an older response finishes last.
109
+ if (captureId > (state.lastCompileResponse?.captureId || 0)) {
110
+ state.lastCompileResponse = capture.compile;
111
+ state.lastCompileAt = capture.compiledAt;
112
+ state.lastCompileSourceChangeTimestamp = capture.sourceChangeTimestamp;
113
+ }
114
+ }
115
+ } catch (_e) { /* ignore parse errors */ }
116
+ }
117
+ return response;
118
+ } finally {
119
+ if (pendingRequest) state.inFlightCompileRequests.delete(pendingRequest);
83
120
  }
84
-
85
- return response;
86
121
  };
87
122
  pageWindow.fetch = state.wrappedFetch;
88
123
  }
89
124
 
90
125
  async function triggerCompile(params = {}) {
126
+ const sourceAtTrigger = state.lastKnownSourceEditTimestamp;
91
127
  const saveResult = await waitForCompileSaveState(params);
92
- if (params.requireVerifiedSave !== false && saveResult.state !== 'verified_saved') {
128
+ // Current Overleaf shows no save indicator while idle; a quiet editor is
129
+ // the same weaker evidence the update-idle check accepts.
130
+ if (params.requireVerifiedSave !== false && !['verified_saved', 'verified_quiet'].includes(saveResult.state)) {
93
131
  return {
94
132
  ok: false,
95
133
  reason: saveResult.reason || 'Overleaf save state was not verified.',
@@ -98,74 +136,29 @@
98
136
  };
99
137
  }
100
138
  const saveMetadata = buildSaveStateMetadata(saveResult);
101
- const sourceChangeTimestamp = state.lastKnownSourceEditTimestamp;
102
- if (params.preferUiClick === true) {
103
- const clicked = clickOverleafCompileButton();
104
- if (clicked) {
105
- const captured = await waitForCapturedCompile(params.captureTimeoutMs || 60000);
106
- if (captured.ok) {
107
- return {
108
- ok: true,
109
- compile: state.lastCompileResponse,
110
- ...saveMetadata,
111
- triggeredBy: 'overleaf-button'
112
- };
113
- }
114
- return {
115
- ok: true,
116
- compile: { ok: true, status: 'triggered' },
117
- resultCaptured: false,
118
- reason: captured.reason,
119
- ...saveMetadata,
120
- triggeredBy: 'overleaf-button'
121
- };
122
- }
123
- }
139
+ const startCaptureId = state.compileRequestSequence;
124
140
  const template = state.capturedRequestTemplate;
125
- if (!template) {
126
- const clicked = clickOverleafCompileButton();
127
- if (!clicked) {
128
- return {
129
- ok: false,
130
- reason: 'No Overleaf Recompile button or compile request template is available.',
131
- ...saveMetadata
132
- };
133
- }
134
- const captured = await waitForCapturedCompile(params.captureTimeoutMs || 60000);
135
- if (!captured.ok) {
136
- return {
137
- ...captured,
138
- ...saveMetadata
139
- };
140
- }
141
+ const button = findOverleafCompileButton();
142
+ // Busy UI or observed requests forbid the template fallback too. Waiting
143
+ // can reuse the in-flight result or click the main button once it is idle.
144
+ if (isCompileBusy(button) || (button && (params.preferUiClick === true || !isFreshCapturedRequestTemplate(template)))) {
145
+ const captured = await waitForCapturedCompile(params.captureTimeoutMs || 60000, sourceAtTrigger);
141
146
  return {
142
147
  ok: true,
143
- compile: state.lastCompileResponse,
148
+ compile: captured.compile || { ok: true, status: 'triggered' },
149
+ resultCaptured: captured.ok,
150
+ reason: captured.reason || '',
144
151
  ...saveMetadata,
145
- triggeredBy: 'overleaf-button'
152
+ ...(captured.clicked ? { triggeredBy: 'overleaf-button' } : {})
146
153
  };
147
154
  }
148
- if (!isFreshCapturedRequestTemplate(template)) {
149
- const clicked = clickOverleafCompileButton();
150
- if (!clicked) {
151
- return {
152
- ok: false,
153
- reason: 'Stored compile request template expired. Please click Overleaf Recompile once, then retry.',
154
- ...saveMetadata
155
- };
156
- }
157
- const captured = await waitForCapturedCompile(params.captureTimeoutMs || 60000);
158
- if (!captured.ok) {
159
- return {
160
- ...captured,
161
- ...saveMetadata
162
- };
163
- }
155
+ if (!template || !isFreshCapturedRequestTemplate(template)) {
164
156
  return {
165
- ok: true,
166
- compile: state.lastCompileResponse,
167
- ...saveMetadata,
168
- triggeredBy: 'overleaf-button'
157
+ ok: false,
158
+ reason: template
159
+ ? 'Stored compile request template expired. Please click Overleaf Recompile once, then retry.'
160
+ : 'No Overleaf Recompile button or compile request template is available.',
161
+ ...saveMetadata
169
162
  };
170
163
  }
171
164
 
@@ -183,17 +176,12 @@
183
176
  ...saveMetadata
184
177
  };
185
178
  }
186
- const json = await response.json();
187
- const CompileAdapter = pageWindow.CodexOverleafCompileAdapter;
188
- const parsed = CompileAdapter
189
- ? CompileAdapter.parseCompileResponse(json)
190
- : { ok: false, reason: 'CompileAdapter not available' };
191
- state.lastCompileResponse = parsed;
192
- state.lastCompileAt = Date.now();
193
- state.lastCompileSourceChangeTimestamp = sourceChangeTimestamp;
179
+ const captured = findEligibleCapture(sourceAtTrigger, startCaptureId)?.compile;
194
180
  return {
195
181
  ok: true,
196
- compile: parsed,
182
+ compile: captured || { ok: false, status: 'triggered' },
183
+ resultCaptured: Boolean(captured),
184
+ reason: captured ? '' : 'compile_capture_missing',
197
185
  ...saveMetadata
198
186
  };
199
187
  } catch (error) {
@@ -215,7 +203,9 @@
215
203
  }
216
204
  try {
217
205
  return normalizeSaveStateResult(
218
- await deps.waitForSaveState({ deadlineMs: resolveWaitForSaveMs(params.waitForSaveMs) })
206
+ await deps.waitForSaveState({ deadlineMs: resolveWaitForSaveMs(params.waitForSaveMs),
207
+ // Only a caller that requires save evidence may settle for a quiet editor.
208
+ allowQuietEditorFallback: params.requireVerifiedSave !== false, quietFallbackMs: 1000 })
219
209
  );
220
210
  } catch (error) {
221
211
  return {
@@ -269,18 +259,16 @@
269
259
  return { ok: false, reason: 'CompileAdapter not loaded' };
270
260
  }
271
261
 
272
- const maxAgeMs = params.maxAgeMs || 30000;
273
- const compileLogFresh = CompileAdapter.isCompileLogFresh(
274
- { sourceChangeTimestamp: state.lastCompileSourceChangeTimestamp },
275
- state.lastKnownSourceEditTimestamp
276
- );
277
- const needsFreshCompile = !state.lastCompileResponse?.ok
278
- || (Date.now() - state.lastCompileAt > maxAgeMs)
279
- || !compileLogFresh;
262
+ const maxAgeMs = params.maxAgeMs ?? COMPILE_LOG_MAX_AGE_MS;
263
+ let capture = params.captureId != null
264
+ ? state.recentCompileCaptures.find(item => item.compile.captureId === params.captureId)
265
+ : state.recentCompileCaptures[0];
266
+ const needsFreshCompile = !capture?.compile.ok || Boolean(captureFreshnessReason(capture, maxAgeMs));
280
267
 
281
- if (needsFreshCompile && params.triggerIfStale !== false) {
268
+ // A named capture must never silently switch to another compile's log.
269
+ if (needsFreshCompile && params.captureId == null && params.triggerIfStale !== false) {
282
270
  const compileResult = await triggerCompile({ waitForSaveMs: resolveWaitForSaveMs(params.waitForSaveMs) });
283
- if (!compileResult.ok) {
271
+ if (!compileResult.ok || !compileResult.resultCaptured) {
284
272
  return {
285
273
  ok: false,
286
274
  reason: `Could not get fresh compile: ${compileResult.reason}`,
@@ -288,38 +276,70 @@
288
276
  saveState: compileResult.saveState
289
277
  };
290
278
  }
279
+ capture = state.recentCompileCaptures.find(item => item.compile.captureId === compileResult.compile?.captureId);
291
280
  }
292
281
 
293
- const compiled = state.lastCompileResponse;
282
+ if (!capture) return { ok: false, reason: 'compile_capture_missing' };
283
+ const { compile: compiled, compiledAt } = capture;
284
+ const freshnessReason = captureFreshnessReason(capture, maxAgeMs);
285
+ if (freshnessReason) return { ok: false, captureId: compiled.captureId, fresh: false, reason: freshnessReason };
294
286
  if (!compiled?.ok || !compiled.logUrl) {
295
- return { ok: false, reason: 'No compile log URL available' };
287
+ return { ok: false, reason: compiled?.reason || 'No compile log URL available' };
296
288
  }
297
289
 
290
+ const candidates = [];
291
+ if (compiled.build && compiled.pdfDownloadDomain) {
292
+ // pdfDownloadDomain already includes Overleaf's outputUrlPrefix.
293
+ candidates.push({ route: 'download', url: `${compiled.pdfDownloadDomain}${compiled.logUrl}` });
294
+ }
298
295
  try {
299
- const logUrl = compiled.logUrl.startsWith('/')
300
- ? `${pageWindow.location.origin}${compiled.logUrl}`
301
- : compiled.logUrl;
302
- const response = await pageWindow.fetch(logUrl, { credentials: 'same-origin' });
303
- if (!response.ok) {
304
- return { ok: false, reason: `Failed to fetch compile log: ${response.status}` };
296
+ const url = new URL(compiled.logUrl, pageWindow.location.origin);
297
+ if (url.origin === pageWindow.location.origin) {
298
+ if (compiled.clsiServerId) url.searchParams.set('clsiserverid', compiled.clsiServerId);
299
+ if (!candidates.some(candidate => candidate.url === url.href)) candidates.push({ route: 'origin', url: url.href });
300
+ }
301
+ } catch (_error) { /* invalid log URL is reported below */ }
302
+ const failures = [];
303
+ for (const { route, url } of candidates) {
304
+ try {
305
+ const credentials = new URL(url).origin === pageWindow.location.origin ? 'same-origin' : 'omit';
306
+ const response = await pageWindow.fetch(url, { credentials, cache: 'no-store' });
307
+ if (!response.ok) {
308
+ failures.push(`${route} HTTP ${response.status}`);
309
+ continue;
310
+ }
311
+ const logContent = await response.text();
312
+ const parsed = CompileAdapter.parseLogErrors(logContent);
313
+ const reason = captureFreshnessReason(capture, maxAgeMs);
314
+ return {
315
+ ok: logContent.trim().length > 0,
316
+ captureId: compiled.captureId,
317
+ log: CompileAdapter.truncateLogForContext(logContent),
318
+ errors: parsed.errors,
319
+ warnings: parsed.warnings,
320
+ compiledAt,
321
+ fresh: !reason,
322
+ reason: reason || (logContent.trim() ? '' : 'compile_log_empty')
323
+ };
324
+ } catch (_error) {
325
+ failures.push(`${route} network error`);
305
326
  }
306
- const logContent = await response.text();
307
- const truncated = CompileAdapter.truncateLogForContext(logContent);
308
- const parsed = CompileAdapter.parseLogErrors(logContent);
309
- return {
310
- ok: true,
311
- log: truncated,
312
- errors: parsed.errors,
313
- warnings: parsed.warnings,
314
- compiledAt: state.lastCompileAt,
315
- fresh: CompileAdapter.isCompileLogFresh(
316
- { sourceChangeTimestamp: state.lastCompileSourceChangeTimestamp },
317
- state.lastKnownSourceEditTimestamp
318
- )
319
- };
320
- } catch (error) {
321
- return { ok: false, reason: `Log fetch error: ${error.message}` };
322
327
  }
328
+ const reason = captureFreshnessReason(capture, maxAgeMs);
329
+ return { ok: false, captureId: compiled.captureId, fresh: !reason,
330
+ reason: reason || (failures.length ? `compile_log_fetch: ${failures.join('; ')}` : 'compile_log_url_invalid') };
331
+ }
332
+
333
+ function captureFreshnessReason(capture, maxAgeMs = COMPILE_LOG_MAX_AGE_MS) {
334
+ if (!capture) return 'compile_capture_missing';
335
+ if (!(capture.sourceChangeTimestamp >= state.lastKnownSourceEditTimestamp)) return 'compile_log_stale_source';
336
+ if (!(Date.now() - capture.compiledAt <= maxAgeMs)) return 'compile_log_expired';
337
+ return '';
338
+ }
339
+
340
+ function findEligibleCapture(sourceAtTrigger, afterCaptureId = 0) {
341
+ return state.recentCompileCaptures.find(capture => capture.compile.captureId > afterCaptureId
342
+ && capture.sourceChangeTimestamp >= sourceAtTrigger && !captureFreshnessReason(capture));
323
343
  }
324
344
 
325
345
  function getCompileState() {
@@ -341,7 +361,7 @@
341
361
  }
342
362
 
343
363
  function markSourceEdited(timestamp = Date.now()) {
344
- state.lastKnownSourceEditTimestamp = timestamp;
364
+ state.lastKnownSourceEditTimestamp = Math.max(timestamp, state.lastKnownSourceEditTimestamp + 1);
345
365
  }
346
366
 
347
367
  function markSourceEditFromInput(event) {
@@ -359,56 +379,50 @@
359
379
  markSourceEdited();
360
380
  }
361
381
 
362
- function clickOverleafCompileButton() {
363
- const candidates = findOverleafCompileButtons();
364
- for (const candidate of candidates) {
365
- if (isDisabled(candidate)) {
366
- continue;
367
- }
368
- if (typeof candidate.click === 'function') {
369
- candidate.click();
382
+ function clickOverleafCompileButton(button) {
383
+ if (!button || isCompileBusy(button)) return false;
384
+ if (typeof button.click === 'function') {
385
+ button.click();
386
+ return true;
387
+ }
388
+ if (typeof button.dispatchEvent === 'function') {
389
+ const EventCtor = pageWindow.MouseEvent || pageWindow.Event;
390
+ if (EventCtor) {
391
+ button.dispatchEvent(new EventCtor('click', { bubbles: true, cancelable: true }));
370
392
  return true;
371
393
  }
372
- if (typeof candidate.dispatchEvent === 'function') {
373
- const EventCtor = pageWindow.MouseEvent || pageWindow.Event;
374
- if (EventCtor) {
375
- candidate.dispatchEvent(new EventCtor('click', { bubbles: true, cancelable: true }));
376
- return true;
377
- }
378
- }
379
394
  }
380
395
  return false;
381
396
  }
382
397
 
383
- function findOverleafCompileButtons() {
384
- if (typeof pageDocument.querySelectorAll !== 'function') {
385
- return [];
386
- }
387
- const selectors = [
388
- 'button',
389
- '[role="button"]',
390
- '[aria-label*="compile" i]',
391
- '[title*="compile" i]',
392
- '[data-testid*="compile" i]'
393
- ];
394
- const nodes = [];
395
- for (const selector of selectors) {
396
- try {
397
- nodes.push(...Array.from(pageDocument.querySelectorAll(selector)));
398
- } catch (_e) { /* ignore unsupported selectors in tests/older browsers */ }
399
- }
400
- return uniqueNodes(nodes).filter(isCompileButton);
398
+ function findOverleafCompileButton() {
399
+ if (typeof pageDocument.querySelectorAll !== 'function') return null;
400
+ const nodes = Array.from(pageDocument.querySelectorAll('button, [role="button"]')).filter(isCompileControl);
401
+ // Keep the main button selected while busy; never fall through to the
402
+ // enabled split toggle, menu entries or another label containing "compile".
403
+ return nodes.find(node => node.classList?.contains('compile-button'))
404
+ || nodes.find(node => /^(?:recompile|compile|重新编译|编译)$/i.test(compileButtonLabel(node))) || null;
401
405
  }
402
406
 
403
- function isCompileButton(node) {
404
- const text = [
405
- node.textContent,
406
- node.innerText,
407
- node.getAttribute?.('aria-label'),
408
- node.getAttribute?.('title'),
409
- node.getAttribute?.('data-testid')
410
- ].filter(Boolean).join(' ').toLowerCase();
411
- return /\b(recompile|compile)\b/.test(text) || /重新编译|编译/.test(text);
407
+ function isCompileControl(node) {
408
+ const extensionRoot = node.closest?.('#codex-overleaf-panel, [data-codex-overleaf-root], [id^="codex-"], [class^="codex-"], [class*=" codex-"]');
409
+ // The extension's mounted-state classes on html/body are not UI roots.
410
+ if (extensionRoot && !['HTML', 'BODY'].includes(extensionRoot.tagName)) return false;
411
+ if (node.closest?.('.compile-dropdown-toggle, #pdf-recompile-dropdown, .dropdown-menu, [role="menu"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [hidden], [inert], [aria-hidden="true"]')) return false;
412
+ const popup = node.getAttribute?.('aria-haspopup');
413
+ if ((popup && popup !== 'false') || node.getAttribute?.('data-toggle') === 'dropdown'
414
+ || node.getAttribute?.('data-bs-toggle') === 'dropdown') return false;
415
+ return !/^(?:stop\b|停止编译|终止编译)/i.test(compileButtonLabel(node));
416
+ }
417
+
418
+ function compileButtonLabel(node) {
419
+ return [node?.textContent, node?.innerText, node?.getAttribute?.('aria-label'), node?.getAttribute?.('title')]
420
+ .map(value => String(value || '').replace(/\s+/g, ' ').trim()).find(Boolean) || '';
421
+ }
422
+
423
+ function isCompileBusy(button) {
424
+ return state.inFlightCompileRequests.size > 0 || Boolean(button && (isDisabled(button)
425
+ || /^(?:compiling|正在编译|编译中)\s*[.。…]*$/i.test(compileButtonLabel(button))));
412
426
  }
413
427
 
414
428
  function isDisabled(node) {
@@ -417,16 +431,19 @@
417
431
  || node.getAttribute?.('aria-disabled') === 'true');
418
432
  }
419
433
 
420
- async function waitForCapturedCompile(timeoutMs) {
421
- const startCompileAt = state.lastCompileAt;
434
+ async function waitForCapturedCompile(timeoutMs, sourceAtTrigger) {
422
435
  const deadline = Date.now() + timeoutMs;
423
- while (Date.now() < deadline) {
424
- if (state.lastCompileAt > startCompileAt && state.lastCompileResponse) {
425
- return { ok: true };
426
- }
427
- await delay(100);
436
+ let clicked = false;
437
+ while (true) {
438
+ const capture = findEligibleCapture(sourceAtTrigger);
439
+ if (capture) return { ok: true, compile: capture.compile, clicked };
440
+ const remainingMs = deadline - Date.now();
441
+ if (remainingMs <= 0) break;
442
+ if (!clicked) clicked = clickOverleafCompileButton(findOverleafCompileButton());
443
+ await delay(Math.min(100, remainingMs));
428
444
  }
429
- return { ok: false, reason: 'Clicked Overleaf Recompile, but no compile result was captured.' };
445
+ return { ok: false, clicked, reason: clicked
446
+ ? 'Clicked Overleaf Recompile, but no compile result was captured.' : 'compile_capture_missing' };
430
447
  }
431
448
 
432
449
  return {
@@ -448,10 +465,6 @@
448
465
  return new Promise(resolve => setTimeout(resolve, ms));
449
466
  }
450
467
 
451
- function uniqueNodes(nodes) {
452
- return Array.from(new Set(nodes.filter(Boolean)));
453
- }
454
-
455
468
  return {
456
469
  COMPILE_TEMPLATE_TTL_MS,
457
470
  create,