codex-overleaf-link 2.5.1 → 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 (93) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +78 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +11 -2
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +920 -276
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsPanel.js +18 -0
  14. package/extension/src/content/generated/content.bundle.js +4496 -924
  15. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  16. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  17. package/extension/src/content/localSkillsPanel.js +2 -2
  18. package/extension/src/content/markdownDomRenderer.js +5 -3
  19. package/extension/src/content/markdownText.js +42 -5
  20. package/extension/src/content/modelPicker.js +1 -0
  21. package/extension/src/content/nativeCompatibilityController.js +29 -10
  22. package/extension/src/content/otWarmMirror.js +1 -1
  23. package/extension/src/content/pageBridgeClient.js +2 -2
  24. package/extension/src/content/panelMaintenance.js +6 -2
  25. package/extension/src/content/panelRenderer.js +51 -27
  26. package/extension/src/content/pendingInputView.js +1 -1
  27. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  28. package/extension/src/content/providerSettingsDialog.js +2 -1
  29. package/extension/src/content/recentProjects.js +4 -3
  30. package/extension/src/content/runActivitySummary.js +122 -14
  31. package/extension/src/content/runCompletionMeta.js +194 -6
  32. package/extension/src/content/runController.js +2 -1
  33. package/extension/src/content/runFailureNotice.js +8 -1
  34. package/extension/src/content/runPresence.js +18 -8
  35. package/extension/src/content/runQueueScheduler.js +9 -5
  36. package/extension/src/content/runRecoveryActions.js +2 -1
  37. package/extension/src/content/runResultActions.js +22 -20
  38. package/extension/src/content/runReviewController.js +94 -35
  39. package/extension/src/content/runTimelineView.js +224 -25
  40. package/extension/src/content/scopedPersistencePanelState.js +92 -23
  41. package/extension/src/content/sensitivePreflight.js +14 -1
  42. package/extension/src/content/sessionManager.js +49 -3
  43. package/extension/src/content/sessionMenuView.js +54 -18
  44. package/extension/src/content/sessionPersistence.js +25 -5
  45. package/extension/src/content/settingsPanel.js +15 -2
  46. package/extension/src/content/settingsWorkbench.js +127 -89
  47. package/extension/src/content/sharedSessionViewSync.js +4 -3
  48. package/extension/src/content/subagentActivityView.js +23 -7
  49. package/extension/src/content/updateNotice.js +60 -6
  50. package/extension/src/content/writebackOrchestrator.js +35 -22
  51. package/extension/src/page/compileBridge.js +193 -180
  52. package/extension/src/page/overleafCapabilities.js +4 -1
  53. package/extension/src/page/textCoordinates.js +17 -1
  54. package/extension/src/page/textFileCreator.js +9 -1
  55. package/extension/src/page/writebackRouter.js +24 -77
  56. package/extension/src/pageBridge.js +107 -66
  57. package/extension/src/popup.js +1 -1
  58. package/extension/src/shared/agentTranscript.js +350 -17
  59. package/extension/src/shared/auditRecords.js +2 -0
  60. package/extension/src/shared/compatibility.js +1 -1
  61. package/extension/src/shared/compileAdapter.js +58 -17
  62. package/extension/src/shared/composerAttachmentDraft.js +6 -1
  63. package/extension/src/shared/contextUsage.js +125 -0
  64. package/extension/src/shared/failureReasons.js +42 -3
  65. package/extension/src/shared/i18n.js +299 -29
  66. package/extension/src/shared/lineReferences.js +38 -31
  67. package/extension/src/shared/pageRpcContract.js +1 -1
  68. package/extension/src/shared/panelIcons.js +39 -0
  69. package/extension/src/shared/reviewing.js +130 -1
  70. package/extension/src/shared/runActivityModel.js +70 -4
  71. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  72. package/extension/src/shared/sessionState.js +63 -4
  73. package/extension/src/shared/sessionTitle.js +14 -1
  74. package/extension/src/shared/sharedSessionBridge.js +81 -13
  75. package/extension/src/shared/staleGuard.js +72 -63
  76. package/extension/src/shared/storageDb.js +35 -3
  77. package/extension/src/shared/storageMigration.js +7 -1
  78. package/extension/src/shared/storageRunActions.js +62 -8
  79. package/extension/src/shared/undoOperations.js +126 -28
  80. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  81. package/extension/src/shared/writebackSettlement.js +21 -3
  82. package/extension/styles/panel.css +1106 -490
  83. package/native-host/src/bootstrapUpdate.js +52 -16
  84. package/native-host/src/chatBridgeRequest.js +60 -6
  85. package/native-host/src/chatBridgeResponse.js +10 -1
  86. package/native-host/src/chatCompletionsBridge.js +29 -2
  87. package/native-host/src/codexPromptAssembly.js +86 -118
  88. package/native-host/src/codexSessionRunner.js +126 -17
  89. package/native-host/src/providerContextError.js +20 -0
  90. package/native-host/src/taskRunnerRuntime.js +18 -6
  91. package/native-host/src/textPatch.js +473 -377
  92. package/native-host/src/updateManager.js +120 -15
  93. package/package.json +5 -8
@@ -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,
@@ -16,6 +16,7 @@
16
16
  const checkpointWrite = hasCheckpointCapability(deps);
17
17
  const compileCapture = Boolean(deps.compileState?.capturedRequestTemplate);
18
18
  const reviewingControl = Boolean(deps.findReviewingActivationControl?.());
19
+ const upgradeModal = Boolean(deps.findUpgradeModal?.());
19
20
 
20
21
  if (!editorRead) {
21
22
  warnings.push('没有识别到当前 Overleaf 编辑器,Codex 不能读取当前文件。');
@@ -47,7 +48,9 @@
47
48
  trigger: compileCapture
48
49
  },
49
50
  reviewing: {
50
- control: reviewingControl
51
+ control: reviewingControl,
52
+ upgradeModal,
53
+ confirmed: !upgradeModal && deps.reviewingConfirmed === true
51
54
  },
52
55
  warnings
53
56
  };
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  function resolveJumpToPositionRange(params, text, filePath) {
9
- if (params.line === undefined && params.column === undefined) {
9
+ if (params.line === undefined && params.column === undefined && params.endLine === undefined) {
10
10
  return {
11
11
  ok: true,
12
12
  from: params.from,
@@ -33,6 +33,22 @@
33
33
  });
34
34
  }
35
35
 
36
+ if (params.endLine !== undefined) {
37
+ const endLineNumber = Number(params.endLine);
38
+ if (!Number.isInteger(endLineNumber) || endLineNumber < lineNumber) {
39
+ return jumpPositionOutOfRange('line_out_of_range', filePath, {
40
+ line: lineNumber,
41
+ endLine: Number.isInteger(endLineNumber) ? endLineNumber : params.endLine,
42
+ lineCount
43
+ });
44
+ }
45
+ return {
46
+ ok: true,
47
+ from: line.start,
48
+ to: lines[Math.min(endLineNumber, lineCount) - 1].end
49
+ };
50
+ }
51
+
36
52
  const lineLength = line.end - line.start;
37
53
  if (params.selectLine === true) {
38
54
  return {
@@ -108,9 +108,14 @@
108
108
  }
109
109
  function verificationFailure(error, changedDocument, phase) {
110
110
  const interrupted = ['codex_cancelled', 'aborted_project_changed'].includes(error?.code);
111
- const code = interrupted ? error.code : changedDocument ? 'file_upload_unconfirmed' : 'source_zip_unavailable';
111
+ // Only a failed server read means the project could not be checked; any
112
+ // other error before the upload came from the Overleaf file-tree controls.
113
+ const treeControls = !interrupted && !changedDocument && error?.code !== 'source_zip_unavailable';
114
+ const code = interrupted ? error.code : changedDocument ? 'file_upload_unconfirmed'
115
+ : treeControls ? 'file_tree_controls_unavailable' : 'source_zip_unavailable';
112
116
  const reason = interrupted ? error.message : changedDocument
113
117
  ? 'The file was submitted, but its saved content could not be confirmed.'
118
+ : treeControls ? 'Codex could not safely perform this file operation with the available Overleaf controls.'
114
119
  : 'The project could not be checked before uploading this file.';
115
120
  return { ok: false, code, reason, changedDocument, diagnostics: {
116
121
  verificationPhase: phase, ...(error?.diagnostics || {}), reason: error?.technicalMessage || error?.message || ''
@@ -610,6 +615,9 @@
610
615
  async function ensureWriteMode(reviewing, params = {}) {
611
616
  const ensureMode = reviewing ? deps.ensureReviewing : deps.ensureEditing;
612
617
  if (typeof ensureMode !== 'function') return { ok: false, reason: 'Editor mode controls are unavailable.' };
618
+ // An open paywall can make the underlying editor inert. Detect it before
619
+ // preparation attempts any file navigation behind that modal.
620
+ if (reviewing && deps.findUpgradeModal?.()) return ensureMode(params);
613
621
  const prepared = await prepareEditor(params);
614
622
  return prepared.ok ? ensureMode(params) : prepared;
615
623
  }