codex-overleaf-link 2.5.0 → 2.5.1

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 (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  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 +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  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 +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -0,0 +1,113 @@
1
+ (function (root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafReviewContext = api;
5
+ })(typeof window !== 'undefined' ? window : globalThis, function () {
6
+ 'use strict';
7
+
8
+ const cancelledOwners = new Map();
9
+ function cancelOwner(token) {
10
+ if (typeof token !== 'string' || !token || token.length > 256) return false;
11
+ for (const [id, until] of cancelledOwners) if (until <= Date.now()) cancelledOwners.delete(id);
12
+ cancelledOwners.set(token, Date.now() + 120000); return true;
13
+ }
14
+
15
+ // One context per request. The small response reserve lets an expired review
16
+ // return its known partial evidence before the caller's transport timer fires.
17
+ function create(deps, input = {}) {
18
+ const now = deps.now || Date.now;
19
+ const timers = typeof deps.window?.setTimeout === 'function' ? deps.window : globalThis;
20
+ const started = now(), limit = input.deadlineAt == null ? started + 120000 : input.deadlineAt;
21
+ const deadlineAt = Number.isFinite(limit) ? limit - Math.min(250, Math.max(0, limit - started) / 10) : started;
22
+ const cancellation = deps.readWriteCancellationSequence?.();
23
+ const appliedLists = new Set(), observations = new Map();
24
+ let stopped = null, mutationStarted = false;
25
+ const params = { ...input, deadlineAt };
26
+ const failure = (code, reason) => ({ ok: false, code, reason, changedDocument: false,
27
+ failure: { code: code === 'codex_cancelled' ? 'codex_run_cancelled' : 'tracked_changes_remain',
28
+ stage: 'reviewing', severity: 'blocked', terminalState: 'blocked', changedDocument: false,
29
+ userMessage: reason, nextAction: 'Check the retained per-file results in Overleaf before retrying.',
30
+ evidence: { originalCode: code, writeStarted: false } } });
31
+ function check() {
32
+ if (stopped) return stopped;
33
+ if (params.reviewOwnerToken && cancelledOwners.has(params.reviewOwnerToken))
34
+ return stopped = failure('codex_cancelled', 'The Undo owner disconnected; no further review writes were started.');
35
+ if (!Number.isFinite(limit) || now() >= deadlineAt)
36
+ return stopped = failure('writeback_deadline_exceeded', 'The review time budget expired; no further review writes were started.');
37
+ if (typeof deps.readWriteCancellationSequence === 'function' && deps.readWriteCancellationSequence() !== cancellation)
38
+ return stopped = failure('codex_cancelled', 'The review was cancelled; no further review writes were started.');
39
+ const project = deps.getProjectId?.() || /\/project\/([^/?#]+)/.exec(deps.window?.location?.pathname || '')?.[1];
40
+ if (!params.runProjectId || (project && project !== params.runProjectId))
41
+ return stopped = failure('aborted_project_changed', 'The original review project is no longer active.');
42
+ const blocked = deps.checkWritebackRunProjectId?.(params);
43
+ if (blocked) return stopped = blocked.skipped?.[0]?.result || blocked;
44
+ return null;
45
+ }
46
+ function assert() {
47
+ const blocked = check();
48
+ if (blocked) throw Object.assign(new Error(blocked.reason || blocked.code), { reviewFailure: blocked });
49
+ }
50
+ async function wait(work) {
51
+ assert();
52
+ let timer;
53
+ try {
54
+ const timeout = new Promise((_, reject) => {
55
+ timer = timers.setTimeout(() => {
56
+ const blocked = check() || failure('writeback_deadline_exceeded', 'The review time budget expired.');
57
+ stopped = blocked;
58
+ reject(Object.assign(new Error(blocked.reason), { reviewFailure: blocked }));
59
+ }, Math.max(0, deadlineAt - now()));
60
+ });
61
+ const result = await Promise.race([Promise.resolve().then(() => { assert(); return work(); }), timeout]);
62
+ assert();
63
+ return result;
64
+ } finally { if (timer != null) timers.clearTimeout(timer); }
65
+ }
66
+ const remember = entry => {
67
+ if (entry.result?.changedDocument === true || entry.result?.failure?.changedDocument === true) mutationStarted = true;
68
+ const ref = entry.trackedChange || { path: entry.operation?.path || '', key: 'snapshot-undo:' + (entry.operation?.path || ''), id: '' };
69
+ observations.set(ref.path + '\0' + (ref.key || ref.id || ''), { trackedChange: ref, result: entry.result });
70
+ };
71
+ const scoped = { ...deps, reviewGuard: check, reviewAwait: work => wait(work),
72
+ reviewDeadlineAt: deadlineAt,
73
+ createReviewApplied() { const list = []; appliedLists.add(list); return list; } };
74
+ for (const name of ['openFileByPath', 'setReviewingEnabled', 'verifyActiveEditorText']) {
75
+ if (typeof deps[name] === 'function') scoped[name] = (...args) => wait(() => deps[name](...args));
76
+ }
77
+ scoped.delay = ms => wait(() => deps.delay(Math.min(ms, Math.max(0, deadlineAt - now()))));
78
+ if (typeof deps.readActiveEditorText === 'function') scoped.readActiveEditorText = (...args) => { assert(); return deps.readActiveEditorText(...args); };
79
+ if (typeof deps.clickNode === 'function') scoped.clickNode = (...args) => { assert(); mutationStarted = true; return deps.clickNode(...args); };
80
+ for (const name of ['applyOperationsCore', 'applyOperationsWithNoTraceUndo']) {
81
+ if (typeof deps[name] !== 'function') continue;
82
+ scoped[name] = async (operations, options = {}) => {
83
+ assert();
84
+ const result = await deps[name](operations, { ...options, deadlineAt,
85
+ reviewGuard: check, reviewAwait: work => wait(work),
86
+ onOperationResult(entry) { remember(entry); options.onOperationResult?.(entry); } });
87
+ for (const entry of result.applied || []) remember(entry);
88
+ // Preserve a completed in-flight result even if cancellation just arrived.
89
+ return result;
90
+ };
91
+ }
92
+ if (typeof deps.confirmReviewWriteback === 'function') scoped.confirmReviewWriteback = async (original, result) => {
93
+ for (const entry of result.applied || []) remember(entry);
94
+ return deps.confirmReviewWriteback({ ...original, deadlineAt, reviewGuard: check, reviewAwait: work => wait(work) }, result);
95
+ };
96
+ async function run(work) {
97
+ try { assert(); return await work(scoped, params); }
98
+ catch (error) {
99
+ if (!error?.reviewFailure) throw error;
100
+ for (const list of appliedLists) for (const entry of list) remember(entry);
101
+ const applied = [...observations.values()].filter(entry => entry.result?.ok !== false);
102
+ const path = deps.getActiveFilePath?.() || '';
103
+ const changedDocument = mutationStarted || applied.some(entry => entry.result?.changedDocument !== false);
104
+ const blocked = { ...error.reviewFailure, changedDocument,
105
+ failure: { ...error.reviewFailure.failure, changedDocument,
106
+ evidence: { ...error.reviewFailure.failure?.evidence, writeStarted: changedDocument } } };
107
+ return { ok: false, applied, skipped: [{ trackedChange: { path }, result: blocked }] };
108
+ }
109
+ }
110
+ return Object.freeze({ run, check, assert, wait, params, scoped });
111
+ }
112
+ return Object.freeze({ create, cancelOwner });
113
+ });
@@ -311,16 +311,18 @@
311
311
  expected.set(path, entry.result.verifiedContent);
312
312
  }
313
313
  }
314
- const deadline = now() + Math.max(0, timeoutMs);
314
+ const deadline = Math.min(now() + Math.max(0, timeoutMs), params.deadlineAt == null ? Infinity
315
+ : Number.isFinite(params.deadlineAt) ? params.deadlineAt : now());
315
316
  let attempts = 0;
316
317
  if (expected.size && typeof readSnapshot === 'function') {
317
318
  do {
318
- if (!params.runProjectId || getProjectId() !== params.runProjectId) break;
319
+ if (now() >= deadline || params.reviewGuard?.() || !params.runProjectId || getProjectId() !== params.runProjectId) break;
319
320
  try {
320
- const snapshot = await readSnapshot({ force: true, maxAgeMs: 0,
321
+ const read = () => readSnapshot({ force: true, maxAgeMs: 0,
321
322
  zipTimeoutMs: Math.max(1, Math.min(8000, deadline - now())) });
323
+ const snapshot = params.reviewAwait ? await params.reviewAwait(read) : await read();
322
324
  attempts++;
323
- if (getProjectId() !== params.runProjectId) break;
325
+ if (now() >= deadline || params.reviewGuard?.() || getProjectId() !== params.runProjectId) break;
324
326
  if (snapshot?.ok === true && Array.from(expected).every(([path, content]) =>
325
327
  snapshot.files?.some(file => file.path === path && file.source === 'overleaf-zip'
326
328
  && file.content === content))) {
@@ -97,9 +97,10 @@
97
97
  };
98
98
  }),
99
99
  capabilities: {
100
- fullProjectSnapshot: true,
100
+ fullProjectSnapshot: zipSnapshot.pathIdentityComplete !== false,
101
+ pathIdentityComplete: zipSnapshot.pathIdentityComplete,
101
102
  method: 'overleaf-zip-file-list',
102
- skipped: [],
103
+ skipped: zipSnapshot.skipped || [],
103
104
  diagnostics: {
104
105
  zipEndpoint: zipSnapshot.endpoint,
105
106
  zipTiming: zipSnapshot.diagnostics,
@@ -243,7 +244,7 @@
243
244
  if (zipSnapshot.ok && zipSnapshot.files.length) {
244
245
  const mergedFiles = mergeSnapshotFiles(
245
246
  zipSnapshot.files,
246
- filterZipSnapshotOverlayFiles(zipSnapshot.files, lightweightSnapshot?.files || [])
247
+ filterZipSnapshotOverlayFiles(zipSnapshot.files, lightweightSnapshot?.files || [], zipSnapshot.skipped)
247
248
  );
248
249
  const files = filterRequestedSnapshotFiles(mergedFiles, params, activePath);
249
250
  const requestedPathsComplete = params.restrictToRequestedPathsOnly === true
@@ -255,7 +256,9 @@
255
256
  activePath,
256
257
  files,
257
258
  capabilities: {
258
- fullProjectSnapshot: params.restrictToRequestedPathsOnly === true ? false : true,
259
+ fullProjectSnapshot: params.restrictToRequestedPathsOnly !== true && zipSnapshot.pathIdentityComplete !== false,
260
+ pathIdentityComplete: zipSnapshot.pathIdentityComplete,
261
+ contentComplete: !(zipSnapshot.skipped || []).length,
259
262
  requestedPathsComplete,
260
263
  method: 'overleaf-zip',
261
264
  skipped: zipSnapshot.skipped || [],
@@ -316,7 +319,8 @@
316
319
  80
317
320
  );
318
321
  const files = [];
319
- const skipped = [];
322
+ const skipped = [...(zipSnapshot?.skipped || [])];
323
+ const ambiguousPaths = new Set(skipped.filter(file => file.reason === 'zip_path_duplicate').map(file => file.path));
320
324
  if (zipSnapshot && !zipSnapshot.ok) {
321
325
  skipped.push({
322
326
  path: 'project.zip',
@@ -326,6 +330,7 @@
326
330
  let previousSignature = '';
327
331
 
328
332
  for (const filePath of projectPaths) {
333
+ if (ambiguousPaths.has(filePath)) continue;
329
334
  if (filePath === snapshotActivePath) {
330
335
  const ready = await waitForActiveEditorText(filePath, 5000);
331
336
  if (!ready.ok) {
@@ -404,7 +409,7 @@
404
409
  await waitForActiveEditorText(activePath, 5000);
405
410
  }
406
411
 
407
- if (snapshotActivePath && !files.length && (!lightweightOnly || params.allowZipFallback === false)) {
412
+ if (snapshotActivePath && !ambiguousPaths.has(snapshotActivePath) && !files.length && (!lightweightOnly || params.allowZipFallback === false)) {
408
413
  files.push({
409
414
  path: snapshotActivePath,
410
415
  content: readActiveEditorText(),
@@ -419,7 +424,8 @@
419
424
  activePath: snapshotActivePath,
420
425
  files,
421
426
  capabilities: {
422
- fullProjectSnapshot: params.restrictToRequestedPathsOnly === true ? false : requestedPathsComplete,
427
+ fullProjectSnapshot: params.restrictToRequestedPathsOnly !== true && requestedPathsComplete && zipSnapshot?.pathIdentityComplete !== false,
428
+ pathIdentityComplete: zipSnapshot?.pathIdentityComplete,
423
429
  requestedPathsComplete,
424
430
  method: resolvePageSnapshotMethod(files, lightweightOnly),
425
431
  skipped,
@@ -438,15 +444,14 @@
438
444
 
439
445
  async function buildZipOnlyProjectSnapshot(params = {}) {
440
446
  const activePath = getPassiveActiveFilePath();
441
- const activeText = params.serverOnly === true ? '' : activePath ? readActiveEditorText() : '';
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);
442
450
 
443
451
  const zipSnapshot = await fetchProjectZipSnapshot(params);
444
452
  if (zipSnapshot.ok && zipSnapshot.files.length) {
445
- const activeOverlay = params.serverOnly === true ? [] : buildActiveEditorOverlay(activePath, activeText, {
446
- allowedPaths: zipSnapshot.files.map(file => file.path)
447
- });
448
453
  const files = filterRequestedSnapshotFiles(
449
- mergeSnapshotFiles(zipSnapshot.files, activeOverlay),
454
+ mergeSnapshotFiles(zipSnapshot.files, filterZipSnapshotOverlayFiles(zipSnapshot.files, activeOverlay, zipSnapshot.skipped)),
450
455
  params,
451
456
  activePath
452
457
  );
@@ -459,7 +464,9 @@
459
464
  activePath,
460
465
  files,
461
466
  capabilities: {
462
- fullProjectSnapshot: params.restrictToRequestedPathsOnly === true ? false : true,
467
+ fullProjectSnapshot: params.restrictToRequestedPathsOnly !== true && zipSnapshot.pathIdentityComplete !== false,
468
+ pathIdentityComplete: zipSnapshot.pathIdentityComplete,
469
+ contentComplete: !(zipSnapshot.skipped || []).length,
463
470
  requestedPathsComplete,
464
471
  method: 'overleaf-zip',
465
472
  skipped: zipSnapshot.skipped || [],
@@ -478,16 +485,18 @@
478
485
  id: getProjectId(),
479
486
  url: window.location.href,
480
487
  activePath,
481
- files: params.serverOnly === true ? [] : buildActiveEditorOverlay(activePath, activeText),
488
+ files: filterZipSnapshotOverlayFiles([], activeOverlay, zipSnapshot.skipped, true),
482
489
  capabilities: {
483
490
  fullProjectSnapshot: false,
491
+ pathIdentityComplete: zipSnapshot.pathIdentityComplete,
484
492
  method: params.serverOnly === true ? 'overleaf-zip-unavailable'
485
- : activePath && window.CodexOverleafProjectFiles.isUsableProjectFileContent(activeText)
493
+ : activeOverlay.length
486
494
  ? 'active-editor-zip-only-fallback'
487
495
  : 'zip-only-fallback-empty',
488
- skipped: [{
496
+ contentComplete: false,
497
+ skipped: [...(zipSnapshot.skipped || []), {
489
498
  path: 'project.zip',
490
- reason: zipSnapshot.reason
499
+ reason: zipSnapshot.reason || 'ZIP file identities were incomplete'
491
500
  }],
492
501
  diagnostics: {
493
502
  docRecordCount: 0,
@@ -500,9 +509,11 @@
500
509
  };
501
510
  }
502
511
 
503
- function filterZipSnapshotOverlayFiles(zipFiles = [], overlayFiles = []) {
512
+ function filterZipSnapshotOverlayFiles(zipFiles = [], overlayFiles = [], skipped = [], allowMissing = false) {
504
513
  const zipPaths = new Set((zipFiles || []).map(file => file.path).filter(Boolean));
505
- return (overlayFiles || []).filter(file => file?.source !== 'active-editor' || zipPaths.has(file.path));
514
+ const ambiguousPaths = new Set((skipped || []).filter(file => file.reason === 'zip_path_duplicate').map(file => file.path));
515
+ return (overlayFiles || []).filter(file => !ambiguousPaths.has(file?.path)
516
+ && (allowMissing || file?.source !== 'active-editor' || zipPaths.has(file.path)));
506
517
  }
507
518
 
508
519
  function getRequestedSnapshotPaths(params = {}, activePath = '') {
@@ -520,19 +531,32 @@
520
531
  return (files || []).filter(file => requestedSet.has(file?.path));
521
532
  }
522
533
 
523
- function buildActiveEditorOverlay(activePath, activeText, options = {}) {
524
- if (!activePath || !window.CodexOverleafProjectFiles.isUsableProjectFileContent(activeText)) {
525
- return [];
526
- }
527
- if (Array.isArray(options.allowedPaths) && !options.allowedPaths.includes(activePath)) {
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) {
528
558
  return [];
529
559
  }
530
- return [{
531
- path: activePath,
532
- content: activeText,
533
- source: 'active-editor',
534
- kind: 'text'
535
- }];
536
560
  }
537
561
 
538
562
  function mergeSnapshotFiles(primaryFiles = [], overrideFiles = []) {
@@ -541,7 +565,7 @@
541
565
  if (!file?.path || file.kind === 'binary' || typeof file.content !== 'string') {
542
566
  continue;
543
567
  }
544
- if (!window.CodexOverleafProjectFiles.isUsableProjectFileContent(file.content)) {
568
+ if (file.content.trim() && !window.CodexOverleafProjectFiles.isUsableProjectFileContent(file.content)) {
545
569
  continue;
546
570
  }
547
571
  filesByPath.set(file.path, {
@@ -691,7 +715,7 @@
691
715
  });
692
716
  const extractMs = Date.now() - extractStartedAt;
693
717
  const files = extracted.files || [];
694
- if (!files.length) {
718
+ if (!files.length && extracted.pathIdentityComplete !== false) {
695
719
  attempts.push({
696
720
  endpoint,
697
721
  status: response.status,
@@ -715,6 +739,7 @@
715
739
  return {
716
740
  ok: true,
717
741
  endpoint,
742
+ pathIdentityComplete: extracted.pathIdentityComplete,
718
743
  skipped: extracted.skipped || [],
719
744
  files: files.map(file => ({
720
745
  ...file,
@@ -841,6 +866,9 @@
841
866
  const entryCount = view.getUint16(eocdOffset + 10, true);
842
867
  const centralDirectoryOffset = view.getUint32(eocdOffset + 16, true);
843
868
  const decoder = new TextDecoder('utf-8');
869
+ const nameDecoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
870
+ const seenPaths = new Set();
871
+ let pathIdentityComplete = true;
844
872
  const files = [];
845
873
  const skipped = [];
846
874
  let binaryBytes = 0;
@@ -859,7 +887,25 @@
859
887
  const commentLength = view.getUint16(offset + 32, true);
860
888
  const localHeaderOffset = view.getUint32(offset + 42, true);
861
889
  const rawName = bytes.slice(offset + 46, offset + 46 + fileNameLength);
862
- const path = normalizeSafeProjectPath(decoder.decode(rawName));
890
+ let name = '';
891
+ try { name = nameDecoder.decode(rawName); } catch (_error) { /* unknown identity is incomplete */ }
892
+ const directory = name.endsWith('/');
893
+ const path = normalizeSafeProjectPath(directory ? name.slice(0, -1) : name);
894
+ if (!path || seenPaths.has(path)) {
895
+ if (path) {
896
+ const previousIndex = files.findIndex(file => file.path === path);
897
+ if (previousIndex >= 0) files.splice(previousIndex, 1);
898
+ }
899
+ pathIdentityComplete = false;
900
+ skipped.push({ path: name, reason: path ? 'zip_path_duplicate' : 'zip_path_invalid', unknownPath: !name });
901
+ offset += 46 + fileNameLength + extraLength + commentLength;
902
+ continue;
903
+ }
904
+ seenPaths.add(path);
905
+ if (directory) {
906
+ offset += 46 + fileNameLength + extraLength + commentLength;
907
+ continue;
908
+ }
863
909
  const isText = window.CodexOverleafProjectFiles.isTextProjectPath(path);
864
910
  const shouldInclude = path && !path.endsWith('/') && (isText || options.includeBinaryFiles);
865
911
 
@@ -904,7 +950,7 @@
904
950
  offset += 46 + fileNameLength + extraLength + commentLength;
905
951
  }
906
952
 
907
- return { files, skipped };
953
+ return { files, skipped, pathIdentityComplete };
908
954
  }
909
955
 
910
956
  function uint8ArrayToBase64(bytes) {
@@ -1035,11 +1081,10 @@
1035
1081
  }
1036
1082
 
1037
1083
  function fallbackNormalizeSafeProjectPath(value) {
1038
- const text = String(value || '').replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '').trim();
1039
- if (!text || text.split('/').some(part => !part || part === '.' || part === '..')) {
1040
- return '';
1041
- }
1042
- return text;
1084
+ if (typeof value !== 'string' || !value || value.length > 2048 || value.trim() !== value
1085
+ || /[\u0000-\u001f\u007f]/.test(value) || value.startsWith('/')
1086
+ || value.includes('\\') || /^[A-Za-z]:/.test(value)) return '';
1087
+ return value.split('/').some(part => !part || part === '.' || part === '..') ? '' : value;
1043
1088
  }
1044
1089
 
1045
1090
  return { create };