codex-overleaf-link 2.5.1 → 2.6.5

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 (111) 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 +148 -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 +46 -10
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +1114 -321
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsController.js +77 -34
  14. package/extension/src/content/diagnosticsExport.js +1 -1
  15. package/extension/src/content/diagnosticsPanel.js +147 -42
  16. package/extension/src/content/generated/content.bundle.js +5761 -1237
  17. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  18. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  19. package/extension/src/content/localSkillsPanel.js +6 -4
  20. package/extension/src/content/markdownDomRenderer.js +5 -3
  21. package/extension/src/content/markdownText.js +53 -14
  22. package/extension/src/content/modelPicker.js +29 -19
  23. package/extension/src/content/nativeCompatibilityController.js +52 -19
  24. package/extension/src/content/otWarmMirror.js +1 -1
  25. package/extension/src/content/pageBridgeClient.js +7 -7
  26. package/extension/src/content/panelMaintenance.js +10 -2
  27. package/extension/src/content/panelRenderer.js +55 -28
  28. package/extension/src/content/pendingInputView.js +1 -1
  29. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  30. package/extension/src/content/providerSettingsDialog.js +28 -19
  31. package/extension/src/content/recentProjects.js +30 -19
  32. package/extension/src/content/runActivitySummary.js +122 -14
  33. package/extension/src/content/runCompletionMeta.js +194 -6
  34. package/extension/src/content/runController.js +2 -1
  35. package/extension/src/content/runFailureNotice.js +8 -1
  36. package/extension/src/content/runPresence.js +18 -8
  37. package/extension/src/content/runQueueScheduler.js +27 -13
  38. package/extension/src/content/runRecoveryActions.js +2 -1
  39. package/extension/src/content/runResultActions.js +73 -24
  40. package/extension/src/content/runReviewController.js +102 -37
  41. package/extension/src/content/runTimelineView.js +224 -25
  42. package/extension/src/content/scopedPersistencePanelState.js +124 -46
  43. package/extension/src/content/selectionContextView.js +30 -3
  44. package/extension/src/content/sensitivePreflight.js +14 -1
  45. package/extension/src/content/sessionManager.js +49 -3
  46. package/extension/src/content/sessionMenuView.js +54 -18
  47. package/extension/src/content/sessionPersistence.js +25 -5
  48. package/extension/src/content/settingsPanel.js +15 -2
  49. package/extension/src/content/settingsWorkbench.js +127 -89
  50. package/extension/src/content/sharedSessionViewSync.js +13 -4
  51. package/extension/src/content/subagentActivityView.js +23 -7
  52. package/extension/src/content/updateNotice.js +60 -6
  53. package/extension/src/content/writebackOrchestrator.js +301 -72
  54. package/extension/src/content/writingStyleSettings.js +70 -17
  55. package/extension/src/page/compileBridge.js +193 -180
  56. package/extension/src/page/overleafCapabilities.js +4 -1
  57. package/extension/src/page/overleafEditor.js +12 -3
  58. package/extension/src/page/overleafProjectSnapshot.js +56 -3
  59. package/extension/src/page/snapshotRouter.js +75 -66
  60. package/extension/src/page/textCoordinates.js +17 -1
  61. package/extension/src/page/textFileCreator.js +9 -1
  62. package/extension/src/page/treeOperations.js +200 -19
  63. package/extension/src/page/writebackRouter.js +172 -113
  64. package/extension/src/pageBridge.js +124 -73
  65. package/extension/src/popup.js +1 -1
  66. package/extension/src/shared/agentTranscript.js +372 -18
  67. package/extension/src/shared/auditRecords.js +3 -0
  68. package/extension/src/shared/compatibility.js +1 -1
  69. package/extension/src/shared/compileAdapter.js +58 -17
  70. package/extension/src/shared/composerAttachmentDraft.js +27 -7
  71. package/extension/src/shared/contextUsage.js +125 -0
  72. package/extension/src/shared/failureReasons.js +57 -3
  73. package/extension/src/shared/i18n.js +315 -29
  74. package/extension/src/shared/lineReferences.js +38 -31
  75. package/extension/src/shared/pageRpcContract.js +1 -1
  76. package/extension/src/shared/panelIcons.js +39 -0
  77. package/extension/src/shared/projectFiles.js +2 -1
  78. package/extension/src/shared/reviewing.js +130 -1
  79. package/extension/src/shared/runActivityModel.js +70 -4
  80. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  81. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
  82. package/extension/src/shared/sessionState.js +63 -4
  83. package/extension/src/shared/sessionTitle.js +14 -1
  84. package/extension/src/shared/sharedSessionBridge.js +81 -13
  85. package/extension/src/shared/staleGuard.js +130 -63
  86. package/extension/src/shared/storageDb.js +35 -3
  87. package/extension/src/shared/storageMigration.js +111 -2
  88. package/extension/src/shared/storageRunActions.js +62 -8
  89. package/extension/src/shared/undoOperations.js +126 -28
  90. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  91. package/extension/src/shared/writebackSettlement.js +21 -3
  92. package/extension/styles/panel.css +1293 -549
  93. package/native-host/src/bootstrapUpdate.js +52 -16
  94. package/native-host/src/chatBridgeRequest.js +60 -6
  95. package/native-host/src/chatBridgeResponse.js +10 -1
  96. package/native-host/src/chatCompletionsBridge.js +29 -2
  97. package/native-host/src/codexPromptAssembly.js +86 -118
  98. package/native-host/src/codexSessionRunner.js +127 -17
  99. package/native-host/src/debugLog.js +27 -3
  100. package/native-host/src/index.js +42 -11
  101. package/native-host/src/localSkills.js +1 -1
  102. package/native-host/src/mirrorWorkspace.js +47 -3
  103. package/native-host/src/nativeEnvironment.js +27 -5
  104. package/native-host/src/providerContextError.js +20 -0
  105. package/native-host/src/providerRuntime.js +1 -0
  106. package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
  107. package/native-host/src/taskRunnerRuntime.js +28 -8
  108. package/native-host/src/textPatch.js +473 -377
  109. package/native-host/src/updateManager.js +144 -21
  110. package/native-host/src/writingStyleRuntime.js +1 -1
  111. package/package.json +5 -8
@@ -16,27 +16,70 @@
16
16
  const snapshotCache = createKeyedCache();
17
17
  const fileListCache = createKeyedCache();
18
18
 
19
+ function editorCaptureKey() {
20
+ const capture = deps.captureEditorDocument?.({ path: deps.getActiveFilePath?.(), projectId: deps.getProjectId?.() });
21
+ return capture?.ok ? capture : null;
22
+ }
23
+
24
+ function sameEditorCapture(left, right) {
25
+ return Boolean(left?.ok && right?.ok) && left.view === right.view && left.state === right.state && left.doc === right.doc
26
+ && left.text === right.text && ['path', 'projectId', 'documentId', 'targetDocumentId']
27
+ .every(key => left.documentIdentity[key] === right.documentIdentity[key]);
28
+ }
29
+
30
+ function hasEditorOverlay(snapshot) {
31
+ // Navigation reads carry the open method as their source. Only ZIP and
32
+ // document fetches are independent of the mounted editor.
33
+ return snapshot?.files?.some(file => typeof file.content === 'string'
34
+ && !/^overleaf-(?:zip|doc-fetch)/.test(file.source || ''));
35
+ }
36
+
37
+ function hasNavigationReads(snapshot) {
38
+ return snapshot?.files?.some(file => typeof file.content === 'string' && file.source !== 'active-editor'
39
+ && !/^overleaf-(?:zip|doc-fetch)/.test(file.source || ''));
40
+ }
41
+
19
42
  async function getProjectSnapshot(params = {}) {
20
43
  const cacheKey = getSnapshotCacheKey(params);
21
44
  const maxAgeMs = normalizeSnapshotMaxAge(params.maxAgeMs);
22
45
  const now = Date.now();
23
46
  const cacheEntry = snapshotCache.entries.get(cacheKey);
47
+ const captureKey = editorCaptureKey();
48
+ const projectId = deps.getProjectId?.();
24
49
 
25
- if (!params.force && cacheEntry?.pending) {
26
- return cacheEntry.pending;
50
+ function currentSnapshot(snapshot) {
51
+ if (deps.getProjectId?.() !== projectId) return withoutEditorFiles(snapshot, true);
52
+ if (!hasEditorOverlay(snapshot) || params.allowEditorNavigation === true || hasNavigationReads(snapshot)) return snapshot;
53
+ const captures = deps.getSnapshotEditorCaptures?.(snapshot) || [];
54
+ if (captures.length === 1 && deps.isCaptureCurrent?.(captures[0])) return snapshot;
55
+ // Fallback captures can be returned once, but never reused from cache.
56
+ if (!snapshot.files.some(file => file.documentIdentity !== undefined)) return snapshot;
57
+ return withoutEditorFiles(snapshot);
27
58
  }
28
59
 
29
- if (!params.force && cacheEntry?.value && now - cacheEntry.capturedAt <= maxAgeMs) {
60
+ if (!params.force && cacheEntry?.pending && (params.allowEditorNavigation === false || params.zipOnly || params.serverOnly)
61
+ && (params.serverOnly === true || sameEditorCapture(cacheEntry.pendingCaptureKey, captureKey))) {
62
+ const snapshot = await cacheEntry.pending;
63
+ return currentSnapshot(snapshot);
64
+ }
65
+
66
+ if (!params.force && cacheEntry?.value && now - cacheEntry.capturedAt <= maxAgeMs
67
+ && (!hasEditorOverlay(cacheEntry.value) || (params.allowEditorNavigation !== true
68
+ && sameEditorCapture(cacheEntry.captureKey, captureKey)))) {
30
69
  return withSnapshotCacheMetadata(cacheEntry.value, 'memory', cacheEntry.capturedAt);
31
70
  }
32
71
 
33
72
  const pending = deps.buildProjectSnapshot(params)
34
73
  .then(snapshot => {
74
+ const captures = deps.getSnapshotEditorCaptures?.(snapshot) || [];
75
+ const finalCapture = captures.length === 1 ? captures[0] : null;
76
+ snapshot = currentSnapshot(snapshot);
35
77
  const capturedAt = Date.now();
36
78
  const currentEntry = snapshotCache.entries.get(cacheKey);
37
79
  if (currentEntry?.pending === pending) {
38
80
  snapshotCache.entries.set(cacheKey, {
39
81
  value: snapshot,
82
+ captureKey: params.allowEditorNavigation === true || hasNavigationReads(snapshot) ? null : finalCapture,
40
83
  capturedAt,
41
84
  pending: null
42
85
  });
@@ -52,12 +95,22 @@
52
95
 
53
96
  snapshotCache.entries.set(cacheKey, {
54
97
  value: cacheEntry?.value || null,
98
+ captureKey: cacheEntry?.captureKey,
99
+ pendingCaptureKey: captureKey,
55
100
  capturedAt: cacheEntry?.capturedAt || 0,
56
101
  pending
57
102
  });
58
103
  return pending;
59
104
  }
60
105
 
106
+ function withoutEditorFiles(snapshot, projectChanged = false) {
107
+ const files = projectChanged ? [] : (snapshot?.files || []).filter(file => /^overleaf-(?:zip|doc-fetch)/.test(file.source || ''));
108
+ const removed = (snapshot?.files || []).filter(file => !files.includes(file));
109
+ return { ...snapshot, files, capabilities: { ...snapshot?.capabilities, fullProjectSnapshot: false,
110
+ requestedPathsComplete: false, skipped: [...(snapshot?.capabilities?.skipped || []),
111
+ ...removed.map(file => ({ path: file.path, reason: 'document_identity_mismatch' }))] } };
112
+ }
113
+
61
114
  async function getProjectFileList(params = {}) {
62
115
  const cacheKey = getProjectFileListCacheKey(params);
63
116
  const maxAgeMs = normalizeFileListMaxAge(params.maxAgeMs);
@@ -17,12 +17,22 @@
17
17
  const normalizeSafeProjectPath = typeof deps.normalizePath === 'function'
18
18
  ? deps.normalizePath
19
19
  : fallbackNormalizeSafeProjectPath;
20
- const readActiveEditorText = typeof deps.readActiveEditorText === 'function'
21
- ? deps.readActiveEditorText
22
- : () => '';
23
20
  let invalidatedAt = 0;
24
21
  let preferredZipEndpoint = '';
25
22
  const writebackReads = new Set();
23
+ const editorCaptures = new WeakMap();
24
+
25
+ function editorFile(path, ready, source) {
26
+ if (ready.documentIdentity && ready.capture) editorCaptures.set(ready.documentIdentity, ready.capture);
27
+ return { path, content: ready.text, source, kind: 'text',
28
+ ...(ready.documentIdentity !== undefined ? { documentIdentity: ready.documentIdentity } : {}) };
29
+ }
30
+
31
+ function getSnapshotEditorCaptures(snapshot) {
32
+ return (snapshot?.files || []).filter(file => typeof file.content === 'string'
33
+ && !/^overleaf-(?:zip|doc-fetch)/.test(file.source || ''))
34
+ .map(file => editorCaptures.get(file.documentIdentity));
35
+ }
26
36
 
27
37
  function getProjectId() {
28
38
  return treeOperations.getProjectId?.() || null;
@@ -214,6 +224,18 @@
214
224
  }
215
225
 
216
226
  async function buildProjectSnapshot(params = {}) {
227
+ const projectId = params.projectId || getProjectId();
228
+ if (!projectId || projectId !== getProjectId()) return projectChangedSnapshot(projectId);
229
+ const snapshot = await buildProjectSnapshotForProject({ ...params, projectId });
230
+ return getProjectId() === projectId ? snapshot : projectChangedSnapshot(projectId);
231
+ }
232
+
233
+ function projectChangedSnapshot(projectId) {
234
+ return { ok: false, id: projectId, files: [], capabilities: { fullProjectSnapshot: false,
235
+ skipped: [{ path: '', reason: 'document_identity_mismatch' }] } };
236
+ }
237
+
238
+ async function buildProjectSnapshotForProject(params = {}) {
217
239
  if (params.zipOnly || params.serverOnly === true) {
218
240
  return buildZipOnlyProjectSnapshot(params);
219
241
  }
@@ -242,9 +264,18 @@
242
264
 
243
265
  const zipSnapshot = await fetchProjectZipSnapshot(params);
244
266
  if (zipSnapshot.ok && zipSnapshot.files.length) {
267
+ const skipped = [...(zipSnapshot.skipped || [])];
268
+ const overlays = [...(lightweightSnapshot?.files || [])];
269
+ for (let index = 0; index < overlays.length; index++) {
270
+ if (overlays[index].source !== 'active-editor') continue;
271
+ const captured = await readActiveEditorOverlay(overlays[index].path, zipSnapshot.files, { projectId: params.projectId });
272
+ skipped.push(...captured.skipped);
273
+ overlays.splice(index, 1, ...captured.files);
274
+ if (!captured.files.length) index--;
275
+ }
245
276
  const mergedFiles = mergeSnapshotFiles(
246
277
  zipSnapshot.files,
247
- filterZipSnapshotOverlayFiles(zipSnapshot.files, lightweightSnapshot?.files || [], zipSnapshot.skipped)
278
+ filterZipSnapshotOverlayFiles(zipSnapshot.files, overlays, skipped)
248
279
  );
249
280
  const files = filterRequestedSnapshotFiles(mergedFiles, params, activePath);
250
281
  const requestedPathsComplete = params.restrictToRequestedPathsOnly === true
@@ -261,7 +292,7 @@
261
292
  contentComplete: !(zipSnapshot.skipped || []).length,
262
293
  requestedPathsComplete,
263
294
  method: 'overleaf-zip',
264
- skipped: zipSnapshot.skipped || [],
295
+ skipped,
265
296
  diagnostics: {
266
297
  zipEndpoint: zipSnapshot.endpoint,
267
298
  zipTiming: zipSnapshot.diagnostics,
@@ -309,15 +340,16 @@
309
340
  snapshotActivePath,
310
341
  ...(Array.isArray(params.focusFiles) ? params.focusFiles : [])
311
342
  ];
343
+ const enumeratedPaths = params.restrictToRequestedPathsOnly === true
344
+ ? []
345
+ : collectProjectTextPaths(snapshotActivePath);
312
346
  const projectPaths = window.CodexOverleafProjectFiles.collectUniqueTextPaths(
313
- params.restrictToRequestedPathsOnly === true
314
- ? requestedPaths
315
- : [
316
- ...requestedPaths,
317
- ...collectProjectTextPaths(snapshotActivePath)
318
- ],
347
+ [...requestedPaths, ...enumeratedPaths],
319
348
  80
320
349
  );
350
+ const enumerationTruncated = projectPaths.truncated === true
351
+ || enumeratedPaths.truncated === true
352
+ || internalDocRecords.truncated === true;
321
353
  const files = [];
322
354
  const skipped = [...(zipSnapshot?.skipped || [])];
323
355
  const ambiguousPaths = new Set(skipped.filter(file => file.reason === 'zip_path_duplicate').map(file => file.path));
@@ -332,7 +364,7 @@
332
364
  for (const filePath of projectPaths) {
333
365
  if (ambiguousPaths.has(filePath)) continue;
334
366
  if (filePath === snapshotActivePath) {
335
- const ready = await waitForActiveEditorText(filePath, 5000);
367
+ const ready = await waitForActiveEditorText(filePath, 5000, { allowEmpty: true, projectId: params.projectId });
336
368
  if (!ready.ok) {
337
369
  skipped.push({
338
370
  path: filePath,
@@ -340,18 +372,14 @@
340
372
  });
341
373
  continue;
342
374
  }
343
- files.push({
344
- path: filePath,
345
- content: ready.text,
346
- source: 'active-editor'
347
- });
375
+ files.push(editorFile(filePath, ready, 'active-editor'));
348
376
  previousSignature = contentSignature(ready.text);
349
377
  continue;
350
378
  }
351
379
 
352
380
  const docRecord = docRecordByPath.get(filePath);
353
381
  if (docRecord) {
354
- const fetched = await fetchOverleafDocContent(docRecord);
382
+ const fetched = await fetchOverleafDocContent(docRecord, params.projectId);
355
383
  if (fetched.ok) {
356
384
  files.push({
357
385
  path: filePath,
@@ -365,6 +393,7 @@
365
393
  path: filePath,
366
394
  reason: fetched.reason
367
395
  });
396
+ if (fetched.reason === 'document_identity_mismatch') continue;
368
397
  }
369
398
 
370
399
  if (!canOpenInactiveFileForSnapshot(filePath, params, lightweightOnly, docRecord)) {
@@ -386,7 +415,9 @@
386
415
 
387
416
  await waitForActiveFile(filePath, 5000);
388
417
  const ready = await waitForActiveEditorText(filePath, 7000, {
389
- notSignature: previousSignature
418
+ notSignature: previousSignature,
419
+ projectId: params.projectId,
420
+ allowEmpty: true
390
421
  });
391
422
  if (!ready.ok) {
392
423
  skipped.push({
@@ -395,11 +426,7 @@
395
426
  });
396
427
  continue;
397
428
  }
398
- files.push({
399
- path: filePath,
400
- content: ready.text,
401
- source: opened.method || 'file-tree-open'
402
- });
429
+ files.push(editorFile(filePath, ready, opened.method || 'file-tree-open'));
403
430
  previousSignature = contentSignature(ready.text);
404
431
  }
405
432
 
@@ -409,14 +436,6 @@
409
436
  await waitForActiveEditorText(activePath, 5000);
410
437
  }
411
438
 
412
- if (snapshotActivePath && !ambiguousPaths.has(snapshotActivePath) && !files.length && (!lightweightOnly || params.allowZipFallback === false)) {
413
- files.push({
414
- path: snapshotActivePath,
415
- content: readActiveEditorText(),
416
- source: 'active-editor'
417
- });
418
- }
419
-
420
439
  const requestedPathsComplete = isCompleteProjectSnapshot(files, projectPaths);
421
440
  return {
422
441
  id: getProjectId(),
@@ -424,7 +443,7 @@
424
443
  activePath: snapshotActivePath,
425
444
  files,
426
445
  capabilities: {
427
- fullProjectSnapshot: params.restrictToRequestedPathsOnly !== true && requestedPathsComplete && zipSnapshot?.pathIdentityComplete !== false,
446
+ fullProjectSnapshot: params.restrictToRequestedPathsOnly !== true && !enumerationTruncated && requestedPathsComplete && zipSnapshot?.pathIdentityComplete !== false,
428
447
  pathIdentityComplete: zipSnapshot?.pathIdentityComplete,
429
448
  requestedPathsComplete,
430
449
  method: resolvePageSnapshotMethod(files, lightweightOnly),
@@ -444,11 +463,11 @@
444
463
 
445
464
  async function buildZipOnlyProjectSnapshot(params = {}) {
446
465
  const activePath = getPassiveActiveFilePath();
447
- // Capture read evidence together with the path/text before awaiting ZIP.
448
- // A later editor identity cannot prove that an earlier empty read succeeded.
449
- const activeOverlay = params.serverOnly === true ? [] : readActiveEditorOverlay(activePath);
450
-
451
466
  const zipSnapshot = await fetchProjectZipSnapshot(params);
467
+ const captured = params.serverOnly === true ? { files: [], skipped: [] }
468
+ : await readActiveEditorOverlay(activePath, zipSnapshot.ok ? zipSnapshot.files : null, { passive: true, projectId: params.projectId });
469
+ const activeOverlay = captured.files;
470
+ const skipped = [...(zipSnapshot.skipped || []), ...captured.skipped];
452
471
  if (zipSnapshot.ok && zipSnapshot.files.length) {
453
472
  const files = filterRequestedSnapshotFiles(
454
473
  mergeSnapshotFiles(zipSnapshot.files, filterZipSnapshotOverlayFiles(zipSnapshot.files, activeOverlay, zipSnapshot.skipped)),
@@ -469,7 +488,7 @@
469
488
  contentComplete: !(zipSnapshot.skipped || []).length,
470
489
  requestedPathsComplete,
471
490
  method: 'overleaf-zip',
472
- skipped: zipSnapshot.skipped || [],
491
+ skipped,
473
492
  diagnostics: {
474
493
  zipEndpoint: zipSnapshot.endpoint,
475
494
  zipTiming: zipSnapshot.diagnostics,
@@ -494,7 +513,7 @@
494
513
  ? 'active-editor-zip-only-fallback'
495
514
  : 'zip-only-fallback-empty',
496
515
  contentComplete: false,
497
- skipped: [...(zipSnapshot.skipped || []), {
516
+ skipped: [...skipped, {
498
517
  path: 'project.zip',
499
518
  reason: zipSnapshot.reason || 'ZIP file identities were incomplete'
500
519
  }],
@@ -531,32 +550,15 @@
531
550
  return (files || []).filter(file => requestedSet.has(file?.path));
532
551
  }
533
552
 
534
- function readActiveEditorOverlay(activePath) {
535
- if (!activePath) return [];
536
- try {
537
- const content = readActiveEditorText();
538
- if (typeof content !== 'string') return [];
539
- if (!content.trim()) {
540
- // The legacy reader also returns '' when no surface exists. A readable
541
- // identity must independently confirm blank text before it can erase ZIP.
542
- const identity = deps.getActiveEditorIdentity?.();
543
- let observed;
544
- if (identity?.type === 'codemirror-view' && typeof identity.doc?.toString === 'function') {
545
- observed = identity.doc.toString();
546
- } else if (identity?.type === 'textarea') {
547
- observed = identity.node?.value;
548
- } else if (identity?.type === 'contenteditable') {
549
- const node = identity.node;
550
- if (typeof node?.innerText === 'string' || typeof node?.textContent === 'string') {
551
- observed = node.innerText || node.textContent || '';
552
- }
553
- }
554
- if (typeof observed !== 'string' || observed !== content) return [];
555
- } else if (!window.CodexOverleafProjectFiles.isUsableProjectFileContent(content)) return [];
556
- return [{ path: activePath, content, source: 'active-editor', kind: 'text' }];
557
- } catch (_error) {
558
- return [];
559
- }
553
+ async function readActiveEditorOverlay(activePath, referenceFiles, options = {}) {
554
+ if (!activePath) return { files: [], skipped: [] };
555
+ const references = referenceFiles ? new Map(referenceFiles
556
+ .filter(file => file.kind !== 'binary' && typeof file.content === 'string')
557
+ .map(file => [file.path, file.content])) : null;
558
+ const ready = await waitForActiveEditorText(activePath, 5000, { ...options, allowEmpty: true, references });
559
+ return ready.ok
560
+ ? { files: [editorFile(activePath, ready, 'active-editor')], skipped: [] }
561
+ : { files: [], skipped: [{ path: activePath, reason: ready.code || ready.reason }] };
560
562
  }
561
563
 
562
564
  function mergeSnapshotFiles(primaryFiles = [], overrideFiles = []) {
@@ -609,7 +611,7 @@
609
611
  return projectPaths.every(filePath => capturedPaths.has(filePath));
610
612
  }
611
613
 
612
- async function fetchOverleafDocContent(record) {
614
+ async function fetchOverleafDocContent(record, expectedProjectId = getProjectId()) {
613
615
  if (!record?.id || !record.path) {
614
616
  return {
615
617
  ok: false,
@@ -625,6 +627,9 @@
625
627
  };
626
628
  }
627
629
 
630
+ const target = treeOperations.resolveTargetDocument?.(record.path, { projectId });
631
+ if (projectId !== expectedProjectId || (target && (target.status !== 'known' || target.documentId !== record.id.toLowerCase())))
632
+ return { ok: false, reason: 'document_identity_mismatch' };
628
633
  try {
629
634
  const response = await fetch(`${window.location.origin}/project/${encodeURIComponent(projectId)}/doc/${encodeURIComponent(record.id)}`, {
630
635
  credentials: 'include',
@@ -639,6 +644,9 @@
639
644
  };
640
645
  }
641
646
  const payload = await response.json();
647
+ const currentTarget = treeOperations.resolveTargetDocument?.(record.path, { projectId });
648
+ if (getProjectId() !== projectId || (target && (currentTarget?.status !== 'known' || currentTarget.documentId !== target.documentId)))
649
+ return { ok: false, reason: 'document_identity_mismatch' };
642
650
  const content = readDocPayloadContent(payload);
643
651
  if (!window.CodexOverleafProjectFiles.isUsableProjectFileContent(content)) {
644
652
  return {
@@ -1071,6 +1079,7 @@
1071
1079
  assembleSnapshot,
1072
1080
  buildProjectFileList,
1073
1081
  buildProjectSnapshot,
1082
+ getSnapshotEditorCaptures,
1074
1083
  fetchProjectZipSnapshot,
1075
1084
  getInvalidatedAt,
1076
1085
  getRequestedSnapshotPaths,
@@ -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
  }