codex-overleaf-link 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  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 +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  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 +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -0,0 +1,633 @@
1
+ (function (root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafRunReviewController = api;
5
+ })(typeof window !== 'undefined' ? window : globalThis, function () {
6
+ 'use strict';
7
+ function create(deps = {}) {
8
+ const {
9
+ Modules, WritebackSettlement, trackedChangeInFlight, findRunRecord,
10
+ getRunUndoCount, isTrackedChangeLifecycleRun, getPendingMirrorRefresh, hasTrackedEditorUndo,
11
+ buildNoTraceUndoRestore, findUnsafeFullFileUndoOperation, showUndoFileSelection, showPluginConfirm,
12
+ tr, truncateRunTitle, formatOperationFiles, formatTrackedChangeFiles,
13
+ formatTrackedUndoFiles, getPageBridgeTimeoutMs, callPageBridge, setRunUndoStatus,
14
+ appendRunRecordEvent, getRunProjectIdForWriteback, isUndoResultEffectivelyApplied, writebackOrchestrator,
15
+ appendUndoReviewingPolicyEvent, formatOperationType, formatApplyResultReason, buildTrackedUndoPostFiles,
16
+ getTrackedChangeCaptureController, buildContentFailure, applyTrackedChangeSettlement, refreshRunCardControls,
17
+ formatBridgeResultReason, advanceRunUndoProgress, saveStateSoon, flushQueuedSaveState,
18
+ getState, getCurrentProjectId, getAccountScopeId, getRouteGeneration, notify, tx,
19
+ } = deps;
20
+ const reviewOwners = new Map();
21
+ function reviewEventFailure(run, result) {
22
+ const reasons = deps.FailureReasons || Modules.FailureReasons;
23
+ if (!reasons?.normalizeFailureReason) return undefined;
24
+ const primary = reasons.selectPrimaryWritebackFailure(WritebackSettlement.collectFailuresFromResult(result, reasons));
25
+ if (!primary) return undefined;
26
+ const paths = new Set(['appliedOperations', 'undoOperations', 'undoBaseFiles', 'undoExpectedFiles', 'undoTrackedChanges']
27
+ .flatMap(key => (Array.isArray(run?.[key]) ? run[key] : []).map(item => item?.path)));
28
+ const candidate = typeof primary.file === 'string' ? primary.file : '';
29
+ const file = paths.has(candidate) && candidate.length <= 300 && !/[\r\n]/.test(candidate)
30
+ && !candidate.replace(/\\/g, '/').startsWith('/') && !/^[A-Za-z]:/.test(candidate) && !candidate.replace(/\\/g, '/').split('/').includes('..') ? candidate : '';
31
+ const code = Object.hasOwn(reasons.FAILURE_CODE_CATALOG, primary.code) ? primary.code : 'unknown_legacy_failure';
32
+ let displayCode = code;
33
+ reasons.formatFailureReasonForDisplay({ ...primary, userMessage: String(primary.userMessage || '').slice(0, 2000) }, undefined, key => {
34
+ const value = tr(key), match = /^failureReason_([a-z0-9_]+)_user$/.exec(key);
35
+ if (value && value !== key) { if (match) displayCode = match[1]; return value; }
36
+ });
37
+ // This event keeps only machine facts and owned UI copy, never the Page's
38
+ // arbitrary reason, technical message or evidence payload.
39
+ const safe = reasons.normalizeFailureReason({ code }, file ? { path: file } : undefined);
40
+ if (displayCode !== code) safe.evidence = { originalCode: displayCode };
41
+ const localized = reasons.localizeFailureReason(safe, undefined, key => { const value = tr(key, file ? undefined : { file: tr('unknownFile'), activeFile: tr('unknownFile') }); return value && value !== key ? value : undefined; });
42
+ safe.userMessage = localized.userMessage;
43
+ safe.nextAction = localized.nextAction || safe.nextAction;
44
+ if (displayCode === 'unknown_legacy_failure') {
45
+ safe.userMessage = tr('unknownReason'); safe.nextAction = tr('failureReason_review_detail_next');
46
+ }
47
+ return safe;
48
+ }
49
+ let undoRecoveryTimer = null, undoRecoveryView = null, undoRecoveryGeneration = 0;
50
+ const undoRecoveryFailures = new Map();
51
+ async function prepareUndoDispatch(runId) {
52
+ const owner = reviewOwners.get(runId);
53
+ if (owner && !owner.deadlineAt) await owner.markDispatched();
54
+ owner?.();
55
+ return Math.min(Date.now() + getPageBridgeTimeoutMs('rejectTrackedChanges'), owner?.deadlineAt || Infinity);
56
+ }
57
+ async function persistUndoOwnerMarker(runId) {
58
+ if (Modules.StorageDb?.sharedSessionsEnabled?.() !== true) return;
59
+ findRunRecord(runId).undoOwnerProtocol = 1;
60
+ try { await flushQueuedSaveState({ requireDurable: true, reviewRunIds: [runId] }); }
61
+ catch (cause) {
62
+ if (cause.code === 'shared_session_conflict' && cause.durableReviewRunIds?.includes(runId)) notify?.(cause.message);
63
+ else throw Object.assign(new Error('The Undo owner marker could not be durably saved.'), { code: 'review_state_not_persisted', cause });
64
+ }
65
+ reviewOwners.get(runId)?.();
66
+ const saved = await Modules.StorageDb.getRecord('sessions', getState().activeSessionId, { adopt: false });
67
+ if (saved?.runs?.find(run => run.id === runId)?.undoOwnerProtocol !== 1)
68
+ throw Object.assign(new Error('The Undo owner marker was not durably retained.'), { code: 'review_state_not_persisted' });
69
+ reviewOwners.get(runId)?.();
70
+ }
71
+ async function withSharedReviewOwnership(runId, work) {
72
+ const db = Modules.StorageDb;
73
+ if (db?.sharedSessionsEnabled?.() !== true) return work();
74
+ let run = findRunRecord(runId);
75
+ if (!run || trackedChangeInFlight.has(runId)) return;
76
+ const assertCurrent = undoOwner(run);
77
+ const sessionId = getState().activeSessionId;
78
+ return db.withSharedReviewLock({ projectId: getRunProjectIdForWriteback(run), accountScopeId: getAccountScopeId(), runId,
79
+ onLost: token => { void callPageBridge('cancelActiveWrite', { reviewOwnerToken: token }).catch(() => {}); } }, async assertOwner => {
80
+ reviewOwners.set(runId, assertOwner);
81
+ try {
82
+ assertCurrent();
83
+ // Only the run is refreshed here; keep the visible session's merge baseline.
84
+ const session = await db.getRecord('sessions', sessionId, { adopt: false });
85
+ run = assertCurrent();
86
+ const latest = session?.runs?.find(record => record.id === runId);
87
+ if (!latest || session.projectId !== getRunProjectIdForWriteback(run) || session.accountScopeId !== getAccountScopeId())
88
+ throw Object.assign(new Error('The shared Undo run is no longer available in its original scope.'), { code: 'review_context_changed' });
89
+ const restored = Modules.SessionState.normalizeRuns([latest], { recoverSettledRuns: true })[0];
90
+ // A budget-omitted first checkpoint may still be complete in its original page.
91
+ // Shared partial/terminal progress always wins over that in-memory copy.
92
+ const keepCheckpoint = restored.recoveryRetention?.state === 'omitted' && restored.recoveryRetention.coreRetained !== true
93
+ && !restored.undoStatus && !restored.trackedChangeStatus && !run.undoStatus && !run.trackedChangeStatus
94
+ && restored.runProjectId === run.runProjectId && restored.nativeEventSeq === run.nativeEventSeq
95
+ && restored.codexThreadId === run.codexThreadId && restored.codexTurnId === run.codexTurnId
96
+ && restored.startedAt === run.startedAt && JSON.stringify(restored.executionSnapshot) === JSON.stringify(run.executionSnapshot);
97
+ const checkpoint = keepCheckpoint ? Object.fromEntries(['appliedOperations', 'undoOperations', 'undoBaseFiles', 'undoTrackedChanges', 'undoExpectedFiles', 'trackedChangeCaptures']
98
+ .filter(key => Object.hasOwn(run, key)).map(key => [key, run[key]])) : {};
99
+ for (const key of Object.keys(run)) delete run[key];
100
+ Object.assign(run, restored, checkpoint);
101
+ if (run.undoStatus === 'applied' || ['accepted', 'rejected'].includes(run.trackedChangeStatus)) return;
102
+ if (run.undoStatus === 'running' && run.undoOwnerProtocol !== 1) run.undoRecoveryCheckRequired = true;
103
+ return await work();
104
+ } finally { reviewOwners.delete(runId); refreshRunCardControls(runId); }
105
+ }).catch(error => {
106
+ if (error.code === 'review_busy') notify?.(tx('Another page is reviewing this run. Wait for it to finish.', '另一页面正在审阅本轮记录,请等待其完成。'));
107
+ throw error;
108
+ });
109
+ }
110
+ async function recoverInterruptedUndoStatuses(options = {}) {
111
+ if (undoRecoveryTimer) { clearTimeout(undoRecoveryTimer); undoRecoveryTimer = null; }
112
+ const generation = ++undoRecoveryGeneration;
113
+ const captured = getState(), sessionId = captured.activeSessionId;
114
+ const scope = JSON.stringify([getCurrentProjectId?.(), getAccountScopeId?.(), getRouteGeneration?.()]);
115
+ const sameScope = () => JSON.stringify([getCurrentProjectId?.(), getAccountScopeId?.(), getRouteGeneration?.()]) === scope;
116
+ const current = () => generation === undoRecoveryGeneration && sameScope()
117
+ && getState() === captured && captured.activeSessionId === sessionId;
118
+ const schedule = delay => {
119
+ if (generation !== undoRecoveryGeneration || !sameScope()) return;
120
+ if (undoRecoveryTimer) clearTimeout(undoRecoveryTimer);
121
+ undoRecoveryTimer = setTimeout(() => {
122
+ if (generation === undoRecoveryGeneration && sameScope())
123
+ void recoverInterruptedUndoStatuses({ automatic: true }).catch(() => {});
124
+ }, delay);
125
+ undoRecoveryTimer.unref?.();
126
+ };
127
+ if (undoRecoveryView?.state !== captured || undoRecoveryView?.scope !== scope || undoRecoveryView?.sessionId !== sessionId) {
128
+ undoRecoveryFailures.clear(); undoRecoveryView = { state: captured, scope, sessionId };
129
+ }
130
+ if (options.automatic !== true) undoRecoveryFailures.clear();
131
+ let nextCheck = Infinity;
132
+ const db = Modules.StorageDb;
133
+ const runs = Array.from(new Set([...(captured.runs || []), ...(captured.sessions || []).flatMap(session => session.runs || [])]));
134
+ for (const run of runs) {
135
+ if (run.undoStatus !== 'running' || (undoRecoveryFailures.get(run.id) || 0) >= 3) continue;
136
+ if (db?.sharedSessionsEnabled?.() === true) {
137
+ let owner;
138
+ try {
139
+ owner = await db.hasSharedReviewOwner({ projectId: getRunProjectIdForWriteback(run), accountScopeId: getAccountScopeId(), runId: run.id });
140
+ if (!owner || typeof owner.busy !== 'boolean') throw new Error('Undo recovery ownership is unavailable.');
141
+ } catch (_error) {
142
+ if (!current()) { schedule(50); return; }
143
+ delete run.undoRecoveryCheckRequired; run.undoRecoveryUnavailable = true; refreshRunCardControls(run.id);
144
+ const attempts = (undoRecoveryFailures.get(run.id) || 0) + 1; undoRecoveryFailures.set(run.id, attempts);
145
+ if (attempts === 1) notify?.(tx('Undo recovery checks are temporarily unavailable. Other tasks remain available; keep the recovery files and retry after the connection recovers.',
146
+ '撤销恢复检查暂不可用,其他任务仍可继续。请保留恢复材料,连接恢复后再检查。'));
147
+ if (attempts < 3) nextCheck = Math.min(nextCheck, 5000 * attempts);
148
+ continue;
149
+ }
150
+ if (!current()) { schedule(50); return; }
151
+ undoRecoveryFailures.delete(run.id); delete run.undoRecoveryUnavailable;
152
+ if (!owner.busy && run.undoOwnerProtocol !== 1) {
153
+ run.undoRecoveryCheckRequired = true; refreshRunCardControls(run.id); continue;
154
+ }
155
+ if (owner.busy) { nextCheck = Math.min(nextCheck, owner.deadlineAt > Date.now() ? owner.deadlineAt - Date.now() + 50 : 5000); continue; }
156
+ }
157
+ // Recovery only restores an explicit action; Page work is never replayed.
158
+ delete run.undoRecoveryCheckRequired; run.undoStatus = 'partial'; refreshRunCardControls(run.id);
159
+ }
160
+ if (nextCheck !== Infinity) schedule(Math.min(30000, Math.max(50, nextCheck)));
161
+ }
162
+ async function withLocalUndoLock(runId, work) {
163
+ const run = findRunRecord(runId);
164
+ if (!run || run.forkSnapshot || run.undoStatus === 'applied' || !getRunUndoCount(run) || trackedChangeInFlight.has(runId)) return;
165
+ const assertOwner = undoOwner(run);
166
+ trackedChangeInFlight.set(runId, 'reject'); refreshRunCardControls(runId);
167
+ try { return await work(); }
168
+ catch (error) {
169
+ let current; try { current = assertOwner(); } catch (_stale) {}
170
+ const durableReviewConflict = error.code === 'shared_session_conflict' && error.durableReviewRunIds?.includes(runId);
171
+ if (current && error.code !== 'review_context_changed' && !durableReviewConflict) {
172
+ if (current.undoStatus === 'running') setRunUndoStatus(runId, 'partial');
173
+ appendRunRecordEvent(runId, { title: tr('undoNoTraceTitle'), status: 'failed',
174
+ detail: { [tr('detailReason')]: error.message } });
175
+ }
176
+ notify?.(error.code === 'review_state_not_persisted' ? tx('Undo recovery ownership could not be saved. No Undo was dispatched; keep this page open and inspect the files before retrying.',
177
+ '撤销恢复归属未能保存,尚未派发撤销。请保留本页并核对文件后重试。') : error.message);
178
+ throw error;
179
+ } finally { trackedChangeInFlight.delete(runId); refreshRunCardControls(runId); }
180
+ }
181
+ const withUndoLock = (runId, work) => withSharedReviewOwnership(runId, () => withLocalUndoLock(runId, work));
182
+ // Undo restores the source; with Auto compile on, refresh the PDF the way a write does.
183
+ async function recompileAfterUndo(runId, work) {
184
+ const before = findRunRecord(runId);
185
+ const prior = `${before?.undoStatus || ''}|${before?.trackedChangeStatus || ''}`;
186
+ const result = await work();
187
+ const run = findRunRecord(runId);
188
+ const undone = ['applied', 'partial'].includes(run?.undoStatus) || run?.trackedChangeStatus === 'rejected';
189
+ if (undone && `${run.undoStatus || ''}|${run.trackedChangeStatus || ''}` !== prior
190
+ && getState()?.autoRecompile !== false && run.runProjectId && run.runProjectId === getCurrentProjectId?.()) {
191
+ Promise.resolve(callPageBridge('triggerCompile', { preferUiClick: true, waitForSaveMs: 5000,
192
+ runProjectId: run.runProjectId })).catch(() => {});
193
+ }
194
+ return result;
195
+ }
196
+ // Inside the lock: a stale page that adopts another page's finished Undo never reaches its work.
197
+ const undoRun = runId => withUndoLock(runId, () => recompileAfterUndo(runId, () => executeUndo(runId)));
198
+ const acceptRun = runId => withSharedReviewOwnership(runId, () => executeAcceptRun(runId));
199
+ const undoRunTrackedChanges = (runId, run) => withUndoLock(runId,
200
+ () => recompileAfterUndo(runId, () => executeTrackedUndo(runId, run)));
201
+
202
+ function undoOwner(run) {
203
+ const sessionId = getState().activeSessionId;
204
+ const scope = [getCurrentProjectId?.(), getAccountScopeId?.(), getRouteGeneration?.()];
205
+ const provenance = value => JSON.stringify([value?.id, value?.runProjectId || '', value?.startedAt || '',
206
+ value?.runAccountScopeId || scope[1] || '', value?.nativeEventSeq || 0, value?.codexThreadId || '', value?.codexTurnId || '', value?.executionSnapshot || null]);
207
+ const sessionScope = () => {
208
+ const session = getState().sessions?.find(item => item.id === sessionId);
209
+ return JSON.stringify([session?.projectId || '', session?.accountScopeId || '']);
210
+ };
211
+ const source = provenance(run), owningSession = sessionScope();
212
+ return () => {
213
+ reviewOwners.get(run.id)?.();
214
+ const live = findRunRecord(run.id);
215
+ if (getState().activeSessionId !== sessionId || sessionScope() !== owningSession || !live
216
+ || !(live === run || Modules.SessionState?.sameRunIdentity?.(run, live)) || provenance(live) !== source
217
+ || JSON.stringify(scope) !== JSON.stringify([getCurrentProjectId?.(), getAccountScopeId?.(), getRouteGeneration?.()]))
218
+ throw Object.assign(new Error('The Undo project, account or session changed.'), { code: 'review_context_changed' });
219
+ return live;
220
+ };
221
+ }
222
+
223
+ function checkpointGuard(run, assertOwner) {
224
+ const snapshot = value => {
225
+ const normalized = Modules.SessionState?.normalizeRuns?.([value])?.[0] || value;
226
+ return JSON.stringify(['appliedOperations', 'undoOperations', 'undoBaseFiles', 'undoExpectedFiles',
227
+ 'undoTrackedChanges', 'trackedChangeCaptures', 'undoStatus', 'trackedChangeStatus', 'forkSnapshot']
228
+ .map(key => normalized[key]));
229
+ };
230
+ const expected = snapshot(run);
231
+ return () => {
232
+ const live = assertOwner();
233
+ if (snapshot(live) !== expected) throw Object.assign(new Error('The Undo project, account or session changed.'), { code: 'review_context_changed' });
234
+ return live;
235
+ };
236
+ }
237
+
238
+ function undoConfirmationTask(run) {
239
+ const task = truncateRunTitle(run.task);
240
+ return run.undoStatus === 'running' && run.undoOwnerProtocol !== 1
241
+ ? task + '\n\n' + tx('Before confirming Undo, close or reload every older project page and inspect the current files in Overleaf. Continue only after those checks are complete.',
242
+ '确认撤销前,请先关闭或重载全部旧版项目页面,并在 Overleaf 核对当前文件。完成这些检查后再继续。')
243
+ : task;
244
+ }
245
+
246
+ async function executeUndo(runId) {
247
+ let run = findRunRecord(runId);
248
+ if (run) Object.assign(run, Modules.UndoOperations.prepareRunUndo(run));
249
+ const assertCurrent = undoOwner(run);
250
+ let assertInputs = checkpointGuard(run, assertCurrent);
251
+ if (!getRunUndoCount(run) || run.undoStatus === 'applied') {
252
+ return;
253
+ }
254
+ // Wait out any background mirror refresh before undoing (v1.7.5): a
255
+ // mirror.sync finishing after the undo would push the pre-undo snapshot
256
+ // as the local baseline and resurrect the undone content next run.
257
+ const pendingMirror = getPendingMirrorRefresh?.();
258
+ if (pendingMirror) {
259
+ await pendingMirror.catch(() => {});
260
+ run = assertInputs();
261
+ }
262
+
263
+ if ((Array.isArray(run.undoTrackedChanges) && run.undoTrackedChanges.length) || hasTrackedEditorUndo(run)
264
+ || (run.trackedChangeStatus === 'needs_review' && run.undoOperations?.some(Modules.UndoOperations.isFileUndoOperation))) {
265
+ await executeTrackedUndo(runId, run);
266
+ return;
267
+ }
268
+
269
+ const undoRestore = buildNoTraceUndoRestore(run);
270
+ const undoOperations = undoRestore.operations;
271
+ const unsafeFullFileUndo = findUnsafeFullFileUndoOperation(undoOperations, {
272
+ allowSnapshotRestore: undoRestore.snapshotRestore
273
+ });
274
+ if (unsafeFullFileUndo) {
275
+ appendRunRecordEvent(runId, {
276
+ title: tr('undoUnsafeFullFileTitle'),
277
+ status: 'failed',
278
+ detail: {
279
+ [tr('detailFile')]: unsafeFullFileUndo.path,
280
+ [tr('detailReason')]: tr('undoUnsafeFullFileReason')
281
+ }
282
+ });
283
+ return;
284
+ }
285
+
286
+ // v1.8.0 C3: multi-file undos confirm through a per-file selection —
287
+ // undoing a subset leaves undoStatus 'partial' and the button usable;
288
+ // re-undoing an already-restored file is skipped by the base check.
289
+ const undoPaths = Array.from(new Set(undoOperations.map(operation => operation?.path).filter(Boolean)));
290
+ let selectedOperations = undoOperations;
291
+ if (undoPaths.length > 1 && typeof showUndoFileSelection === 'function') {
292
+ const selectedPaths = await showUndoFileSelection({
293
+ title: tr('undoFileSelectTitle'),
294
+ task: undoConfirmationTask(run),
295
+ confirmLabel: tr('undoFileSelectConfirm'),
296
+ cancelLabel: tr('confirmDefaultCancel'),
297
+ selectAllLabel: tr('undoFileSelectAll'),
298
+ paths: undoPaths
299
+ });
300
+ if (!selectedPaths) {
301
+ return;
302
+ }
303
+ const selectedSet = new Set(selectedPaths);
304
+ selectedOperations = undoOperations.filter(operation => selectedSet.has(operation?.path));
305
+ } else {
306
+ const approved = await showPluginConfirm({
307
+ title: tr('undoNoTraceTitle'),
308
+ message: [
309
+ undoConfirmationTask(run),
310
+ '',
311
+ tr('undoNoTraceMessage', { files: formatOperationFiles(undoOperations) })
312
+ ].join('\n'),
313
+ confirmLabel: tr('undoConfirm'),
314
+ cancelLabel: tr('confirmDefaultCancel'),
315
+ destructive: true
316
+ });
317
+ if (!approved) {
318
+ return;
319
+ }
320
+ }
321
+
322
+ run = assertInputs();
323
+ const deadlineAt = await prepareUndoDispatch(runId);
324
+ run = assertInputs();
325
+ setRunUndoStatus(runId, 'running');
326
+ appendRunRecordEvent(runId, {
327
+ title: tr('undoNoTraceStarted'),
328
+ status: 'running',
329
+ detail: { [tr('detailWillUndo')]: formatOperationFiles(selectedOperations) }
330
+ });
331
+
332
+ assertInputs = checkpointGuard(run, assertCurrent);
333
+ await persistUndoOwnerMarker(runId); run = assertInputs();
334
+ const result = await callPageBridge('applyOperations', {
335
+ operations: selectedOperations, deadlineAt, reviewOwnerToken: reviewOwners.get(runId)?.token,
336
+ baseFiles: run.undoBaseFiles || [],
337
+ reviewingPolicy: 'no-trace-undo',
338
+ // Undo remains bound to its original project, including after navigation.
339
+ runProjectId: getRunProjectIdForWriteback(run)
340
+ });
341
+ run = assertInputs();
342
+ const undoApplied = isUndoResultEffectivelyApplied(run, result);
343
+ writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
344
+ appendUndoReviewingPolicyEvent(runId, result.reviewingPolicy);
345
+ appendRunRecordEvent(runId, {
346
+ title: tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
347
+ failure: reviewEventFailure(run, result),
348
+ status: undoApplied ? 'completed' : 'failed',
349
+ detail: {
350
+ [tr('detailUndone')]: (result.applied || []).map(item => ({
351
+ [tr('detailAction')]: formatOperationType(item.operation?.type),
352
+ [tr('detailFile')]: item.operation?.path
353
+ })),
354
+ [tr('detailSkipped')]: (result.skipped || []).map(item => ({
355
+ [tr('detailAction')]: formatOperationType(item.operation?.type),
356
+ [tr('detailFile')]: item.operation?.path,
357
+ [tr('detailReason')]: formatApplyResultReason(item)
358
+ }))
359
+ }
360
+ });
361
+ // A subset undo never closes the ledger: unselected files can still
362
+ // be undone later, so the run stays 'partial' and the button usable.
363
+ const fullSelection = selectedOperations.length === undoOperations.length;
364
+ await applyLegacyUndoSettlement(
365
+ runId,
366
+ undoApplied && fullSelection ? 'applied' : 'partial',
367
+ result
368
+ );
369
+ }
370
+
371
+ async function executeTrackedUndo(runId, run) {
372
+ const assertCurrent = undoOwner(run);
373
+ let assertInputs = checkpointGuard(run, assertCurrent);
374
+ const fileUndoOperations = Modules.UndoOperations.getFileUndoOperations(run);
375
+ const textRun = Modules.UndoOperations.textUndoRun(run);
376
+ const trackedUndo = textRun.undoTrackedChanges.length > 0;
377
+ const hasTextUndo = trackedUndo || buildTrackedUndoPostFiles(textRun).length > 0;
378
+ const approved = await showPluginConfirm({
379
+ title: !hasTextUndo ? tr('undoNoTraceTitle') : trackedUndo ? tr('undoTrackedTitle') : tr('undoNativeTitle'),
380
+ message: [
381
+ undoConfirmationTask(run),
382
+ '',
383
+ ...(hasTextUndo ? [trackedUndo
384
+ ? tr('undoTrackedMessage', { files: formatTrackedChangeFiles(run.undoTrackedChanges) })
385
+ : tr('undoNativeMessage', { files: formatTrackedUndoFiles(run) })] : []),
386
+ ...(fileUndoOperations.length ? [tr('undoNoTraceMessage', { files: formatOperationFiles(fileUndoOperations) })] : [])
387
+ ].join('\n'),
388
+ confirmLabel: trackedUndo ? tr('undoTrackedConfirm') : tr('undoConfirm'),
389
+ cancelLabel: tr('confirmDefaultCancel'),
390
+ destructive: true
391
+ });
392
+ if (!approved) {
393
+ return;
394
+ }
395
+
396
+ run = assertInputs();
397
+ const deadlineAt = await prepareUndoDispatch(runId);
398
+ run = assertInputs();
399
+ // Keep the review lifecycle until every pending part has settled.
400
+ // Its local lock stays active while verified recovery progress is persisted.
401
+ const lifecycleReject = (trackedUndo || fileUndoOperations.length > 0) && isTrackedChangeLifecycleRun(run);
402
+ if (lifecycleReject) {
403
+ trackedChangeInFlight.set(runId, 'reject');
404
+ refreshRunCardControls(runId);
405
+ } else {
406
+ setRunUndoStatus(runId, 'running');
407
+ }
408
+ appendRunRecordEvent(runId, {
409
+ title: !hasTextUndo ? tr('undoNoTraceStarted') : trackedUndo ? tr('undoTrackedStarted') : tr('undoNativeStarted'),
410
+ status: 'running',
411
+ detail: { [tr('detailWillUndo')]: !hasTextUndo ? formatOperationFiles(fileUndoOperations)
412
+ : trackedUndo ? formatTrackedChangeFiles(run.undoTrackedChanges) : formatTrackedUndoFiles(run) }
413
+ });
414
+
415
+ let result;
416
+ try {
417
+ assertInputs = checkpointGuard(run, assertCurrent);
418
+ await persistUndoOwnerMarker(runId); run = assertInputs();
419
+ const trackedUndoPostFilesAtDispatch = buildTrackedUndoPostFiles(textRun);
420
+ result = hasTextUndo ? await callPageBridge('rejectTrackedChanges', {
421
+ trackedChanges: textRun.undoTrackedChanges || [], deadlineAt, reviewOwnerToken: reviewOwners.get(runId)?.token,
422
+ expectedFiles: textRun.undoExpectedFiles || [],
423
+ postFiles: trackedUndoPostFilesAtDispatch,
424
+ untrackedUndo: run.executionSnapshot
425
+ ? run.executionSnapshot.requireReviewing === false && ['submitted', 'legacy-captured'].includes(run.executionSnapshot.source)
426
+ : getTrackedChangeCaptureController().hasLegacyUntrackedCheckpoint(run),
427
+ // The page-side project guard still refuses writes after navigation.
428
+ runProjectId: getRunProjectIdForWriteback(run)
429
+ }) : { ok: true, applied: [], skipped: [] };
430
+ run = assertInputs();
431
+ if (hasTextUndo) WritebackSettlement.attachUndoNotVerifiedFailure(textRun, result, {
432
+ buildFailure: buildContentFailure, postFiles: trackedUndoPostFilesAtDispatch });
433
+ if (fileUndoOperations.length && lifecycleReject && hasTextUndo) {
434
+ WritebackSettlement.attachUndoNotVerifiedFailure(textRun, result, { buildFailure: buildContentFailure, postFiles: trackedUndoPostFilesAtDispatch });
435
+ await applyTrackedChangeSettlement(runId, 'reject', { ...result, ok: false }, { assertCurrent });
436
+ run = assertCurrent(); assertInputs = checkpointGuard(run, assertCurrent);
437
+ }
438
+ if (fileUndoOperations.length) {
439
+ let createdResult;
440
+ try {
441
+ if (result.ok === false || result.skipped?.length) {
442
+ throw new Error('Text changes remain unresolved; file restoration was kept for the next undo attempt.');
443
+ }
444
+ createdResult = await callPageBridge('applyOperations', {
445
+ operations: fileUndoOperations, reviewOwnerToken: reviewOwners.get(runId)?.token, deadlineAt,
446
+ baseFiles: run.undoBaseFiles || [],
447
+ reviewingPolicy: 'no-trace-undo',
448
+ runProjectId: getRunProjectIdForWriteback(run)
449
+ });
450
+ } catch (error) {
451
+ createdResult = { ok: false, applied: [], skipped: fileUndoOperations.map(operation => ({
452
+ operation, result: { ok: false, code: 'undo_operation_failed', reason: error.message }
453
+ })) };
454
+ }
455
+ run = assertInputs();
456
+ result = Modules.UndoOperations.mergeFileUndoResult(result, createdResult, fileUndoOperations);
457
+ }
458
+ appendRunRecordEvent(runId, {
459
+ title: fileUndoOperations.length ? tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }) : trackedUndo
460
+ ? tr('undoTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 })
461
+ : tr('undoNativeResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
462
+ failure: reviewEventFailure(run, result),
463
+ status: result.ok === true && !result.skipped?.length ? 'completed' : 'failed',
464
+ detail: {
465
+ [trackedUndo && !fileUndoOperations.length ? tr('detailRejected') : tr('detailUndone')]: (result.applied || []).map(item => ({
466
+ [tr('detailFile')]: item.trackedChange?.path || item.operation?.path || tr('unknownFile'),
467
+ [tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key || item.operation?.type
468
+ })),
469
+ [tr('detailSkipped')]: (result.skipped || []).map(item => ({
470
+ [tr('detailFile')]: item.trackedChange?.path || item.operation?.path || tr('unknownFile'),
471
+ [tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key || item.operation?.type || '',
472
+ [tr('detailReason')]: formatBridgeResultReason(item.result, item.trackedChange?.path || item.operation?.path)
473
+ }))
474
+ }
475
+ });
476
+ if (lifecycleReject) {
477
+ WritebackSettlement.attachUndoNotVerifiedFailure(textRun, result, {
478
+ buildFailure: buildContentFailure,
479
+ postFiles: trackedUndoPostFilesAtDispatch
480
+ });
481
+ await applyTrackedChangeSettlement(runId, 'reject', result, { assertCurrent });
482
+ return;
483
+ }
484
+ await applyLegacyUndoSettlement(runId, result.ok === true && !result.skipped?.length ? 'applied' : 'partial', result);
485
+ } finally {
486
+ // Known Page changes require invalidation even when a durable checkpoint fails.
487
+ if (result) writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
488
+ }
489
+ }
490
+
491
+ function reviewOwner(run) {
492
+ const projectId = getCurrentProjectId(), accountScopeId = getAccountScopeId();
493
+ const assertRun = undoOwner(run);
494
+ const assert = () => {
495
+ let live;
496
+ try { live = assertRun(); } catch (error) {
497
+ if (error.code === 'review_context_changed') error.message = 'The original review project, account or session changed.';
498
+ throw error;
499
+ }
500
+ if (!projectId || !accountScopeId || live.runProjectId !== projectId
501
+ || (live.runAccountScopeId && live.runAccountScopeId !== accountScopeId))
502
+ throw Object.assign(new Error('The original review project, account or session changed.'), { code: 'review_context_changed' });
503
+ return live;
504
+ };
505
+ assert(); return { projectId, accountScopeId, assert };
506
+ }
507
+
508
+ async function executeAcceptRun(runId) {
509
+ let run = findRunRecord(runId);
510
+ if (!run || !isTrackedChangeLifecycleRun(run)) {
511
+ return;
512
+ }
513
+ const status = run.trackedChangeStatus || '';
514
+ // pending and needs_review are both actionable per §7: needs_review means
515
+ // the prior attempt could not prove a clean post-action state, and the
516
+ // user is supposed to be able to retry after inspecting Overleaf.
517
+ if (status !== 'pending' && status !== 'needs_review') {
518
+ return;
519
+ }
520
+ if (!Array.isArray(run.undoTrackedChanges) || !run.undoTrackedChanges.length) {
521
+ return;
522
+ }
523
+ if (trackedChangeInFlight.get(runId)) {
524
+ return;
525
+ }
526
+
527
+ const owner = reviewOwner(run);
528
+ const assertInputs = checkpointGuard(run, owner.assert);
529
+ trackedChangeInFlight.set(runId, 'accept');
530
+ refreshRunCardControls(runId);
531
+ appendRunRecordEvent(runId, {
532
+ title: tr('runAcceptTrackedStarted'),
533
+ status: 'running',
534
+ detail: { [tr('detailAccepted')]: formatTrackedChangeFiles(run.undoTrackedChanges) }
535
+ });
536
+
537
+ try {
538
+ const deadlineAt = await prepareUndoDispatch(runId);
539
+ run = assertInputs();
540
+ const result = await callPageBridge('acceptTrackedChanges', { deadlineAt, reviewOwnerToken: reviewOwners.get(runId)?.token,
541
+ trackedChanges: run.undoTrackedChanges || [],
542
+ expectedFiles: run.undoExpectedFiles || [],
543
+ postFiles: buildTrackedUndoPostFiles(run),
544
+ appliedOperations: Array.isArray(run.appliedOperations) ? run.appliedOperations : [],
545
+ runProjectId: owner.projectId, accountScopeId: owner.accountScopeId
546
+ });
547
+ run = assertInputs();
548
+ appendAcceptDiagnosticEvents(runId, Array.isArray(result.diagnostics) ? result.diagnostics : []);
549
+ appendRunRecordEvent(runId, {
550
+ title: tr('runAcceptTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
551
+ failure: reviewEventFailure(run, result),
552
+ status: result.ok === true && !result.skipped?.length ? 'completed' : 'failed',
553
+ detail: {
554
+ [tr('detailAccepted')]: (result.applied || []).map(item => ({
555
+ [tr('detailFile')]: item.trackedChange?.path || tr('unknownFile'),
556
+ [tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key
557
+ })),
558
+ [tr('detailSkipped')]: (result.skipped || []).map(item => ({
559
+ [tr('detailFile')]: item.trackedChange?.path || tr('unknownFile'),
560
+ [tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key || '',
561
+ [tr('detailReason')]: formatBridgeResultReason(item.result, item.trackedChange?.path)
562
+ }))
563
+ }
564
+ });
565
+ if (result.ok === false) {
566
+ appendRunRecordEvent(runId, {
567
+ title: tr('runAcceptTrackedFailed'),
568
+ status: 'failed',
569
+ detail: {
570
+ [tr('detailReason')]: tr('runAcceptTrackedFailedReason')
571
+ }
572
+ });
573
+ if (!WritebackSettlement.isSuccessfulTrackedChangeSettlement(result)) {
574
+ refreshRunCardControls(runId);
575
+ }
576
+ }
577
+ WritebackSettlement.attachAcceptNotVerifiedFailure(run, result, {
578
+ buildFailure: buildContentFailure
579
+ });
580
+ await applyTrackedChangeSettlement(runId, 'accept', result, { assertCurrent: owner.assert });
581
+ owner.assert();
582
+ } catch (error) { notify?.(error.message); throw error; }
583
+ finally {
584
+ trackedChangeInFlight.delete(runId);
585
+ refreshRunCardControls(runId);
586
+ }
587
+ }
588
+
589
+ function appendAcceptDiagnosticEvents(runId, diagnostics) {
590
+ for (const entry of diagnostics || []) {
591
+ const step = entry && entry.step;
592
+ const info = (entry && entry.info) || {};
593
+ const titleKey = WritebackSettlement.ACCEPT_DIAGNOSTIC_TITLE_KEYS[step];
594
+ if (!titleKey) {
595
+ continue;
596
+ }
597
+ const title = titleKey === 'runAcceptTrackedStepReplayStart' || titleKey === 'runAcceptTrackedStepReplayDone'
598
+ ? tr(titleKey, { path: info.path || tr('unknownFile') })
599
+ : tr(titleKey);
600
+ const status = WritebackSettlement.acceptDiagnosticStatus(step, info);
601
+ appendRunRecordEvent(runId, {
602
+ title,
603
+ kind: 'activity',
604
+ status,
605
+ detail: info
606
+ });
607
+ }
608
+ }
609
+
610
+ async function applyLegacyUndoSettlement(runId, undoStatus, result) {
611
+ const run = findRunRecord(runId);
612
+ if (!run) return;
613
+ const assertCurrent = undoOwner(run);
614
+ const checkpoint = advanceRunUndoProgress(run, result);
615
+ const transition = WritebackSettlement.applySettlementTransition(
616
+ checkpoint,
617
+ WritebackSettlement.settleLegacyUndo({
618
+ run: checkpoint,
619
+ status: undoStatus,
620
+ result
621
+ })
622
+ );
623
+ await Modules.ScopedPersistenceCoordinator.applyReviewTransition({ getState, runId, transition,
624
+ legacyUndo: true, persist: flushQueuedSaveState, assertCurrent, SessionState: Modules.SessionState });
625
+ refreshRunCardControls(runId);
626
+ }
627
+ // From the shared review lock until settlement, a replaced view would
628
+ // invalidate the action's ownership checks after Page effects.
629
+ const hasActiveReview = () => reviewOwners.size > 0;
630
+ return { recoverInterruptedUndoStatuses, undoRun, undoRunTrackedChanges, acceptRun, applyLegacyUndoSettlement, hasActiveReview };
631
+ }
632
+ return { create };
633
+ });