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
@@ -1564,15 +1564,6 @@
1564
1564
  };
1565
1565
  }
1566
1566
 
1567
- function fileTreeMethodNames(type) {
1568
- return {
1569
- create: ['createDoc', 'createFile', 'addDoc', 'addFile'],
1570
- rename: ['renameEntity', 'renameFile', 'renameDoc'],
1571
- move: ['moveEntity', 'moveFile', 'moveDoc'],
1572
- delete: ['deleteEntity', 'deleteFile', 'deleteDoc', 'removeEntity']
1573
- }[type] || [];
1574
- }
1575
-
1576
1567
  function resolveTimeoutMs(options = {}, fallback) {
1577
1568
  const value = Number(options.timeoutMs);
1578
1569
  return Number.isFinite(value) && value >= 0 ? value : fallback;
@@ -1615,5 +1606,11 @@
1615
1606
  return text;
1616
1607
  }
1617
1608
 
1618
- return { create };
1609
+ function fileTreeMethodNames(type) {
1610
+ return {
1611
+ create: ['createDoc', 'createFile', 'addDoc', 'addFile'], rename: ['renameEntity', 'renameFile', 'renameDoc'],
1612
+ move: ['moveEntity', 'moveFile', 'moveDoc'], delete: ['deleteEntity', 'deleteFile', 'deleteDoc', 'removeEntity']
1613
+ }[type] || [];
1614
+ }
1615
+ return { create, fileTreeMethodNames };
1619
1616
  });
@@ -0,0 +1,41 @@
1
+ (function installUpdateSafetyProbe(root) {
2
+ 'use strict';
3
+ if (root.CodexOverleafUpdateSafetyProbe?.revision === '1') return;
4
+ const identities = new WeakMap();
5
+ function identity(value) {
6
+ if (!identities.has(value)) identities.set(value, crypto.randomUUID());
7
+ return identities.get(value);
8
+ }
9
+ // The same full CodeMirror document sources used by pageBridge. No DOM
10
+ // viewport text is eligible for the quiet-save fallback.
11
+ function editorView() {
12
+ const store = root.overleaf?.unstable?.store || root.Overleaf?.unstable?.store;
13
+ const get = key => { try { return store?.get?.(key) || {}; } catch (_) { return {}; } };
14
+ return [get('editor.view'), get('editor.view.camel'), get('editor').view,
15
+ root.overleaf?.unstable?.editorView, root.Overleaf?.editorView, root._ide?.editorView]
16
+ .find(value => value && typeof value.dispatch === 'function' && value.state?.doc
17
+ && typeof value.state.doc.toString === 'function') || null;
18
+ }
19
+ function fileIdentity() {
20
+ const store = root.overleaf?.unstable?.store || root.Overleaf?.unstable?.store;
21
+ const get = key => { try { return store?.get?.(key); } catch (_) { return ''; } };
22
+ const selected = document.querySelector('[role="treeitem"][aria-selected="true"] [data-file-id]');
23
+ return [get('editor.open_doc_id'), get('editor.openDocId'), root._ide?.editor?.open_doc_id,
24
+ root._ide?.editor?.openDocId, selected?.getAttribute('data-file-id'),
25
+ get('editor.open_doc_name'), get('editor.openDocName')].find(value => typeof value === 'string' && value) || '';
26
+ }
27
+ async function probe(expectedUrl) {
28
+ const view = editorView(), doc = view?.state?.doc, file = fileIdentity();
29
+ if (location.href !== expectedUrl || !file || !root.CodexOverleafSaveState) return { ok: false, state: 'unavailable' };
30
+ const save = root.CodexOverleafSaveState.create({ window: root, document,
31
+ delay: ms => new Promise(resolve => setTimeout(resolve, ms)),
32
+ detectEditor: () => ({ ok: Boolean(view && editorView() === view && view.state.doc === doc) }),
33
+ getActiveFilePath: () => doc ? identity(doc) : '', readActiveEditorText: () => view?.state.doc.toString() || '' });
34
+ const result = await save.waitForSaveState({ deadlineMs: 1500, pollIntervalMs: 100,
35
+ requirePositiveSignal: true, allowQuietEditorFallback: Boolean(doc), quietFallbackMs: 1000 });
36
+ if (location.href !== expectedUrl || fileIdentity() !== file || editorView() !== view
37
+ || (view && view.state.doc !== doc)) return { ok: false, state: 'unavailable' };
38
+ return { ok: result.ok === true, state: result.state, identity: file + ':' + (doc ? identity(doc) : 'positive-save-signal') };
39
+ }
40
+ root.CodexOverleafUpdateSafetyProbe = Object.freeze({ revision: '1', probe });
41
+ })(globalThis);
@@ -87,8 +87,10 @@
87
87
  return applyOperations(params.operations || [], {
88
88
  baseFiles: params.baseFiles || null,
89
89
  reviewingPolicy: params.reviewingPolicy || '',
90
+ ...(params.reviewOwnerToken ? { reviewOwnerToken: params.reviewOwnerToken } : {}),
90
91
  requireReviewing: params.requireReviewing === true,
91
92
  requireEditing: params.requireEditing === true,
93
+ retryCreates: params.retryCreates === true,
92
94
  runProjectId: projectId,
93
95
  ...(params.deadlineAt == null ? {} : { deadlineAt: params.deadlineAt }),
94
96
  onOperationResult
@@ -1,11 +1,12 @@
1
1
  (function initCodexOverleafWritebackRouter(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory(require('./treeOperations'));
4
4
  } else {
5
- root.CodexOverleafWritebackRouter = factory();
5
+ root.CodexOverleafWritebackRouter = factory(root.CodexOverleafTreeOperations);
6
6
  }
7
- })(typeof window !== 'undefined' ? window : globalThis, function writebackRouterFactory() {
7
+ })(typeof window !== 'undefined' ? window : globalThis, function writebackRouterFactory(TreeOperations) {
8
8
  'use strict';
9
+ const { fileTreeMethodNames } = TreeOperations;
9
10
 
10
11
  function create(deps = {}) {
11
12
  const window = deps.window || (typeof globalThis !== 'undefined' ? globalThis : {});
@@ -358,7 +359,9 @@
358
359
 
359
360
  async function applyOperations(operations, options = {}) {
360
361
  if (options.reviewingPolicy === 'no-trace-undo') {
361
- return applyOperationsWithNoTraceUndo(operations, options);
362
+ const Context = window.CodexOverleafReviewContext || (typeof module === 'object' && module.exports ? require('./reviewContext') : null);
363
+ return Context.create({ window, now, readWriteCancellationSequence, getProjectId: () => treeOperations.getProjectId?.() }, options).run(
364
+ (scoped, input) => applyOperationsWithNoTraceUndo(operations, { ...input, reviewGuard: scoped.reviewGuard, reviewAwait: scoped.reviewAwait }));
362
365
  }
363
366
  const hasOperations = (operations || []).length > 0;
364
367
  const trackReviewingChanges = options.requireReviewing === true && hasOperations;
@@ -388,6 +391,7 @@
388
391
  }
389
392
  const result = await applyOperationsCore(operations, {
390
393
  ...options,
394
+ noTraceUndo: false,
391
395
  trackReviewingChanges
392
396
  });
393
397
  return reviewingPolicy
@@ -398,10 +402,12 @@
398
402
  : result;
399
403
  }
400
404
 
401
- async function confirmNoTraceUndoEditing() {
405
+ async function confirmNoTraceUndoEditing(options = {}) {
402
406
  let changed = false;
403
407
  let toggle = { ok: true, changed: false, enabled: false };
404
408
  for (let attempt = 0; attempt < 2; attempt++) {
409
+ const blocked = options.reviewGuard?.();
410
+ if (blocked) return blocked;
405
411
  if (!isEditingConfirmedForNoTraceUndo(getReviewingState({}))) {
406
412
  toggle = await setReviewingEnabled(false, { waitMs: 1800 });
407
413
  if (!toggle.ok && (!['editing_not_confirmed', 'reviewing_disable_failed'].includes(toggle.code)
@@ -418,8 +424,13 @@
418
424
  reason: 'Overleaf Editing mode did not remain confirmed before undo; no undo write was started.' };
419
425
  }
420
426
 
427
+ async function awaitReview(options, work) {
428
+ try { return options.reviewAwait ? await options.reviewAwait(work) : await work(); }
429
+ catch (error) { if (error?.reviewFailure) return error.reviewFailure; throw error; }
430
+ }
431
+
421
432
  async function applyOperationsWithNoTraceUndo(operations, options = {}) {
422
- const disabled = await confirmNoTraceUndoEditing();
433
+ const disabled = await awaitReview(options, () => confirmNoTraceUndoEditing(options));
423
434
  if (!disabled.ok) return buildNoTraceUndoBlockedResult(operations, disabled);
424
435
  const result = await applyOperationsCore(operations, {
425
436
  ...options, noTraceUndo: true, trackReviewingChanges: false
@@ -441,13 +452,7 @@
441
452
  const safeBaseFiles = normalizeBaseFilesForSafety(options.baseFiles);
442
453
  const baseFileLookup = window.CodexOverleafStaleGuard?.buildBaseFileLookup(safeBaseFiles);
443
454
  const baseBinaryFileLookup = buildBaseBinaryFileLookup(safeBaseFiles);
444
- // Mid-run project-change guard. The pageBridge wrapper already ran the
445
- // writeGuard once at entry, but each per-op step here can take 0.5-5s
446
- // (openFileByPath, waitForActiveEditorText, save-state verification),
447
- // so a multi-op write can span 10-30 seconds total. If the user SPA-
448
- // navigates to a different project mid-flight, the remaining operations
449
- // would land in the wrong project. Re-check the editor project before
450
- // each op and short-circuit the rest of the queue when it diverges.
455
+ // Recheck project identity between awaited operations and before each write.
451
456
  const runProjectId = typeof options.runProjectId === 'string' ? options.runProjectId : '';
452
457
  const writeGuardSurface = window.CodexOverleafWriteGuard?.create({
453
458
  window,
@@ -478,13 +483,17 @@
478
483
  ? await deps.beginTextCreateBatch(createCandidates, { isCurrent: () =>
479
484
  treeOperations.getProjectId?.() === runProjectId && readWriteCancellationSequence() === cancelBaselineSequence }) : null;
480
485
  const deferredCreates = [];
486
+ let haltRemainingOperations = false;
487
+ const skipTail = (operation, result) => operations.slice(operations.indexOf(operation) + 1)
488
+ .forEach(tail => skipped.push({ operation: normalizeOperationPaths(tail), result }));
481
489
  const reopenQueue = [];
482
490
  const deadlineFailure = () => Number.isFinite(options.deadlineAt) && now() >= options.deadlineAt
483
491
  ? { ok: false, code: 'writeback_deadline_exceeded', changedDocument: false,
484
492
  reason: 'The undo time budget expired before this operation could start.' } : null;
485
- const editOptions = () => ({ baseFileLookup, runProjectId,
493
+ const editOptions = () => ({ baseFileLookup, runProjectId, deadlineAt: options.deadlineAt,
494
+ reviewGuard: options.reviewGuard, reviewAwait: options.reviewAwait,
486
495
  recheckWriteProject: writeGuardSurface ? () => {
487
- const expired = deadlineFailure();
496
+ const expired = options.reviewGuard?.() || deadlineFailure();
488
497
  return expired ? { applied: [], skipped: [{ operation: {}, result: expired }] }
489
498
  : writeGuardSurface.runWriteGuard({ runProjectId });
490
499
  } : null,
@@ -499,13 +508,15 @@
499
508
  };
500
509
 
501
510
  for (const rawOperation of operations) {
511
+ const reviewBlock = options.reviewGuard?.();
512
+ if (reviewBlock) {
513
+ skipped.push({ operation: normalizeOperationPaths(rawOperation), result: reviewBlock });
514
+ skipTail(rawOperation, reviewBlock); break;
515
+ }
502
516
  // Stop undispatched work when cancellation arrives during an awaited operation.
503
517
  if (readWriteCancellationSequence() !== cancelBaselineSequence) {
504
518
  skipped.push({ operation: normalizeOperationPaths(rawOperation), result: cancelledSkipResult });
505
- const remainingForCancel = operations.slice(operations.indexOf(rawOperation) + 1);
506
- for (const tailOperation of remainingForCancel) {
507
- skipped.push({ operation: normalizeOperationPaths(tailOperation), result: cancelledSkipResult });
508
- }
519
+ skipTail(rawOperation, cancelledSkipResult);
509
520
  break;
510
521
  }
511
522
  // Re-check project ownership before this operation and preserve every skipped tail entry.
@@ -515,10 +526,7 @@
515
526
  // Keep the current path and the remaining operations attributable.
516
527
  const guardFailure = guardBlock.skipped[0].result;
517
528
  skipped.push({ operation: normalizeOperationPaths(rawOperation), result: guardFailure });
518
- const remaining = operations.slice(operations.indexOf(rawOperation) + 1);
519
- for (const tailOperation of remaining) {
520
- skipped.push({ operation: normalizeOperationPaths(tailOperation), result: guardFailure });
521
- }
529
+ skipTail(rawOperation, guardFailure);
522
530
  break;
523
531
  }
524
532
  }
@@ -538,7 +546,7 @@
538
546
  } else if (['binary-create', 'overwrite-binary'].includes(operation.type)) {
539
547
  result = await applyBinaryAssetOperation(operation, { baseFileLookup, baseBinaryFileLookup });
540
548
  } else if (['create', 'rename', 'move', 'delete'].includes(operation.type)) {
541
- result = await applyFileTreeOperation(operation, { baseFileLookup, runProjectId, deadlineAt: options.deadlineAt,
549
+ result = await applyFileTreeOperation(operation, { baseFileLookup, runProjectId, deadlineAt: options.deadlineAt, reviewGuard: options.reviewGuard, noTraceUndo: options.noTraceUndo === true,
542
550
  createBatch: createBatch?.targets.has(operation.path) ? createBatch : null,
543
551
  retryCreates: options.retryCreates === true });
544
552
  } else {
@@ -557,16 +565,21 @@
557
565
  continue;
558
566
  }
559
567
  record(operation, result);
568
+ if (operation.type === 'move' && operation.moveProof && !result.ok) {
569
+ haltRemainingOperations = true;
570
+ skipTail(rawOperation, { ok: false, code: 'file_plan_not_completed', changedDocument: false,
571
+ reason: 'The file move did not finish; the remaining file plan was not started.' }); break;
572
+ }
560
573
  }
561
574
 
562
575
  // A file that could not be opened mid-batch usually opens once the
563
576
  // previous document has settled: give untouched open failures one pass.
564
- if (reopenQueue.length && readWriteCancellationSequence() === cancelBaselineSequence
577
+ if (!haltRemainingOperations && reopenQueue.length && readWriteCancellationSequence() === cancelBaselineSequence
565
578
  && !skipped.some(entry => entry.result?.code === 'aborted_project_changed')) {
566
579
  await delay(deps.reopenRetryDelayMs ?? 1200);
567
580
  }
568
581
  for (const entry of reopenQueue) {
569
- const blocked = readWriteCancellationSequence() !== cancelBaselineSequence
582
+ const blocked = options.reviewGuard?.() || haltRemainingOperations || readWriteCancellationSequence() !== cancelBaselineSequence
570
583
  || skipped.some(item => item.result?.code === 'aborted_project_changed')
571
584
  || Boolean(runProjectId && writeGuardSurface && await writeGuardSurface.runWriteGuard({ runProjectId }));
572
585
  const result = blocked ? entry.result : await applyEditOperation(entry.operation, editOptions());
@@ -589,25 +602,26 @@
589
602
  ok: skipped.length === 0,
590
603
  applied,
591
604
  skipped,
605
+ ...(haltRemainingOperations ? { haltRemainingOperations: true } : {}),
592
606
  trackedChanges: trackedChangeCaptures.some(capture => capture.state !== 'observed') ? [] : mergeTrackedChangeRefs(trackedChanges),
593
607
  trackedChangeCaptures
594
608
  };
595
609
  }
596
610
 
597
- function isUntouchedOpenFailure(result) {
598
- return result?.ok === false && result.failure?.code === 'target_file_open_failed'
599
- && result.failure.changedDocument === false;
600
- }
611
+ const isUntouchedOpenFailure = result => result?.ok === false && result.failure?.code === 'target_file_open_failed'
612
+ && result.failure.changedDocument === false;
601
613
 
602
614
  async function applyEditOperation(operation, options = {}) {
615
+ const reviewBlock = options.reviewGuard?.();
616
+ if (reviewBlock) return reviewBlock;
603
617
  const currentPath = getActiveFilePath();
604
- const editorReady = await ensureEditorReadyForOperation(operation, options.baseFileLookup, currentPath);
618
+ const editorReady = await awaitReview(options, () => ensureEditorReadyForOperation(operation, options.baseFileLookup, currentPath));
605
619
  if (!editorReady.ok) {
606
620
  return editorReady;
607
621
  }
608
622
 
609
623
  if (options.noTraceUndo === true) {
610
- const editing = await confirmNoTraceUndoEditing();
624
+ const editing = await awaitReview(options, () => confirmNoTraceUndoEditing(options));
611
625
  if (!editing.ok) return buildNoTraceUndoBlockedResult([operation], editing).skipped[0].result;
612
626
  }
613
627
  const trackReviewingChanges = options.trackReviewingChanges === true;
@@ -625,7 +639,8 @@
625
639
  options.baseFileLookup
626
640
  ) || { ok: true };
627
641
  if (!freshness.ok) {
628
- const ready = await waitForFreshEditorTextForOperation(operation, options.baseFileLookup, 1500);
642
+ const ready = await awaitReview(options, () => waitForFreshEditorTextForOperation(operation, options.baseFileLookup, 1500));
643
+ if (options.reviewGuard?.()) return options.reviewGuard();
629
644
  if (!ready.ok) {
630
645
  const decorated = attachWritebackDebug(freshness, 'stale_guard', operation, options.baseFileLookup, current, {
631
646
  initialActivePath: currentPath,
@@ -665,8 +680,11 @@
665
680
  }
666
681
 
667
682
  let nextContent = null;
683
+ let preparedPatches;
668
684
  if (Array.isArray(operation.patches) && operation.patches.length) {
669
- const patched = applyTextPatches(current, operation.patches);
685
+ preparedPatches = window.CodexOverleafStaleGuard?.prepareOperationPatches(operation, current, options.baseFileLookup)
686
+ || { ok: true, patches: operation.patches };
687
+ const patched = preparedPatches.ok ? applyTextPatches(current, preparedPatches.patches) : preparedPatches;
670
688
  if (!patched.ok) {
671
689
  const decorated = attachWritebackDebug(patched, 'apply_text_patches', operation, options.baseFileLookup, current, {
672
690
  initialActivePath: currentPath,
@@ -729,14 +747,18 @@
729
747
  reason: 'Overleaf left Editing mode before the undo write.' }).skipped[0].result;
730
748
  }
731
749
 
750
+ const finalReviewBlock = options.reviewGuard?.();
751
+ if (finalReviewBlock) return finalReviewBlock;
732
752
  const result = Array.isArray(operation.patches) && operation.patches.length
733
- ? replaceActiveEditorPatches(operation.patches, nextContent)
753
+ ? replaceActiveEditorPatches(preparedPatches.patches, nextContent)
734
754
  : replaceActiveEditorText(nextContent);
735
755
  if (!result.ok) {
736
756
  return result;
737
757
  }
738
758
 
739
- const verified = await verifyActiveEditorText(nextContent, operation.path, deps.writeVerifyWaitMs ?? 2500, current);
759
+ const verifyWaitMs = Math.min(deps.writeVerifyWaitMs ?? 2500, Number.isFinite(options.deadlineAt)
760
+ ? Math.max(0, options.deadlineAt - now()) : Infinity);
761
+ const verified = await verifyActiveEditorText(nextContent, operation.path, verifyWaitMs, current);
740
762
  if (!verified.ok) {
741
763
  const decorated = attachWritebackDebug(verified, 'write_verification', operation, options.baseFileLookup, readActiveEditorText(), {
742
764
  initialActivePath: currentPath,
@@ -776,7 +798,7 @@
776
798
  // Slow batches are covered by request receipts; Accept replay retains
777
799
  // its independent safety window proving that no new tracking was added.
778
800
  const trackedDiff = trackReviewingChanges
779
- ? await captureTrackedWrite({ trackedBefore, captureBaseline, operation, beforeContent: current,
801
+ ? await captureTrackedWrite({ trackedBefore, captureBaseline, operation: { ...operation, patches: preparedPatches?.patches }, beforeContent: current,
780
802
  postContent: nextContent, runProjectId: options.runProjectId, waitMs: deps.trackCaptureWaitMs ?? 35000 })
781
803
  : await waitForTrackedChangeDiff(trackedBefore, operationPaths, {
782
804
  waitMs: 3600, intervalMs: 180, stopOnFirst: true
@@ -812,7 +834,7 @@
812
834
  window.CodexOverleafStaleGuard?.updateExpectedFileContent(
813
835
  options.baseFileLookup,
814
836
  operation.path,
815
- nextContent
837
+ preparedPatches?.sourceContent ?? nextContent
816
838
  );
817
839
  return {
818
840
  ...result,
@@ -823,7 +845,6 @@
823
845
  };
824
846
  }
825
847
 
826
-
827
848
  async function verifyActiveEditorText(expected, filePath, waitMs = 1000, before) {
828
849
  const deadline = Date.now() + waitMs;
829
850
  let actual = readActiveEditorText();
@@ -1149,7 +1170,6 @@
1149
1170
  };
1150
1171
  }
1151
1172
 
1152
-
1153
1173
  async function applyBinaryAssetOperation(operation, options = {}) {
1154
1174
  const freshness = await checkBinaryAssetFreshness(operation, options);
1155
1175
  if (!freshness.ok) {
@@ -1319,9 +1339,7 @@
1319
1339
  return baseFile.size == null || currentSize == null || baseFile.size === currentSize;
1320
1340
  }
1321
1341
 
1322
- function normalizeBase64(value) {
1323
- return String(value || '').replace(/\s+/g, '');
1324
- }
1342
+ const normalizeBase64 = value => String(value || '').replace(/\s+/g, '');
1325
1343
 
1326
1344
  function decodeBase64Bytes(contentBase64) {
1327
1345
  try {
@@ -1372,6 +1390,17 @@
1372
1390
  }
1373
1391
  const initialProjectId = treeOperations.getProjectId?.();
1374
1392
  const initialCancellationSequence = readWriteCancellationSequence();
1393
+ if (operation.type === 'move' && operation.moveProof) {
1394
+ if (options.noTraceUndo !== true) return { ok: false, code: 'file_tree_controls_unavailable', changedDocument: false,
1395
+ reason: 'Native file moves are available only to undo a previously recorded move.' };
1396
+ if (typeof deps.moveFile !== 'function') return { ok: false, code: 'file_move_unavailable', changedDocument: false,
1397
+ reason: 'Verified native file moves are unavailable. Reload the extension and Overleaf.' };
1398
+ const result = await deps.moveFile(operation, { noTraceUndo: true, runProjectId: options.runProjectId, deadlineAt: options.deadlineAt,
1399
+ isCurrent: () => treeOperations.getProjectId?.() === initialProjectId
1400
+ && readWriteCancellationSequence() === initialCancellationSequence && !options.reviewGuard?.() });
1401
+ if (result.ok) recordFileTreeOperationSuccess(operation, options.baseFileLookup);
1402
+ return result;
1403
+ }
1375
1404
  if (operation.type === 'create' && options.createBatch && typeof deps.createTextFile === 'function') {
1376
1405
  if (options.baseFileLookup?.has(operation.path) && options.retryCreates !== true) {
1377
1406
  return checkFileTreeOperationFreshness(operation, options.baseFileLookup);
@@ -1379,13 +1408,14 @@
1379
1408
  return deps.createTextFile(operation, { batch: options.createBatch,
1380
1409
  allowExistingMatches: options.retryCreates === true,
1381
1410
  isCurrent: () => treeOperations.getProjectId?.() === initialProjectId
1382
- && readWriteCancellationSequence() === initialCancellationSequence });
1411
+ && readWriteCancellationSequence() === initialCancellationSequence && !options.reviewGuard?.() });
1383
1412
  }
1384
1413
  const freshness = await checkFileTreeOperationFreshness(operation, options.baseFileLookup);
1385
1414
  if (!freshness.ok) {
1386
1415
  return freshness;
1387
1416
  }
1388
1417
 
1418
+ const ownerBlocked = options.reviewGuard?.(); if (ownerBlocked) return ownerBlocked;
1389
1419
  const manager = findFileTreeManager();
1390
1420
  const methodNames = fileTreeMethodNames(operation.type);
1391
1421
 
@@ -1432,7 +1462,7 @@
1432
1462
  const nativeOperation = { create: deps.createTextFile, delete: deps.deleteTextFile }[operation.type];
1433
1463
  if (typeof nativeOperation === 'function' && (operation.type === 'delete' || typeof operation.content === 'string')) {
1434
1464
  const isCurrent = () => Boolean(initialProjectId) && treeOperations.getProjectId?.() === initialProjectId
1435
- && readWriteCancellationSequence() === initialCancellationSequence;
1465
+ && readWriteCancellationSequence() === initialCancellationSequence && !options.reviewGuard?.();
1436
1466
  const created = await nativeOperation(operation, { isCurrent, expectedContent: options.baseFileLookup?.get(operation.path) });
1437
1467
  if (!created.ok) return created;
1438
1468
  if (!isCurrent() || created.verified !== true || created.verification !== 'overleaf-zip') {
@@ -1456,7 +1486,7 @@
1456
1486
  const text = proof?.v === 1 && proof.kind === 'text' && typeof expected === 'string';
1457
1487
  const binary = proof?.v === 1 && proof.kind === 'binary' && /^[a-f0-9]{64}$/i.test(proof.sha256 || '');
1458
1488
  const isCurrent = () => Boolean(projectId) && projectId === options.runProjectId
1459
- && treeOperations.getProjectId?.() === projectId && readWriteCancellationSequence() === cancellation;
1489
+ && treeOperations.getProjectId?.() === projectId && readWriteCancellationSequence() === cancellation && !options.reviewGuard?.();
1460
1490
  if (!isCurrent() || (!text && !binary) || typeof deps.deleteTextFile !== 'function') {
1461
1491
  return { ok: false, code: 'undo_operation_failed', changedDocument: false,
1462
1492
  reason: 'Verified created-file deletion is unavailable for this run.' };
@@ -1653,15 +1683,6 @@
1653
1683
  }
1654
1684
  }
1655
1685
 
1656
- function fileTreeMethodNames(type) {
1657
- return {
1658
- create: ['createDoc', 'createFile', 'addDoc', 'addFile'],
1659
- rename: ['renameEntity', 'renameFile', 'renameDoc'],
1660
- move: ['moveEntity', 'moveFile', 'moveDoc'],
1661
- delete: ['deleteEntity', 'deleteFile', 'deleteDoc', 'removeEntity']
1662
- }[type] || [];
1663
- }
1664
-
1665
1686
  function normalizeOperationPaths(operation) {
1666
1687
  if (!operation || typeof operation !== 'object') {
1667
1688
  return operation;
@@ -1734,10 +1755,6 @@
1734
1755
  }
1735
1756
 
1736
1757
 
1737
-
1738
-
1739
-
1740
-
1741
1758
  function normalizeTrackedChangeRefs(refs = []) {
1742
1759
  const seen = new Set();
1743
1760
  const normalized = [];
@@ -1796,23 +1813,12 @@
1796
1813
 
1797
1814
  async function applyOperationsForBridge(operationsOrOptions, options = {}) {
1798
1815
  if (Array.isArray(operationsOrOptions)) {
1799
- // Welcome-panel + write-guard: defense-in-depth
1800
- // also runs on the array-shaped entry. T2 only guarded the
1801
- // payload-object branch; if any future caller goes straight to the
1802
- // array entry without `options.runProjectId`, the router would have
1803
- // silently dispatched. Mirror the same `editor_project_id_unavailable`
1804
- // failure shape the payload branch and the page-side guard already
1805
- // emit.
1816
+ // Both public call shapes require the original project identity.
1806
1817
  const writeGuardBlock = checkWritebackRunProjectId(options);
1807
1818
  if (writeGuardBlock) return writeGuardBlock;
1808
1819
  return applyOperations(operationsOrOptions, options);
1809
1820
  }
1810
1821
  const payload = operationsOrOptions || {};
1811
- // Welcome-panel + write-guard: defense-in-depth.
1812
- // The pageBridge wrapper already runs the runProjectId guard, but a
1813
- // future caller could reach the router directly. A missing or empty
1814
- // `runProjectId` on the payload-shaped call blocks the write with the
1815
- // same `editor_project_id_unavailable` shape the page-side guard uses.
1816
1822
  const writeGuardBlock = checkWritebackRunProjectId(payload);
1817
1823
  if (writeGuardBlock) return writeGuardBlock;
1818
1824
  return applyOperations(payload.operations || [], payload);
@@ -1821,10 +1827,7 @@
1821
1827
  function checkWritebackRunProjectId(params) {
1822
1828
  const runProjectId = params && typeof params.runProjectId === 'string' ? params.runProjectId : '';
1823
1829
  if (runProjectId) return null;
1824
- // operation: {} (not null) — batch-level guard fired before any per-op
1825
- // dispatch; matches the abortDispatchResult shape in pageBridge.js and
1826
- // avoids crashing downstream audit / transcript code that reads
1827
- // operation.path without a null guard.
1830
+ // Keep operation.path safe for downstream readers of batch-level failures.
1828
1831
  return {
1829
1832
  ok: false,
1830
1833
  applied: [],
@@ -1916,9 +1919,7 @@
1916
1919
  };
1917
1920
  }
1918
1921
 
1919
- // Tracked-changes lifecycle — carved to trackedChangesLifecycle.js in
1920
- // v1.8.0 (structural-debt phase 7, #117). Loaded just before this file
1921
- // in the manifest's page-script list; the Node test path requires it.
1922
+ // Tracked lifecycle is loaded by the Page script list or required by Node tests.
1922
1923
  const trackedChangesLifecycleModule = (typeof window !== 'undefined' && window.CodexOverleafTrackedChangesLifecycle)
1923
1924
  || (typeof module === 'object' && module.exports ? require('./trackedChangesLifecycle.js') : null)
1924
1925
  || (typeof globalThis !== 'undefined' ? globalThis.CodexOverleafTrackedChangesLifecycle : null);
@@ -1930,7 +1931,7 @@
1930
1931
  captureTrackedWrite, reconcileTrackedChangeCapture,
1931
1932
  waitForTrackedChangeDiff
1932
1933
  } = trackedChangesLifecycleModule.create({
1933
- window, confirmReviewWriteback: deps.confirmReviewWriteback,
1934
+ window, now, readWriteCancellationSequence, confirmReviewWriteback: deps.confirmReviewWriteback,
1934
1935
  checkWritebackRunProjectId,
1935
1936
  buildNoTraceUndoBlockedResult,
1936
1937
  buildReviewingRequiredBlockedResult,
@@ -63,6 +63,7 @@
63
63
  window
64
64
  });
65
65
  snapshotRouter = requirePageModule('CodexOverleafSnapshotRouter').create({
66
+ getActiveEditorIdentity,
66
67
  normalizePath: normalizeSafeProjectPath,
67
68
  readActiveEditorText,
68
69
  treeOperations,
@@ -76,12 +77,17 @@
76
77
  });
77
78
  binaryAssetUploader = requirePageModule('CodexOverleafBinaryAssetUploader').create({
78
79
  prepareUploadParent: textFileCreator.prepareUploadParent,
80
+ cleanupCreatedDirectories: textFileCreator.cleanupCreatedDirectories,
81
+ isUploadParentCurrent: textFileCreator.isUploadParentCurrent,
79
82
  findFolderNode: textFileCreator.findFolderNode,
80
83
  document,
81
84
  snapshotRouter,
82
85
  treeOperations,
83
86
  window
84
87
  });
88
+ const fileMover = requirePageModule('CodexOverleafFileMover').create({
89
+ window, document, treeOperations, snapshotRouter, textFileCreator, normalizeSafeProjectPath
90
+ });
85
91
  writeGuard = requirePageModule('CodexOverleafWriteGuard').create({
86
92
  document,
87
93
  treeOperations,
@@ -102,6 +108,7 @@
102
108
  beginTextCreateBatch: textFileCreator.beginCreateBatch,
103
109
  finishTextCreateBatch: textFileCreator.finishCreateBatch,
104
110
  deleteTextFile: textFileCreator.deleteFile,
111
+ moveFile: fileMover.moveFile,
105
112
  activeEditorIdentityChanged,
106
113
  clickNode,
107
114
  collectElements,
@@ -221,7 +228,8 @@
221
228
 
222
229
  const pageRpcHandlers = {
223
230
  probe,
224
- cancelActiveWrite() {
231
+ cancelActiveWrite(params = {}) {
232
+ if (params.reviewOwnerToken) return { ok: true, cancelled: requirePageModule('CodexOverleafReviewContext').cancelOwner(params.reviewOwnerToken) };
225
233
  const sequence = bumpWriteCancellationSequence();
226
234
  textFileCreator.cancelVerifications();
227
235
  snapshotRouter.cancelWritebackReads();
@@ -690,7 +698,14 @@
690
698
  if (to - from > 20000) return { ok: false, code: 'selection_too_large' };
691
699
  const source = editorState.doc.toString();
692
700
  const text = source.slice(from, to);
693
- const digest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(source));
701
+ // UTF-8 replaces isolated surrogates; exclude them so flank hashes prove exact UTF-16 text.
702
+ if (/[\uD800-\uDFFF]/u.test(source) || /[\uD800-\uDFFF]/u.test(text)) return { ok: false, code: 'selection_unavailable' };
703
+ const hashText = async value => Array.from(new Uint8Array(
704
+ await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))
705
+ ), byte => byte.toString(16).padStart(2, '0')).join('');
706
+ const [documentHash, prefixHash, suffixHash] = await Promise.all([
707
+ hashText(source), hashText(source.slice(0, from)), hashText(source.slice(to))
708
+ ]);
694
709
  if (getProjectId() !== projectId || getActiveFilePath() !== path || getCodeMirrorEditorView() !== view
695
710
  || view.state.doc !== editorState.doc || view.state.selection.main.from !== from || view.state.selection.main.to !== to) {
696
711
  return { ok: false, code: 'selection_changed' };
@@ -698,7 +713,8 @@
698
713
  const captured = { projectId, path, from, to, text,
699
714
  lineStart: editorState.doc.lineAt(from).number,
700
715
  lineEnd: editorState.doc.lineAt(Math.max(from, to - 1)).number,
701
- documentHash: Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''),
716
+ documentHash,
717
+ rebaseEvidence: { v: 1, documentLength: source.length, prefixHash, suffixHash },
702
718
  capturedAt: new Date().toISOString() };
703
719
  let anchor = null;
704
720
  try {