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
@@ -1,7 +1,7 @@
1
1
  (function (root, factory) {
2
- if (typeof module === 'object' && module.exports) module.exports = factory();
3
- else root.CodexOverleafTextFileCreator = factory();
4
- })(typeof window !== 'undefined' ? window : globalThis, function () {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory(require('./ownedDirectoryCleanup'));
3
+ else root.CodexOverleafTextFileCreator = factory(root.CodexOverleafOwnedDirectoryCleanup);
4
+ })(typeof window !== 'undefined' ? window : globalThis, function (OwnedDirectoryCleanup) {
5
5
  'use strict';
6
6
 
7
7
  const MAX_CONFIRMATION_OBSERVATIONS = 8;
@@ -18,6 +18,7 @@
18
18
  const delay = ms => new Promise(resolve => window.setTimeout(resolve, ms));
19
19
  const now = typeof deps.now === 'function' ? deps.now : Date.now;
20
20
  const verificationReads = new Set();
21
+ const uploadParentProofs = new WeakMap();
21
22
  const verificationBudget = () => ({ remainingMs: 90000 });
22
23
  async function pauseVerification(budget, durationMs) {
23
24
  const started = now();
@@ -81,7 +82,18 @@
81
82
  } : {}) };
82
83
  } finally { if (controller) verificationReads.delete(controller); }
83
84
  assertVerificationOwner(projectId, isCurrent);
84
- if (result?.ok === true && Array.isArray(result.files)) return result;
85
+ if (result?.ok === true && Array.isArray(result.files)) {
86
+ const skipped = [...(Array.isArray(result.skipped) ? result.skipped : []),
87
+ ...(Array.isArray(result.capabilities?.skipped) ? result.capabilities.skipped : [])];
88
+ if (result.pathIdentityComplete === false || result.capabilities?.pathIdentityComplete === false
89
+ || skipped.some(file => file?.unknownPath || ['zip_path_invalid', 'zip_path_duplicate'].includes(file?.reason))) {
90
+ throw Object.assign(new Error('The server snapshot contains unresolved file identities; absence cannot be confirmed.'), {
91
+ code: 'source_zip_unavailable', originalCode: 'zip_path_identity_incomplete', retryable: false,
92
+ diagnostics: { ...(result.diagnostics || {}), pathIdentityComplete: false }
93
+ });
94
+ }
95
+ return result;
96
+ }
85
97
  last = Object.assign(new Error('The server project snapshot could not be read.'), {
86
98
  code: 'source_zip_unavailable', diagnostics: result?.diagnostics || null,
87
99
  technicalMessage: result?.reason || 'No complete server snapshot was returned.',
@@ -142,11 +154,13 @@
142
154
  if (!lastError && observed.get(operation.path)?.content === operation.content) {
143
155
  results.push({ operation, result: { ok: true, verified: true, changedDocument: true,
144
156
  method: 'overleaf.native-text-upload', verification: 'overleaf-zip',
157
+ createdDirectories: entry.result?.createdDirectories || [], fileUploadAttempted: entry.result?.fileUploadAttempted === true,
145
158
  saveReceipt: await makeSaveReceipt(batch.projectId, operation.path, operation.content) } });
146
159
  } else {
147
- results.push({ operation, result: verificationFailure(lastError || new Error(
160
+ results.push({ operation, result: { ...verificationFailure(lastError || new Error(
148
161
  observed.has(operation.path) ? 'Server content differs from the submitted file.' : 'The file is not yet present in the server snapshot.'
149
- ), true, 'post-upload') });
162
+ ), true, 'post-upload'), createdDirectories: entry.result?.createdDirectories || [],
163
+ fileUploadAttempted: entry.result?.fileUploadAttempted === true } });
150
164
  }
151
165
  }
152
166
  // The final fresh snapshot can also prove unrelated edits in this batch.
@@ -191,6 +205,10 @@
191
205
  return folder;
192
206
  };
193
207
 
208
+ const directoryCleanup = OwnedDirectoryCleanup.create({ document, treeOperations, delay, now,
209
+ readServerSnapshot, assertVerificationOwner, normalizeDomText, normalizeSafeProjectPath });
210
+ const { folderIdentity, cleanupCreatedDirectories } = directoryCleanup;
211
+
194
212
  async function createFolderWithDom(entryPath, options = {}) {
195
213
  const target = normalizeSafeProjectPath(entryPath);
196
214
  const projectId = getProjectId();
@@ -230,7 +248,15 @@
230
248
  toggle.click();
231
249
  await waitFor(() => findFolder(parentPath)?.getAttribute('aria-expanded') === 'true');
232
250
  };
233
- let ownedDialog = null;
251
+ let ownedDialog = null, createdProof = null;
252
+ const publishProof = () => {
253
+ if (!createdProof) return;
254
+ if (getProjectId() === projectId) {
255
+ const row = findFolder(target);
256
+ if (row) { createdProof.nativeFolderId = folderIdentity(row); createdProof.observedAfterCreate = true; }
257
+ }
258
+ options.onCreatedDirectory?.({ ...createdProof });
259
+ };
234
260
  try {
235
261
  assertCurrent();
236
262
  if (findFolder(target)) return;
@@ -259,12 +285,18 @@
259
285
  await delay(0);
260
286
  assertCurrent();
261
287
  if (findFolder(target) || projectPathExists(target)) throw new Error('The target path appeared during creation; no replacement was attempted.');
288
+ const confirm = choose(ownedDialog, /^(?:Create|创建)$/i);
289
+ createdProof = { v: 1, projectId, path: target, nativeFolderId: '', ownershipConfirmed: false, observedAfterCreate: false };
290
+ options.onCreatedDirectory?.({ ...createdProof });
262
291
  options.onMutation?.();
263
- choose(ownedDialog, /^(?:Create|创建)$/i).click();
292
+ confirm.click();
293
+ publishProof();
264
294
  await waitFor(() => !dialogs().length);
265
295
  await expandParent();
266
296
  await waitFor(() => findFolder(target));
297
+ publishProof();
267
298
  } finally {
299
+ publishProof();
268
300
  if (ownedDialog && getProjectId() === projectId && dialogs().includes(ownedDialog)) {
269
301
  const cancel = Array.from(ownedDialog.querySelectorAll('button')).filter(node => visible(node) && /^(?:Cancel|取消)$/i.test(controlName(node)));
270
302
  if (cancel.length === 1) cancel[0].click();
@@ -281,7 +313,12 @@
281
313
  if (!target || !projectId || typeof operation.content !== 'string' || !window.CodexOverleafProjectFiles?.isTextProjectPath(target)) {
282
314
  return { ok: false, code: 'invalid_project_path', reason: 'A valid text-file path and project are required.' };
283
315
  }
284
- let mutationAttempted = false;
316
+ let mutationAttempted = false, fileUploadAttempted = false;
317
+ const createdDirectories = [];
318
+ const recordDirectory = proof => {
319
+ const index = createdDirectories.findIndex(item => item.path === proof.path);
320
+ if (index < 0) createdDirectories.push(proof); else createdDirectories[index] = proof;
321
+ };
285
322
  let verificationPhase = 'preflight', zipReadCount = 0, lastZipFailure = null;
286
323
  const readBudget = options.batch?.budget || verificationBudget();
287
324
  let uploadDialog = null;
@@ -346,7 +383,7 @@
346
383
  data.items.add(file);
347
384
  assertCurrent();
348
385
  uploadHelpers.assignFilesToInput(input, data.files, window);
349
- mutationAttempted = true;
386
+ mutationAttempted = true; fileUploadAttempted = true;
350
387
  options.batch?.pending.set(target, operation);
351
388
  input.dispatchEvent(new window.Event('change', { bubbles: true }));
352
389
  // Never click Replace/Overwrite. A concurrent name conflict must stop.
@@ -402,20 +439,28 @@
402
439
  const folderPath = parts.slice(0, depth).join('/');
403
440
  if (!findFolder(folderPath)) await createFolderWithDom(folderPath, {
404
441
  isCurrent: () => { assertCurrent(); return true; },
405
- onMutation: () => { mutationAttempted = true; }
442
+ onMutation: () => { mutationAttempted = true; }, onCreatedDirectory: recordDirectory
406
443
  });
407
444
  await ensureFolderExpanded(folderPath);
408
445
  }
409
446
  await uploadText();
410
- if (options.batch) return { ok: false, pendingVerification: true, changedDocument: true };
411
- return { ok: true, method: 'overleaf.native-text-upload', changedDocument: true,
447
+ if (options.batch) return { ok: false, pendingVerification: true, changedDocument: true, createdDirectories, fileUploadAttempted };
448
+ return { ok: true, method: 'overleaf.native-text-upload', changedDocument: true, createdDirectories, fileUploadAttempted,
412
449
  verified: true, verification: 'overleaf-zip',
413
450
  saveReceipt: await makeSaveReceipt(projectId, target, operation.content) };
414
451
  } catch (error) {
415
- return {
416
- ...verificationFailure(error, mutationAttempted, verificationPhase),
417
- diagnostics: { verificationPhase, zipReadCount, lastZipFailure }
418
- };
452
+ const directoryCleanup = createdDirectories.length
453
+ ? await cleanupCreatedDirectories(createdDirectories, { projectId, isCurrent: options.isCurrent }) : undefined;
454
+ const result = verificationFailure(error, mutationAttempted, verificationPhase);
455
+ if (!fileUploadAttempted && mutationAttempted) {
456
+ const reason = directoryCleanup?.ok ? 'File upload did not start. The new empty folders were removed.'
457
+ : 'File upload did not start. Some new folders remain and need review.';
458
+ result.reason = reason; result.code = 'file_tree_operation_unverified';
459
+ result.failure = { ...result.failure, code: result.code, stage: 'directory-preparation', userMessage: reason,
460
+ nextAction: directoryCleanup?.ok ? 'Retry sync to create the file.' : 'Review the remaining folders before retrying.' };
461
+ }
462
+ return { ...result, createdDirectories: directoryCleanup?.remaining || createdDirectories,
463
+ fileUploadAttempted, directoryCleanup, diagnostics: { verificationPhase, zipReadCount, lastZipFailure } };
419
464
  } finally {
420
465
  if (uploadDialog && getProjectId() === projectId && dialogs().includes(uploadDialog)) {
421
466
  const close = Array.from(uploadDialog.querySelectorAll('button')).find(node => visible(node)
@@ -424,6 +469,31 @@
424
469
  }
425
470
  }
426
471
  }
472
+ const rootUploadIdentity = row => row?.querySelector?.('[data-file-type="doc"][data-file-id], [data-file-type="file"][data-file-id]')
473
+ ?.getAttribute?.('data-file-id') || '';
474
+ function rememberUploadParent(path, node, projectId) {
475
+ if (node && typeof node === 'object') uploadParentProofs.set(node, Object.freeze({
476
+ path, projectId, nativeId: path ? folderIdentity(node) : rootUploadIdentity(node)
477
+ }));
478
+ return node;
479
+ }
480
+ // Synchronous last-moment guard after any uploader await. Snapshot the
481
+ // identity at preparation, since React can reuse a DOM row for a new ID.
482
+ function isUploadParentCurrent(folderPath, parentNode) {
483
+ const target = normalizeSafeProjectPath(folderPath);
484
+ const proof = parentNode && uploadParentProofs.get(parentNode);
485
+ if ((!target && folderPath !== '') || !proof?.projectId || !proof.nativeId
486
+ || proof.path !== target || getProjectId() !== proof.projectId) return false;
487
+ const root = document.querySelector('[data-testid="file-tree-list-root"], .file-tree-list[role="tree"]');
488
+ if (!root?.children) return false;
489
+ const selected = Array.from(root.querySelectorAll?.('[role="treeitem"]') || [])
490
+ .filter(row => row.getAttribute('aria-selected') === 'true');
491
+ if (selected.length !== 1 || selected[0] !== parentNode) return false;
492
+ if (!target) return parentNode.closest?.('[role="tree"]') === root
493
+ && !parentNode.querySelector('[data-file-type="folder"]') && rootUploadIdentity(parentNode) === proof.nativeId;
494
+ return findFolder(target) === parentNode && folderIdentity(parentNode) === proof.nativeId;
495
+ }
496
+
427
497
  // Binary uploads use the same folder hierarchy as verified text uploads.
428
498
  // A folder is not a text document and must never use basename fallback.
429
499
  async function prepareUploadParent(folderPath, options = {}) {
@@ -454,7 +524,7 @@
454
524
  if (selected.length !== 1 || selected[0] !== anchor || anchor.closest('[role="tree"]') !== currentRoot) {
455
525
  throw new Error('Overleaf did not confirm the selected root upload parent.');
456
526
  }
457
- return anchor;
527
+ return rememberUploadParent('', anchor, projectId);
458
528
  }
459
529
  const expand = async path => {
460
530
  assertCurrent();
@@ -479,7 +549,8 @@
479
549
  const path = parts.slice(0, depth).join('/');
480
550
  assertCurrent();
481
551
  if (!findFolder(path) && options.createMissing === true) await createFolderWithDom(path, {
482
- isCurrent: () => { assertCurrent(); return true; }, onMutation: options.onMutation
552
+ isCurrent: () => { assertCurrent(); return true; }, onMutation: options.onMutation,
553
+ onCreatedDirectory: options.onCreatedDirectory
483
554
  });
484
555
  await expand(path);
485
556
  }
@@ -493,7 +564,7 @@
493
564
  if (selected.getAttribute('aria-selected') !== 'true') {
494
565
  throw new Error('Overleaf did not confirm the selected upload parent.');
495
566
  }
496
- return selected;
567
+ return rememberUploadParent(target, selected, projectId);
497
568
  }
498
569
 
499
570
  async function prepareEditor(params = {}) {
@@ -552,6 +623,15 @@
552
623
  const deadlineAt = Math.min(now() + 90000, Number.isFinite(options.deadlineAt) ? options.deadlineAt : Infinity);
553
624
  const preflightDeadlineAt = deadlineAt - 15000;
554
625
  const expected = options.expectedContent;
626
+ const finishCreatedDeletion = result => {
627
+ const proofs = operation.undoCreatedFile?.createdDirectories;
628
+ if (options.undoCreatedFile !== true || !Array.isArray(proofs) || !proofs.length) return result;
629
+ // The ZIP-confirmed file deletion completes its Undo checkpoint.
630
+ // Directory observations are retained separately and grant no deletion authority.
631
+ return { ...result, fileDeleted: true, directoryCleanup: {
632
+ ok: false, removed: [], remaining: proofs, changedDocument: false
633
+ } };
634
+ };
555
635
  const binary = operation.undoCreatedFile?.v === 1 && operation.undoCreatedFile.kind === 'binary'
556
636
  && options.undoCreatedFile === true && /^[a-f0-9]{64}$/i.test(options.expectedSha256 || '');
557
637
  const matchesExpected = async file => {
@@ -658,8 +738,8 @@
658
738
  if (!snapshotRouter?.fetchProjectZipSnapshot) throw new Error('Verified deletion support is unavailable.');
659
739
  const original = (await readServerFiles()).find(file => file.path === target);
660
740
  if (!original && options.undoCreatedFile === true && !resolveRow()) {
661
- return { ok: true, method: 'overleaf.native-created-file-delete', changedDocument: false,
662
- idempotent: true, verified: true, verification: 'overleaf-zip' };
741
+ return finishCreatedDeletion({ ok: true, method: 'overleaf.native-created-file-delete', changedDocument: false,
742
+ idempotent: true, verified: true, verification: 'overleaf-zip' });
663
743
  }
664
744
  const originalMatches = await matchesExpected(original);
665
745
  assertCurrent();
@@ -741,8 +821,8 @@
741
821
  for (let observation = 0; ; observation++) {
742
822
  const files = await readServerFiles();
743
823
  if (!files.some(file => file.path === target)) {
744
- return { ok: true, method: binary ? 'overleaf.native-created-file-delete' : 'overleaf.native-text-delete', changedDocument: true,
745
- verified: true, verification: 'overleaf-zip' };
824
+ return finishCreatedDeletion({ ok: true, method: binary ? 'overleaf.native-created-file-delete' : 'overleaf.native-text-delete', changedDocument: true,
825
+ verified: true, verification: 'overleaf-zip' });
746
826
  }
747
827
  await delay(Math.min(250 * (2 ** Math.min(observation, 3)), remainingMs()));
748
828
  }
@@ -776,7 +856,7 @@
776
856
  async function createFile(operation, options = {}) {
777
857
  return createTextFileWithDom(operation, options);
778
858
  }
779
- return { createFile, deleteFile, prepareEditor, ensureWriteMode, prepareUploadParent, findFolderNode: findFolder,
859
+ return { createFile, deleteFile, prepareEditor, ensureWriteMode, prepareUploadParent, isUploadParentCurrent, cleanupCreatedDirectories, findFolderNode: findFolder,
780
860
  beginCreateBatch, finishCreateBatch,
781
861
  cancelVerifications() { for (const controller of verificationReads) controller.abort(); } };
782
862
  }
@@ -129,12 +129,22 @@
129
129
  const mergedPrefix = 'native-merge:';
130
130
  const nativeKey = tuple => 'native:' + JSON.stringify(tuple);
131
131
  const ledgerKeys = refs => refs.map(ref => ref.key).sort();
132
- const { mergedInsertionScope, prepareMergedInsertionReview, nativeOwned, rangesFor, shiftedBaseline, scopeCovered } = Ownership.create({
132
+ const { mergedInsertionScope, prepareMergedInsertionReview, nativeOwned, rangesFor, shiftedBaseline, scopeCovered, reviewNodeMatches } = Ownership.create({
133
133
  fingerprint, readActiveEditorText, mergedPrefix, nativeKey, ledgerKeys
134
134
  });
135
135
 
136
136
 
137
137
 
138
+ function isTrackedChangeNodeInNativeScope(node, path) {
139
+ if (typeof deps.getCodeMirrorEditorView !== 'function') return true;
140
+ return reviewNodeMatches(node, path, readNativeSnapshot(path), explicitId(node));
141
+ }
142
+ function rememberTrackedChangeNodeContext(path) {
143
+ if (typeof deps.getCodeMirrorEditorView !== 'function') return;
144
+ const snapshot = readNativeSnapshot(path);
145
+ if (snapshot.ready) for (const node of collectTrackedChangeNodes()) reviewNodeMatches(node, path, snapshot, explicitId(node));
146
+ }
147
+
138
148
  function prepareTrackedChangeReview(path, refs = []) {
139
149
  const snapshot = readNativeSnapshot(path);
140
150
  const blocked = reason => ({ ...snapshot, ok: false, reason });
@@ -321,7 +331,7 @@
321
331
  }
322
332
  return observe(capture, params.expectedContent, 1200);
323
333
  }
324
- return { collectTrackedChangeNodes, trackedChangeRefFromNode, collectTrackedChangeRefsForPaths,
334
+ return { collectTrackedChangeNodes, trackedChangeRefFromNode, collectTrackedChangeRefsForPaths, isTrackedChangeNodeInNativeScope, rememberTrackedChangeNodeContext,
325
335
  waitForTrackedChangeDiff, prepareTrackedChangeCapture, prepareTrackedChangeReview, prepareUntrackedUndo, getTrackedChangeCaptureStatus, captureTrackedWrite, reconcileTrackedChangeCapture };
326
336
  }
327
337
  return { create };
@@ -191,7 +191,25 @@
191
191
  }));
192
192
  }
193
193
 
194
- return { mergedInsertionScope, prepareMergedInsertionReview, nativeOwned, rangesFor, shiftedBaseline, scopeCovered };
194
+ const reviewNodeOwners = new WeakMap();
195
+ function reviewNodeMatches(node, path, snapshot, id = '') {
196
+ if (!node || snapshot?.ready !== true || !snapshot.nativeDocId) return false;
197
+ const explicitPath = node.getAttribute?.('data-path') || node.getAttribute?.('data-file-path') || node.getAttribute?.('data-doc-path');
198
+ if (explicitPath && explicitPath !== path) return false;
199
+ const rawPosition = node.getAttribute?.('data-pos'), from = Number(rawPosition);
200
+ const parts = Array.from(node.querySelectorAll?.('.review-panel-expandable-content') || []);
201
+ if (parts.length !== 1 || (!id && (rawPosition == null || rawPosition === '' || !Number.isSafeInteger(from) || from < 0))) return false;
202
+ const kind = /(?:delete|change-d)(?:\s|$)/.test(node.className || '') ? 'delete' : 'insert';
203
+ const textHash = fingerprint(parts[0].textContent || '');
204
+ if (!snapshot.refs.some(ref => (id ? ref.id === id : ref.from === from) && ref.kind === kind && ref.textHash === textHash)) return false;
205
+ const signature = JSON.stringify([id, from, kind, textHash]);
206
+ const prior = reviewNodeOwners.get(node);
207
+ if (prior && prior.docId !== snapshot.nativeDocId && prior.signature === signature) return false;
208
+ reviewNodeOwners.set(node, { docId: snapshot.nativeDocId, signature });
209
+ return true;
210
+ }
211
+
212
+ return { mergedInsertionScope, prepareMergedInsertionReview, nativeOwned, rangesFor, shiftedBaseline, scopeCovered, reviewNodeMatches };
195
213
  }
196
214
 
197
215
  return { create };
@@ -33,26 +33,17 @@
33
33
 
34
34
  // Preferred: re-apply the run's original forward patches verbatim.
35
35
  const originalPatches = patchesByPath.get(path);
36
- if (originalPatches && originalPatches.length && patchesApplyCleanly(preContent, originalPatches)) {
37
- operations.push({
38
- type: 'edit',
39
- path,
40
- patches: originalPatches,
41
- reason: 'Accept tracked edit (replay untracked)'
42
- });
43
- continue;
44
- }
45
-
46
36
  // Fallback: minimal pre->post diff trimmed to a single targeted patch.
47
- const diffPatch = buildMinimalDiffPatch(preContent, postContent);
48
- if (!diffPatch) {
37
+ const useOriginal = originalPatches?.length > 0 && applyTextPatches(preContent, originalPatches).ok === true;
38
+ const patches = useOriginal ? originalPatches : [buildMinimalDiffPatch(preContent, postContent)].filter(Boolean);
39
+ if (!patches.length) {
49
40
  // pre === post: nothing to replay for this file.
50
41
  continue;
51
42
  }
52
43
  operations.push({
53
44
  type: 'edit',
54
45
  path,
55
- patches: [diffPatch],
46
+ patches,
56
47
  reason: 'Accept tracked edit (replay untracked)'
57
48
  });
58
49
  }
@@ -87,13 +78,6 @@
87
78
  return patchesByPath;
88
79
  }
89
80
 
90
- // Confirms a patch set re-applies cleanly against the given (pre-write) text:
91
- // every patch range is valid and its `expected` slice matches.
92
- function patchesApplyCleanly(text, patches) {
93
- const applied = applyTextPatches(text, patches);
94
- return applied.ok === true;
95
- }
96
-
97
81
  // Computes a single targeted {from,to,insert} patch describing the pre->post
98
82
  // change by trimming the common prefix and suffix. Returns null when the two
99
83
  // strings are identical.
@@ -157,7 +141,43 @@
157
141
  };
158
142
  }
159
143
 
160
- return { buildAcceptReplayOperations, rebaseCheckpointPair };
144
+ function prepareSnapshotUndoCheckpoints(expectedFiles, postFiles) {
145
+ const contentByPath = files => new Map((files || [])
146
+ .filter(file => file?.path && typeof file.content === 'string')
147
+ .map(file => [normalizeSafeProjectPath(file.path), file.content])
148
+ .filter(([path]) => path));
149
+ const expectedByPath = contentByPath(expectedFiles);
150
+ const postByPath = contentByPath(postFiles);
151
+ return { expectedByPath, postByPath,
152
+ paths: Array.from(expectedByPath.keys()).filter(path => postByPath.has(path)) };
153
+ }
154
+
155
+ // Convert edit receipts back to the review checkpoint contract. Native
156
+ // identity checks and editor mutations remain owned by the lifecycle.
157
+ function buildSnapshotUndoResult(result, expectedByPath, postByPath, undoRebaseProofs) {
158
+ const toTrackedResult = item => ({
159
+ trackedChange: {
160
+ key: `snapshot-undo:${item.operation?.path || ''}`,
161
+ id: '',
162
+ path: item.operation?.path || '',
163
+ label: 'No-trace snapshot undo'
164
+ },
165
+ result: item.result?.ok !== false && item.result?.verified === true
166
+ && undoRebaseProofs.has(item.operation?.path)
167
+ && item.result.verifiedContent === expectedByPath.get(item.operation?.path)
168
+ ? { ...item.result, undoRebaseProof: undoRebaseProofs.get(item.operation.path) }
169
+ : item.result
170
+ });
171
+ return {
172
+ attempted: true,
173
+ applied: (result.applied || []).map(toTrackedResult),
174
+ skipped: (result.skipped || []).map(toTrackedResult),
175
+ rebasedExpectedFiles: Array.from(expectedByPath, ([path, content]) => ({ path, content })),
176
+ rebasedPostFiles: Array.from(postByPath, ([path, content]) => ({ path, content }))
177
+ };
178
+ }
179
+
180
+ return { buildAcceptReplayOperations, rebaseCheckpointPair, prepareSnapshotUndoCheckpoints, buildSnapshotUndoResult };
161
181
  }
162
182
 
163
183
  return { create };