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.
- package/README.md +155 -392
- package/README.zh-CN.md +154 -393
- package/extension/bootstrap/background.js +52 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.html +1 -0
- package/extension/bootstrap/popupFallback.js +55 -0
- package/extension/bootstrap/updateStatus.js +1 -0
- package/extension/src/background.js +283 -55
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +129 -16
- package/extension/src/backgroundUpdateCoordinator.js +242 -29
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +171 -8
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +104 -47
- package/extension/src/content/composerPanel.js +110 -12
- package/extension/src/content/contentRuntime.js +1555 -926
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +48 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +11077 -3412
- package/extension/src/content/generated/content.bundle.meta.json +14 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +10 -0
- package/extension/src/content/localSkillsPanel.js +104 -14
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +47 -11
- package/extension/src/content/modelPicker.js +8 -4
- package/extension/src/content/nativeCompatibilityController.js +74 -30
- package/extension/src/content/otWarmMirror.js +5 -6
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +17 -4
- package/extension/src/content/panelMaintenance.js +46 -14
- package/extension/src/content/panelRenderer.js +57 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +67 -6
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +73 -14
- package/extension/src/content/recentProjects.js +15 -8
- package/extension/src/content/runActivitySummary.js +160 -18
- package/extension/src/content/runCompletionMeta.js +532 -0
- package/extension/src/content/runController.js +39 -15
- package/extension/src/content/runFailureNotice.js +29 -13
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +157 -47
- package/extension/src/content/runRecoveryActions.js +147 -0
- package/extension/src/content/runResultActions.js +28 -143
- package/extension/src/content/runReviewController.js +633 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +367 -175
- package/extension/src/content/scopedPersistencePanelState.js +134 -20
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +153 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +50 -4
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +22 -5
- package/extension/src/content/settingsWorkbench.js +132 -89
- package/extension/src/content/sharedSessionViewSync.js +49 -11
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +118 -12
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +116 -94
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/ownedDirectoryCleanup.js +94 -0
- package/extension/src/page/referenceProjects.js +1 -0
- package/extension/src/page/reviewContext.js +113 -0
- package/extension/src/page/saveState.js +6 -4
- package/extension/src/page/snapshotRouter.js +84 -39
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +114 -26
- package/extension/src/page/trackedChangeCapture.js +12 -2
- package/extension/src/page/trackedChangeOwnership.js +19 -1
- package/extension/src/page/trackedChangeReplay.js +41 -21
- package/extension/src/page/trackedChangesLifecycle.js +217 -199
- package/extension/src/page/treeOperations.js +7 -10
- package/extension/src/page/updateSafetyProbe.js +41 -0
- package/extension/src/page/writebackReceiptJournal.js +2 -0
- package/extension/src/page/writebackRouter.js +103 -155
- package/extension/src/pageBridge.js +126 -69
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +373 -17
- package/extension/src/shared/auditRecords.js +4 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +129 -66
- package/extension/src/shared/composerAttachmentDraft.js +156 -0
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +349 -4
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +590 -24
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/managedUpdateProjection.js +2 -2
- package/extension/src/shared/nativeRequestIdentity.js +1 -0
- package/extension/src/shared/pageRpcContract.js +5 -4
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +132 -28
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +118 -27
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +166 -52
- package/extension/src/shared/storageDb.js +90 -26
- package/extension/src/shared/storageMigration.js +78 -112
- package/extension/src/shared/storageRunActions.js +267 -30
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +253 -32
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +17 -3
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +58 -40
- package/extension/styles/panel.css +1123 -493
- package/native-host/src/anthropicBridgeRequest.js +4 -2
- package/native-host/src/anthropicBridgeResponse.js +6 -2
- package/native-host/src/anthropicMessagesBridge.js +24 -16
- package/native-host/src/bootstrapMigration.js +296 -0
- package/native-host/src/bootstrapMigrationContract.js +101 -0
- package/native-host/src/bootstrapUpdate.js +269 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +20 -1
- package/native-host/src/chatCompletionsBridge.js +48 -15
- package/native-host/src/codexConfigPaths.js +106 -0
- package/native-host/src/codexHome.js +66 -73
- package/native-host/src/codexHomeScope.js +124 -0
- package/native-host/src/codexInheritedAuth.js +90 -0
- package/native-host/src/codexLocalSkills.js +301 -0
- package/native-host/src/codexPromptAssembly.js +114 -127
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +234 -198
- package/native-host/src/codexSessionTiming.js +18 -8
- package/native-host/src/codexSkillIsolation.js +170 -0
- package/native-host/src/codexThreadFork.js +19 -3
- package/native-host/src/index.js +19 -7
- package/native-host/src/managedInstall.js +110 -35
- package/native-host/src/managedInstallRecovery.js +140 -0
- package/native-host/src/managedRuntimeEvidence.js +57 -0
- package/native-host/src/mirrorWorkspace.js +92 -22
- package/native-host/src/nativeDoctor.js +15 -0
- package/native-host/src/nativeHelperLifecycle.js +21 -0
- package/native-host/src/nativeHostRegistration.js +33 -0
- package/native-host/src/nativeRunLifecycle.js +71 -0
- package/native-host/src/processStopBarrier.js +42 -7
- package/native-host/src/projectPathPlatform.js +30 -0
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerHttp.js +80 -0
- package/native-host/src/providerProfile.js +4 -2
- package/native-host/src/providerReasoning.js +2 -7
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/providerStore.js +72 -14
- package/native-host/src/responsesPassthroughBridge.js +266 -52
- package/native-host/src/runtimeInstaller.js +21 -1
- package/native-host/src/selectionScope.js +36 -8
- package/native-host/src/subagentAdmission.js +26 -0
- package/native-host/src/subagentBroker.js +79 -66
- package/native-host/src/subagentTelemetry.js +19 -10
- package/native-host/src/subagentWorkspacePath.js +5 -2
- package/native-host/src/taskRunnerRuntime.js +92 -57
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +409 -46
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +5 -8
- package/scripts/install-managed.mjs +1 -0
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
(function initCodexOverleafTrackedChangesLifecycle(root, factory) {
|
|
2
2
|
if (typeof module === 'object' && module.exports) {
|
|
3
|
-
module.exports = factory(require('./trackedChangeReplay'));
|
|
3
|
+
module.exports = factory(require('./trackedChangeReplay'), require('./reviewContext'));
|
|
4
4
|
} else {
|
|
5
|
-
root.CodexOverleafTrackedChangesLifecycle = factory(root.CodexOverleafTrackedChangeReplay);
|
|
5
|
+
root.CodexOverleafTrackedChangesLifecycle = factory(root.CodexOverleafTrackedChangeReplay, root.CodexOverleafReviewContext);
|
|
6
6
|
}
|
|
7
|
-
})(typeof window !== 'undefined' ? window : globalThis, function trackedChangesLifecycleFactory(Replay) {
|
|
7
|
+
})(typeof window !== 'undefined' ? window : globalThis, function trackedChangesLifecycleFactory(Replay, ReviewContext) {
|
|
8
8
|
'use strict';
|
|
9
|
-
|
|
10
|
-
// Tracked-changes lifecycle — carved out of writebackRouter.js in v1.8.0
|
|
11
|
-
// (structural-debt phase 7, #117): the accept/reject flows, the
|
|
12
|
-
// accept-replay + editor-undo recovery machinery, the reviewing/editing
|
|
13
|
-
// blocked-result builders, and the DOM collectors for Overleaf's
|
|
14
|
-
// tracked-change widgets. Code moved verbatim (original indentation kept);
|
|
15
|
-
// page-side collaborators are factory-injected by writebackRouter, which
|
|
16
|
-
// remains the only consumer and the owner of applyOperations itself.
|
|
17
9
|
function create(deps = {}) {
|
|
10
|
+
const base = createCore(deps);
|
|
11
|
+
const run = (method, params) => ReviewContext.create(deps, params).run((scoped, input) => createCore(scoped)[method](input));
|
|
12
|
+
return { ...base, rejectTrackedChanges: params => run('rejectTrackedChanges', params),
|
|
13
|
+
acceptTrackedChanges: params => run('acceptTrackedChanges', params) };
|
|
14
|
+
}
|
|
15
|
+
function createCore(deps = {}) {
|
|
18
16
|
const {
|
|
19
17
|
window,
|
|
20
18
|
checkWritebackRunProjectId,
|
|
@@ -41,7 +39,7 @@
|
|
|
41
39
|
normalizeOperationPaths,
|
|
42
40
|
normalizeReviewingSignalText,
|
|
43
41
|
normalizeSafeProjectPath,
|
|
44
|
-
openFileByPath,
|
|
42
|
+
openFileByPath: openReviewFileByPath,
|
|
45
43
|
readActiveEditorText,
|
|
46
44
|
readNodeSignalText,
|
|
47
45
|
setReviewingEnabled,
|
|
@@ -49,13 +47,19 @@
|
|
|
49
47
|
verifyActiveEditorText,
|
|
50
48
|
collectElements,
|
|
51
49
|
} = deps;
|
|
52
|
-
const { buildAcceptReplayOperations, rebaseCheckpointPair } = Replay.create({
|
|
50
|
+
const { buildAcceptReplayOperations, rebaseCheckpointPair, prepareSnapshotUndoCheckpoints, buildSnapshotUndoResult } = Replay.create({
|
|
53
51
|
normalizeSafeProjectPath, applyTextPatches
|
|
54
52
|
});
|
|
53
|
+
function openFileByPath(path, ...options) {
|
|
54
|
+
if (path !== getActiveFilePath()) rememberTrackedChangeNodeContext(getActiveFilePath());
|
|
55
|
+
return openReviewFileByPath(path, ...options);
|
|
56
|
+
}
|
|
55
57
|
|
|
56
58
|
function nativeReviewBlocked(path, code, applied = [], requestStarted = false) {
|
|
57
59
|
const reason = requestStarted
|
|
58
60
|
? 'The native review request could not be fully verified; some target changes may already be accepted. No text replay was attempted. Check Overleaf Review before retrying.'
|
|
61
|
+
: code === 'native_untracked_undo_pending_changes'
|
|
62
|
+
? 'This file has unresolved tracked changes. Undo without track changes was stopped to preserve them; no document changes were made.'
|
|
59
63
|
: 'The native tracked-change scope could not be safely isolated. No review mutation was attempted. Use Overleaf Review to handle the changes individually.';
|
|
60
64
|
return { ok: false, applied, skipped: [{ trackedChange: { path }, result: {
|
|
61
65
|
ok: false, code, reason, failure: {
|
|
@@ -68,14 +72,15 @@
|
|
|
68
72
|
} }] };
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
async function waitForNativeReview(path, refs, untracked = false) {
|
|
75
|
+
async function waitForNativeReview(path, refs, untracked = false, checkpointReady = () => true) {
|
|
72
76
|
const prepare = () => untracked ? prepareUntrackedUndo(path) : prepareTrackedChangeReview(path, refs);
|
|
73
77
|
for (let attempt = 0; attempt < 16; attempt++) {
|
|
74
78
|
const scope = prepare();
|
|
75
|
-
if (scope.ok) return scope;
|
|
79
|
+
if (scope.ok && checkpointReady(scope)) return scope;
|
|
76
80
|
await delay(150);
|
|
77
81
|
}
|
|
78
|
-
|
|
82
|
+
const scope = prepare();
|
|
83
|
+
return scope.ok && !checkpointReady(scope) ? { ...scope, ok: false } : scope;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
async function prepareNativeReview(params, allowUntrackedUndo = false) {
|
|
@@ -99,7 +104,7 @@
|
|
|
99
104
|
...checkpoints.map(file => normalizeSafeProjectPath(file?.path))])];
|
|
100
105
|
if ((!refs.length && !untracked) || refs.some(ref => ref.invalidProjectPath || !ref.path) || paths.some(path => !path))
|
|
101
106
|
return nativeReviewBlocked('', 'native_review_identity_unavailable');
|
|
102
|
-
const files = [], deadline = Date.now() + 90000;
|
|
107
|
+
const files = [], deadline = Math.min(Date.now() + 90000, deps.reviewDeadlineAt ?? Infinity);
|
|
103
108
|
for (const path of paths) {
|
|
104
109
|
if (Date.now() >= deadline || checkWritebackRunProjectId(params))
|
|
105
110
|
return nativeReviewBlocked(path, 'native_review_context_changed');
|
|
@@ -162,6 +167,7 @@
|
|
|
162
167
|
}));
|
|
163
168
|
const expectedText = accepting ? post[0].content : before[0].content;
|
|
164
169
|
const expectedKeys = accepting ? scope.merged.preservedKeys : scope.merged.beforeKeys;
|
|
170
|
+
if (deps.reviewGuard?.()) return nativeReviewBlocked(path, 'native_review_context_changed');
|
|
165
171
|
attempted = true;
|
|
166
172
|
view.dispatch({ changes });
|
|
167
173
|
compileBridge.markSourceEdited();
|
|
@@ -199,7 +205,7 @@
|
|
|
199
205
|
async function rejectPlanWithMergedInsertions(params, plan) {
|
|
200
206
|
const mixed = plan.files.find(file => !file.scope.merged && file.scope.unrelated.length);
|
|
201
207
|
if (mixed) return nativeReviewBlocked(mixed.path, 'native_reject_mixed_changes');
|
|
202
|
-
const applied = [];
|
|
208
|
+
const applied = deps.createReviewApplied?.() || [];
|
|
203
209
|
for (const file of plan.files) {
|
|
204
210
|
if (Date.now() >= plan.deadline || checkWritebackRunProjectId(params))
|
|
205
211
|
return nativeReviewBlocked(file.path, 'native_review_context_changed', applied, applied.length > 0);
|
|
@@ -220,7 +226,7 @@
|
|
|
220
226
|
}
|
|
221
227
|
|
|
222
228
|
async function acceptNativeReview(params, plan) {
|
|
223
|
-
const applied = [];
|
|
229
|
+
const applied = deps.createReviewApplied?.() || [];
|
|
224
230
|
let token = window?.document?.querySelector?.('meta[name="ol-csrfToken"]')?.getAttribute('content');
|
|
225
231
|
try { token = JSON.parse(token); } catch (_error) { /* Older pages expose plain text. */ }
|
|
226
232
|
if (typeof token !== 'string' || !token || token.length > 4096
|
|
@@ -251,6 +257,7 @@
|
|
|
251
257
|
const controller = new window.AbortController();
|
|
252
258
|
const timer = window.setTimeout(() => controller.abort(), Math.min(12000, Math.max(1, plan.deadline - Date.now())));
|
|
253
259
|
try {
|
|
260
|
+
if (deps.reviewGuard?.()) return nativeReviewBlocked(path, 'native_review_context_changed', applied, applied.length > 0);
|
|
254
261
|
// Same ID-scoped endpoint and CSRF header used by Overleaf's RangesProvider.
|
|
255
262
|
const response = await window.fetch('/project/' + encodeURIComponent(params.runProjectId)
|
|
256
263
|
+ '/doc/' + encodeURIComponent(scope.nativeDocId) + '/changes/accept', {
|
|
@@ -258,6 +265,7 @@
|
|
|
258
265
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-Csrf-Token': token },
|
|
259
266
|
body: JSON.stringify({ change_ids: scope.ids })
|
|
260
267
|
});
|
|
268
|
+
if (deps.reviewGuard?.()) return nativeReviewBlocked(path, 'native_review_context_changed', applied, true);
|
|
261
269
|
if (!response.ok) return nativeReviewBlocked(path, 'native_accept_http_' + response.status, applied, true);
|
|
262
270
|
} catch (_error) {
|
|
263
271
|
return nativeReviewBlocked(path, 'native_accept_request_unconfirmed', applied, true);
|
|
@@ -282,19 +290,15 @@
|
|
|
282
290
|
return { ok: true, verified: true, applied, skipped: [] };
|
|
283
291
|
}
|
|
284
292
|
|
|
293
|
+
let nativeRejectPlan = null;
|
|
285
294
|
async function rejectTrackedChanges(params = {}) {
|
|
286
|
-
//
|
|
287
|
-
// The pageBridge wrapper already runs the runProjectId guard, but a
|
|
288
|
-
// future caller could reach the router directly. A missing or empty
|
|
289
|
-
// `runProjectId` blocks the reject with the same shape the page-side
|
|
290
|
-
// guard uses. Existing tests that target individual reject behaviors set
|
|
291
|
-
// `runProjectId` on params; tests that intentionally omit it land here.
|
|
295
|
+
// Direct router callers also require the original run project identity.
|
|
292
296
|
const writeGuardBlock = checkWritebackRunProjectId(params);
|
|
293
297
|
if (writeGuardBlock) return writeGuardBlock;
|
|
294
298
|
const trackedChanges = normalizeTrackedChangeRefs(params.trackedChanges || []);
|
|
295
299
|
const expectedFiles = Array.isArray(params.expectedFiles) ? params.expectedFiles : [];
|
|
296
300
|
const postFiles = Array.isArray(params.postFiles) ? params.postFiles : [];
|
|
297
|
-
const applied = [];
|
|
301
|
+
const applied = deps.createReviewApplied?.() || [];
|
|
298
302
|
const skipped = [];
|
|
299
303
|
const appliedPaths = new Set();
|
|
300
304
|
const invalidTrackedChange = trackedChanges.find(trackedChange => trackedChange.invalidProjectPath);
|
|
@@ -310,6 +314,7 @@
|
|
|
310
314
|
}
|
|
311
315
|
|
|
312
316
|
const nativeReview = await prepareNativeReview(params, true);
|
|
317
|
+
nativeRejectPlan = nativeReview;
|
|
313
318
|
if (nativeReview && !nativeReview.ok) return nativeReview;
|
|
314
319
|
if (nativeReview?.files.some(file => file.scope.merged))
|
|
315
320
|
return rejectPlanWithMergedInsertions(params, nativeReview);
|
|
@@ -320,12 +325,12 @@
|
|
|
320
325
|
// An ordinary write has no owned editor-history entry. Restore its
|
|
321
326
|
// checkpoint directly; global Undo may belong to an earlier user edit.
|
|
322
327
|
const restored = await restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles, {
|
|
323
|
-
runProjectId: params.runProjectId
|
|
328
|
+
runProjectId: params.runProjectId, untrackedPlan: nativeReview
|
|
324
329
|
});
|
|
325
|
-
if (restored.applied.
|
|
330
|
+
if (restored.applied.some(entry => entry.result?.changedDocument !== false)) compileBridge.markSourceEdited();
|
|
326
331
|
return { ...restored, ok: restored.attempted && restored.ok,
|
|
327
|
-
skipped: restored.attempted ? restored.skipped : [{ trackedChange:
|
|
328
|
-
ok: false, code: restored.code || 'snapshot_undo_unavailable',
|
|
332
|
+
skipped: restored.attempted ? restored.skipped : [{ trackedChange: { path: restored.file || '' }, result: {
|
|
333
|
+
ok: false, file: restored.file || '', code: restored.code || 'snapshot_undo_unavailable',
|
|
329
334
|
reason: restored.reason || 'The saved checkpoint is unavailable for this undo.'
|
|
330
335
|
} }] };
|
|
331
336
|
}
|
|
@@ -427,7 +432,7 @@
|
|
|
427
432
|
continue;
|
|
428
433
|
}
|
|
429
434
|
|
|
430
|
-
const rejectControl = findRejectControlForTrackedChangeNode(node);
|
|
435
|
+
const rejectControl = isTrackedChangeNodeInNativeScope(node, trackedChange.path || getActiveFilePath()) ? findRejectControlForTrackedChangeNode(node) : null;
|
|
431
436
|
if (!rejectControl) {
|
|
432
437
|
if (expectedFiles.length || trackedChange.path) {
|
|
433
438
|
continue;
|
|
@@ -477,7 +482,7 @@
|
|
|
477
482
|
const completion = await rejectRemainingTrackedChangesForExpectedFiles(expectedFiles, applied);
|
|
478
483
|
if (!completion.ok) {
|
|
479
484
|
skipped.push({
|
|
480
|
-
trackedChange: null,
|
|
485
|
+
trackedChange: completion.file ? { path: completion.file } : null,
|
|
481
486
|
result: completion
|
|
482
487
|
});
|
|
483
488
|
}
|
|
@@ -485,7 +490,7 @@
|
|
|
485
490
|
const completion = await rejectRemainingTrackedChangesForTrackedPaths(trackedChanges, applied);
|
|
486
491
|
if (!completion.ok) {
|
|
487
492
|
skipped.push({
|
|
488
|
-
trackedChange: null,
|
|
493
|
+
trackedChange: completion.file ? { path: completion.file } : null,
|
|
489
494
|
result: completion
|
|
490
495
|
});
|
|
491
496
|
}
|
|
@@ -505,18 +510,15 @@
|
|
|
505
510
|
// Native-ledger browsers accept exact IDs without changing document text.
|
|
506
511
|
// The original undo/replay flow remains only for legacy adapter callers.
|
|
507
512
|
async function acceptTrackedChanges(params = {}) {
|
|
508
|
-
//
|
|
509
|
-
// The pageBridge wrapper already runs the runProjectId guard, but a
|
|
510
|
-
// future caller could reach the router directly. A missing or empty
|
|
511
|
-
// `runProjectId` blocks the accept with the same shape the page-side
|
|
512
|
-
// guard uses.
|
|
513
|
+
// Direct router callers also require the original run project identity.
|
|
513
514
|
const writeGuardBlock = checkWritebackRunProjectId(params);
|
|
514
515
|
if (writeGuardBlock) return writeGuardBlock;
|
|
516
|
+
if (params.verifyManualReview === true) return nativeReviewBlocked('', 'native_accept_protocol_unsupported');
|
|
515
517
|
const nativeReview = await prepareNativeReview(params);
|
|
516
518
|
if (nativeReview) return nativeReview.ok ? acceptNativeReview(params, nativeReview) : nativeReview;
|
|
517
519
|
const expectedFiles = Array.isArray(params.expectedFiles) ? params.expectedFiles : [];
|
|
518
520
|
const postFiles = Array.isArray(params.postFiles) ? params.postFiles : [];
|
|
519
|
-
const applied = [];
|
|
521
|
+
const applied = deps.createReviewApplied?.() || [];
|
|
520
522
|
const skipped = [];
|
|
521
523
|
// diagnostics: a per-step trace returned to the caller so the run card can
|
|
522
524
|
// surface exactly what happened on each Accept All step. Each entry is
|
|
@@ -559,12 +561,8 @@
|
|
|
559
561
|
const editorUndo = await rejectTrackedChangesViaEditorUndo(expectedFiles, postFiles, applied);
|
|
560
562
|
let undoReady = editorUndo.ok === true;
|
|
561
563
|
let snapshotUndo = null;
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
// reuse Undo's guarded snapshot path: it first verifies that every file is
|
|
565
|
-
// still exactly at this run's post-write content, then restores the
|
|
566
|
-
// persisted pre-write content with Track Changes disabled. The normal
|
|
567
|
-
// Accept replay below can then apply the post-write patch untracked.
|
|
564
|
+
// After a refresh clears native history, guarded snapshot rollback can
|
|
565
|
+
// restore exact checkpoints in Editing before the legacy Accept replay.
|
|
568
566
|
if (!undoReady && applied.length === appliedBeforeUndo) {
|
|
569
567
|
snapshotUndo = await restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles, {
|
|
570
568
|
runProjectId: params.runProjectId
|
|
@@ -630,10 +628,8 @@
|
|
|
630
628
|
reason: editingSwitch.reason || ''
|
|
631
629
|
});
|
|
632
630
|
if (!editingSwitch.ok) {
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
// tracked changes. The structured failure from forceEditingForAcceptReplay
|
|
636
|
-
// (editing_not_confirmed, changedDocument:true) is preserved verbatim.
|
|
631
|
+
// Undo already changed the document. Preserve that evidence and stop
|
|
632
|
+
// when Editing cannot be confirmed for an untracked replay.
|
|
637
633
|
return {
|
|
638
634
|
ok: false,
|
|
639
635
|
applied: [],
|
|
@@ -676,12 +672,8 @@
|
|
|
676
672
|
};
|
|
677
673
|
}
|
|
678
674
|
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
// write only the minimal changed fragments via the `patches` path of
|
|
682
|
-
// applyOperationsCore — never a whole-file replaceAll, which would clobber
|
|
683
|
-
// any unrelated content and produce one giant tracked change if anything
|
|
684
|
-
// about the mode switch were imperfect.
|
|
675
|
+
// Replay only the changed fragments in Editing; whole-file replacement
|
|
676
|
+
// would overwrite unrelated text or create one large tracked change.
|
|
685
677
|
const operationsResult = buildAcceptReplayOperations(
|
|
686
678
|
paths,
|
|
687
679
|
expectedByPath,
|
|
@@ -703,18 +695,9 @@
|
|
|
703
695
|
};
|
|
704
696
|
}
|
|
705
697
|
const operations = operationsResult.operations;
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
// Bug C: even after forceEditingForAcceptReplay confirmed Editing once,
|
|
710
|
-
// Overleaf has been observed flipping back to Reviewing between the
|
|
711
|
-
// confirm and the next CodeMirror write (Overleaf-side override, per-user
|
|
712
|
-
// "Track Changes for me" setting, or a positive-confirm false-positive
|
|
713
|
-
// window). The fix: re-verify Editing immediately BEFORE every single
|
|
714
|
-
// operation, and if Reviewing has slipped back on, force the toggle off
|
|
715
|
-
// again and positively re-confirm before writing. If Editing still cannot
|
|
716
|
-
// be positively confirmed after the re-toggle, bail the rest of the loop
|
|
717
|
-
// so we never land a tracked write.
|
|
698
|
+
// Overleaf may return to Reviewing between confirmation and a write.
|
|
699
|
+
// Reconfirm stable Editing before every operation, force it off again
|
|
700
|
+
// if needed, and stop the tail whenever confirmation fails.
|
|
718
701
|
let bailedReason = null;
|
|
719
702
|
for (const operation of operations) {
|
|
720
703
|
const opPath = operation?.path || '';
|
|
@@ -868,12 +851,8 @@
|
|
|
868
851
|
}
|
|
869
852
|
}
|
|
870
853
|
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
// CodeMirror dispatch can race with Overleaf's collaboration/reviewing
|
|
874
|
-
// settlement and cause the replay itself to land as fresh tracked changes.
|
|
875
|
-
// Leave the editor in Editing after a successful replay; the user can turn
|
|
876
|
-
// Reviewing back on manually after Overleaf has saved the accepted text.
|
|
854
|
+
// Leave Editing active: restoring Reviewing before collaboration settles
|
|
855
|
+
// can turn the untracked Accept replay into fresh tracked changes.
|
|
877
856
|
if (weToggledOff) {
|
|
878
857
|
pushDiagnostic('restoreReviewing', {
|
|
879
858
|
ok: true,
|
|
@@ -895,12 +874,8 @@
|
|
|
895
874
|
};
|
|
896
875
|
}
|
|
897
876
|
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
// reviewing detector's verdict (ok/status/source), a compact controls
|
|
901
|
-
// summary, and the two strict gates (isReviewingPositivelyOn /
|
|
902
|
-
// isEditingPositivelyConfirmed). Self-contained — the run card can render
|
|
903
|
-
// this verbatim without re-reading any page state.
|
|
877
|
+
// Capture the detector signals and strict gates for self-contained
|
|
878
|
+
// diagnostics; rendering must not reread mutable page state.
|
|
904
879
|
function summarizeReviewingStateForDiagnostics(state = {}) {
|
|
905
880
|
const reviewing = state.reviewing || {};
|
|
906
881
|
const controls = (state.signals?.controls || []).slice(0, 6).map(control => {
|
|
@@ -926,10 +901,8 @@
|
|
|
926
901
|
};
|
|
927
902
|
}
|
|
928
903
|
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
// gate the Accept replay needs: it must never replay while tracked, so
|
|
932
|
-
// anything short of a positive "Editing on / Reviewing off" is rejected.
|
|
904
|
+
// Accept replay requires positively detected Editing and no positively
|
|
905
|
+
// confirmed Reviewing; every weaker observation must stop the replay.
|
|
933
906
|
function isEditingPositivelyConfirmed(state = {}) {
|
|
934
907
|
return isEditingConfirmedForNoTraceUndo(state) && !isReviewingConfirmedForWrite(state);
|
|
935
908
|
}
|
|
@@ -988,42 +961,20 @@
|
|
|
988
961
|
};
|
|
989
962
|
}
|
|
990
963
|
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
// Bug B root cause: the accept flow used ensureEditing, which short-circuits
|
|
995
|
-
// on isEditingConfirmedForNoTraceUndo. That detector is negation-based — it
|
|
996
|
-
// returns true whenever Reviewing is not *positively* confirmed-for-write
|
|
997
|
-
// (which requires an active aria attribute the real Overleaf reviewing
|
|
998
|
-
// control does not always carry) AND a control loosely matches an Editing
|
|
999
|
-
// pattern. So while Track Changes was actually ON, it false-positived
|
|
1000
|
-
// "already Editing", ensureEditing returned activated:false without toggling,
|
|
1001
|
-
// and the replay landed as tracked changes.
|
|
1002
|
-
//
|
|
1003
|
-
// The fix here does NOT trust that lenient short-circuit: it reads the
|
|
1004
|
-
// reviewing state explicitly and treats Editing as confirmed only when it is
|
|
1005
|
-
// *positively* confirmed (Editing detected AND Reviewing not confirmed on).
|
|
1006
|
-
// Whenever that strict gate is not met it forces setReviewingEnabled(false),
|
|
1007
|
-
// then positively re-confirms Editing before returning ok; if it still cannot
|
|
1008
|
-
// confirm Editing it returns not-ok so the caller bails instead of replaying
|
|
1009
|
-
// while tracked.
|
|
964
|
+
// Accept replay requires positively confirmed Editing. The legacy detector
|
|
965
|
+
// can misclassify Reviewing controls without aria state; force the toggle
|
|
966
|
+
// and recheck before replaying untracked text.
|
|
1010
967
|
async function forceEditingForAcceptReplay() {
|
|
1011
968
|
const initial = getReviewingState({});
|
|
1012
969
|
if (isEditingPositivelyConfirmed(initial)) {
|
|
1013
|
-
// Positively confirmed already in Editing — no toggle needed.
|
|
1014
970
|
return { ok: true, activated: false };
|
|
1015
971
|
}
|
|
1016
972
|
|
|
1017
|
-
//
|
|
1018
|
-
// toggle rather than trusting ensureEditing's lenient detection.
|
|
973
|
+
// Force the toggle whenever Editing is not positively confirmed.
|
|
1019
974
|
const switched = await setReviewingEnabled(false, { waitMs: 1800 });
|
|
1020
975
|
if (!switched.ok) {
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
// was rolled back so the user-visible doc state has moved from the
|
|
1024
|
-
// post-write tracked state back to pre-write — set changedDocument:true
|
|
1025
|
-
// per the design spec's "is there text the user might want to inspect"
|
|
1026
|
-
// contract for accept-side failures after a rollback.
|
|
976
|
+
// Native Undo already changed the document; a failed mode switch must
|
|
977
|
+
// preserve changedDocument evidence even though replay never started.
|
|
1027
978
|
return {
|
|
1028
979
|
ok: false,
|
|
1029
980
|
code: switched.code || 'accept_editing_not_confirmed',
|
|
@@ -1065,10 +1016,6 @@
|
|
|
1065
1016
|
return { ok: true, activated: true };
|
|
1066
1017
|
}
|
|
1067
1018
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
1019
|
async function rejectTrackedChangesViaEditorUndo(expectedFiles, postFiles, applied) {
|
|
1073
1020
|
const expectedByPath = new Map((expectedFiles || [])
|
|
1074
1021
|
.filter(file => file?.path && typeof file.content === 'string')
|
|
@@ -1087,10 +1034,8 @@
|
|
|
1087
1034
|
const postContent = postByPath.get(path);
|
|
1088
1035
|
const activePathMatches = getActiveFilePath() === path;
|
|
1089
1036
|
const activeContentMatches = readActiveEditorText() === postContent;
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1092
|
-
// readActiveEditorText() still belongs to the previously viewed file.
|
|
1093
|
-
// Force-reopen the target whenever either identity signal disagrees.
|
|
1037
|
+
// Tree selection can precede the editor swap. Reopen when either
|
|
1038
|
+
// the target path or its exact postimage is not yet active.
|
|
1094
1039
|
if (path && (!activePathMatches || !activeContentMatches)) {
|
|
1095
1040
|
const opened = await openFileByPath(path, { force: true });
|
|
1096
1041
|
if (!opened.ok) {
|
|
@@ -1141,18 +1086,23 @@
|
|
|
1141
1086
|
}
|
|
1142
1087
|
|
|
1143
1088
|
async function restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles, options = {}) {
|
|
1144
|
-
const expectedByPath =
|
|
1145
|
-
.filter(file => file?.path && typeof file.content === 'string')
|
|
1146
|
-
.map(file => [normalizeSafeProjectPath(file.path), file.content])
|
|
1147
|
-
.filter(([path]) => path));
|
|
1148
|
-
const postByPath = new Map((postFiles || [])
|
|
1149
|
-
.filter(file => file?.path && typeof file.content === 'string')
|
|
1150
|
-
.map(file => [normalizeSafeProjectPath(file.path), file.content])
|
|
1151
|
-
.filter(([path]) => path));
|
|
1152
|
-
const paths = Array.from(expectedByPath.keys()).filter(path => postByPath.has(path));
|
|
1089
|
+
const { expectedByPath, postByPath, paths } = prepareSnapshotUndoCheckpoints(expectedFiles, postFiles);
|
|
1153
1090
|
const rebasedExpectedByPath = new Map();
|
|
1154
1091
|
const rebasedPostByPath = new Map();
|
|
1155
1092
|
const undoRebaseProofs = new Map();
|
|
1093
|
+
// Only the ordinary, native-ledger-checked Undo route may recognize a
|
|
1094
|
+
// lost receipt's exact pre-image. This consumes an existing checkpoint;
|
|
1095
|
+
// it grants no native review, file creation or move ownership.
|
|
1096
|
+
const untrackedByPath = new Map((options.untrackedPlan?.files || []).map(file => [file.path, file]));
|
|
1097
|
+
const alreadyRestored = new Set();
|
|
1098
|
+
const mismatch = path => ({ ok: false, file: path, changedDocument: false, code: 'snapshot_undo_current_mismatch',
|
|
1099
|
+
reason: `${path} 当前内容或文档身份与本轮撤销检查点不一致;Codex 保留恢复材料并停止撤销。` });
|
|
1100
|
+
async function verifyAlreadyRestored(path) {
|
|
1101
|
+
if (getActiveFilePath() !== path && !(await openFileByPath(path)).ok) return false;
|
|
1102
|
+
return (await waitForNativeReview(path, [], true, scope => Boolean(scope.nativeDocId)
|
|
1103
|
+
&& scope.nativeDocId === untrackedByPath.get(path)?.nativeDocId
|
|
1104
|
+
&& getActiveFilePath() === path && readActiveEditorText() === expectedByPath.get(path))).ok === true;
|
|
1105
|
+
}
|
|
1156
1106
|
if (!paths.length) {
|
|
1157
1107
|
return {
|
|
1158
1108
|
ok: false,
|
|
@@ -1173,7 +1123,7 @@
|
|
|
1173
1123
|
attempted: false,
|
|
1174
1124
|
applied: [],
|
|
1175
1125
|
skipped: [],
|
|
1176
|
-
code: 'snapshot_undo_open_failed',
|
|
1126
|
+
code: 'snapshot_undo_open_failed', file: path,
|
|
1177
1127
|
reason: `无法打开 ${path} 来执行快照撤销。`
|
|
1178
1128
|
};
|
|
1179
1129
|
}
|
|
@@ -1182,7 +1132,8 @@
|
|
|
1182
1132
|
path,
|
|
1183
1133
|
expectedByPath.get(path),
|
|
1184
1134
|
postByPath.get(path),
|
|
1185
|
-
1500
|
|
1135
|
+
1500,
|
|
1136
|
+
untrackedByPath.has(path)
|
|
1186
1137
|
);
|
|
1187
1138
|
if (!ready.ok) {
|
|
1188
1139
|
return {
|
|
@@ -1192,10 +1143,14 @@
|
|
|
1192
1143
|
skipped: [],
|
|
1193
1144
|
rebasedExpectedFiles: [],
|
|
1194
1145
|
rebasedPostFiles: [],
|
|
1195
|
-
code: 'snapshot_undo_current_mismatch',
|
|
1146
|
+
code: 'snapshot_undo_current_mismatch', file: path,
|
|
1196
1147
|
reason: `${path} 当前内容已经不是本轮写入后的内容;为避免覆盖你的后续修改,Codex 不执行快照撤销。`
|
|
1197
1148
|
};
|
|
1198
1149
|
}
|
|
1150
|
+
if (ready.alreadyRestored) {
|
|
1151
|
+
if (!(await verifyAlreadyRestored(path))) return { ...mismatch(path), attempted: false, applied: [], skipped: [] };
|
|
1152
|
+
alreadyRestored.add(path);
|
|
1153
|
+
}
|
|
1199
1154
|
rebasedExpectedByPath.set(path, ready.expectedContent);
|
|
1200
1155
|
rebasedPostByPath.set(path, ready.postContent);
|
|
1201
1156
|
if (ready.prefixLength || ready.suffixLength) {
|
|
@@ -1203,55 +1158,49 @@
|
|
|
1203
1158
|
}
|
|
1204
1159
|
}
|
|
1205
1160
|
|
|
1206
|
-
|
|
1161
|
+
// Re-check observations after other preflight awaits before starting the
|
|
1162
|
+
// unfinished tail. They are checked again after writes before settlement.
|
|
1163
|
+
for (const path of alreadyRestored) {
|
|
1164
|
+
if (!(await verifyAlreadyRestored(path))) return { ...mismatch(path), attempted: false, applied: [], skipped: [] };
|
|
1165
|
+
}
|
|
1166
|
+
const operations = paths.filter(path => !alreadyRestored.has(path)).map(path => ({
|
|
1207
1167
|
type: 'edit',
|
|
1208
1168
|
path,
|
|
1209
1169
|
replaceAll: rebasedExpectedByPath.get(path),
|
|
1210
1170
|
reason: 'Undo tracked edit'
|
|
1211
1171
|
}));
|
|
1212
|
-
const result = await applyOperationsWithNoTraceUndo(operations, {
|
|
1172
|
+
const result = operations.length ? await applyOperationsWithNoTraceUndo(operations, {
|
|
1213
1173
|
baseFiles: paths.map(path => ({
|
|
1214
1174
|
path,
|
|
1215
1175
|
content: rebasedPostByPath.get(path)
|
|
1216
1176
|
})),
|
|
1217
1177
|
runProjectId: typeof options.runProjectId === 'string' ? options.runProjectId : ''
|
|
1218
|
-
});
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
},
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
}
|
|
1232
|
-
return {
|
|
1233
|
-
ok: !(result.skipped || []).length,
|
|
1234
|
-
attempted: true,
|
|
1235
|
-
applied: (result.applied || []).map(toTrackedResult),
|
|
1236
|
-
skipped: (result.skipped || []).map(toTrackedResult),
|
|
1237
|
-
rebasedExpectedFiles: paths.map(path => ({
|
|
1238
|
-
path,
|
|
1239
|
-
content: rebasedExpectedByPath.get(path)
|
|
1240
|
-
})),
|
|
1241
|
-
rebasedPostFiles: paths.map(path => ({
|
|
1242
|
-
path,
|
|
1243
|
-
content: rebasedPostByPath.get(path)
|
|
1244
|
-
}))
|
|
1245
|
-
};
|
|
1178
|
+
}) : { applied: [], skipped: [] };
|
|
1179
|
+
const converted = buildSnapshotUndoResult(result, rebasedExpectedByPath, rebasedPostByPath, undoRebaseProofs);
|
|
1180
|
+
const applied = deps.createReviewApplied?.() || [];
|
|
1181
|
+
applied.push(...converted.applied);
|
|
1182
|
+
const skipped = converted.skipped;
|
|
1183
|
+
for (const path of alreadyRestored) {
|
|
1184
|
+
const verified = await verifyAlreadyRestored(path);
|
|
1185
|
+
const entry = { trackedChange: { key: `snapshot-undo:${path}`, id: '', path,
|
|
1186
|
+
label: 'Already restored checkpoint' }, result: verified
|
|
1187
|
+
? { ok: true, verified: true, verifiedContent: expectedByPath.get(path), changedDocument: false, idempotent: true }
|
|
1188
|
+
: mismatch(path) };
|
|
1189
|
+
(verified ? applied : skipped).push(entry);
|
|
1190
|
+
}
|
|
1191
|
+
return { ...converted, ok: !skipped.length, applied, skipped };
|
|
1246
1192
|
}
|
|
1247
1193
|
|
|
1248
|
-
async function waitForActiveEditorCheckpoint(filePath, expectedContent, postContent, timeoutMs) {
|
|
1194
|
+
async function waitForActiveEditorCheckpoint(filePath, expectedContent, postContent, timeoutMs, allowAlreadyRestored = false) {
|
|
1249
1195
|
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
|
|
1250
1196
|
let actual = readActiveEditorText();
|
|
1251
1197
|
while (Date.now() < deadline) {
|
|
1252
1198
|
const activeFileMatches = !filePath || getActiveFilePath() === filePath;
|
|
1253
1199
|
actual = readActiveEditorText();
|
|
1254
1200
|
if (activeFileMatches) {
|
|
1201
|
+
if (allowAlreadyRestored && actual === expectedContent) {
|
|
1202
|
+
return { ok: true, alreadyRestored: true, expectedContent, postContent, prefixLength: 0, suffixLength: 0 };
|
|
1203
|
+
}
|
|
1255
1204
|
const rebased = rebaseCheckpointPair(actual, expectedContent, postContent);
|
|
1256
1205
|
if (rebased.ok) {
|
|
1257
1206
|
return rebased;
|
|
@@ -1265,7 +1214,6 @@
|
|
|
1265
1214
|
};
|
|
1266
1215
|
}
|
|
1267
1216
|
|
|
1268
|
-
|
|
1269
1217
|
async function waitForActiveEditorExpectedText(filePath, expectedContent, timeoutMs) {
|
|
1270
1218
|
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
|
|
1271
1219
|
let actual = readActiveEditorText();
|
|
@@ -1292,34 +1240,36 @@
|
|
|
1292
1240
|
const maxClicks = 200;
|
|
1293
1241
|
for (let clicks = 0; clicks <= maxClicks; clicks += 1) {
|
|
1294
1242
|
if (readActiveEditorText() === expectedContent) {
|
|
1295
|
-
return {
|
|
1296
|
-
ok: true,
|
|
1297
|
-
attempted: clicks > 0,
|
|
1298
|
-
clicks
|
|
1299
|
-
};
|
|
1243
|
+
return { ok: true, attempted: clicks > 0, clicks };
|
|
1300
1244
|
}
|
|
1301
1245
|
if (clicks === maxClicks) {
|
|
1302
1246
|
break;
|
|
1303
1247
|
}
|
|
1304
1248
|
|
|
1305
|
-
const
|
|
1249
|
+
const beforeText = readActiveEditorText();
|
|
1250
|
+
let undoControl = findEditorUndoControl();
|
|
1306
1251
|
if (!undoControl) {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1252
|
+
// Text and the native ledger can be ready before the toolbar updates.
|
|
1253
|
+
// Wait only before dispatch, keeping this click's scope fixed.
|
|
1254
|
+
const assertStable = await createReviewScopeGuard(path, beforeText), until = Date.now() + 1500;
|
|
1255
|
+
while (!undoControl && Date.now() < until) {
|
|
1256
|
+
assertStable();
|
|
1257
|
+
await delay(Math.min(60, until - Date.now()));
|
|
1258
|
+
assertStable();
|
|
1259
|
+
undoControl = findEditorUndoControl();
|
|
1260
|
+
}
|
|
1261
|
+
assertStable();
|
|
1262
|
+
}
|
|
1263
|
+
if (!undoControl) {
|
|
1264
|
+
return { ok: false, attempted: clicks > 0, code: 'editor_undo_control_not_found',
|
|
1311
1265
|
reason: `没有找到 Overleaf 编辑器自己的 Undo/撤销按钮,无法一次性撤销 ${path} 的本轮留痕。`
|
|
1312
1266
|
};
|
|
1313
1267
|
}
|
|
1314
1268
|
|
|
1315
|
-
const beforeText = readActiveEditorText();
|
|
1316
1269
|
clickNode(undoControl);
|
|
1317
1270
|
await waitForEditorTextProgress(beforeText, expectedContent, 1000);
|
|
1318
1271
|
if (readActiveEditorText() === beforeText) {
|
|
1319
|
-
return {
|
|
1320
|
-
ok: false,
|
|
1321
|
-
attempted: clicks > 0,
|
|
1322
|
-
code: 'editor_undo_no_progress',
|
|
1272
|
+
return { ok: false, attempted: clicks > 0, code: 'editor_undo_no_progress',
|
|
1323
1273
|
reason: `Codex 点击了 Overleaf Undo/撤销,但 ${path} 内容没有变化。`
|
|
1324
1274
|
};
|
|
1325
1275
|
}
|
|
@@ -1352,7 +1302,9 @@
|
|
|
1352
1302
|
}
|
|
1353
1303
|
const result = await rejectRemainingTrackedChangesForExpectedFile(file, applied);
|
|
1354
1304
|
if (!result.ok) {
|
|
1355
|
-
|
|
1305
|
+
// The sweep knows its target even when no DOM review node remains.
|
|
1306
|
+
// Keep that scope so independently verified files can leave recovery.
|
|
1307
|
+
return { ...result, file: file.path };
|
|
1356
1308
|
}
|
|
1357
1309
|
}
|
|
1358
1310
|
return { ok: true };
|
|
@@ -1373,7 +1325,7 @@
|
|
|
1373
1325
|
for (const path of paths) {
|
|
1374
1326
|
const result = await rejectRemainingTrackedChangesForPath(path, applied);
|
|
1375
1327
|
if (!result.ok) {
|
|
1376
|
-
return result;
|
|
1328
|
+
return { ...result, file: path };
|
|
1377
1329
|
}
|
|
1378
1330
|
}
|
|
1379
1331
|
return { ok: true };
|
|
@@ -1394,7 +1346,9 @@
|
|
|
1394
1346
|
const appliedCountBefore = applied.length;
|
|
1395
1347
|
const maxAttempts = 200;
|
|
1396
1348
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1397
|
-
const
|
|
1349
|
+
const selection = await waitForRejectableTrackedChange(path);
|
|
1350
|
+
selection.assertStable();
|
|
1351
|
+
const node = selection.node;
|
|
1398
1352
|
if (!node) {
|
|
1399
1353
|
if (applied.length > appliedCountBefore) {
|
|
1400
1354
|
return { ok: true };
|
|
@@ -1406,7 +1360,7 @@
|
|
|
1406
1360
|
};
|
|
1407
1361
|
}
|
|
1408
1362
|
|
|
1409
|
-
const rejectControl = findRejectControlForTrackedChangeNode(node);
|
|
1363
|
+
const rejectControl = isTrackedChangeNodeInNativeScope(node, path) ? findRejectControlForTrackedChangeNode(node) : null;
|
|
1410
1364
|
if (!rejectControl) {
|
|
1411
1365
|
return {
|
|
1412
1366
|
ok: false,
|
|
@@ -1417,6 +1371,7 @@
|
|
|
1417
1371
|
|
|
1418
1372
|
const trackedChange = trackedChangeRefFromNode(node, path);
|
|
1419
1373
|
const beforeText = readActiveEditorText();
|
|
1374
|
+
selection.assertStable();
|
|
1420
1375
|
clickNode(rejectControl);
|
|
1421
1376
|
await waitForTrackedChangeRejectProgress(trackedChange, path, beforeText, 1200);
|
|
1422
1377
|
applied.push({
|
|
@@ -1447,6 +1402,17 @@
|
|
|
1447
1402
|
};
|
|
1448
1403
|
}
|
|
1449
1404
|
|
|
1405
|
+
async function verifyReviewFileContent(file, applied) {
|
|
1406
|
+
const verified = await verifyActiveEditorText(file.content, file.path, 800);
|
|
1407
|
+
if (getActiveFilePath() !== file.path) verified.ok = false;
|
|
1408
|
+
if (verified.ok) for (const entry of applied) {
|
|
1409
|
+
if (entry.trackedChange?.path === file.path && entry.result?.ok === true
|
|
1410
|
+
&& ['overleaf-review-reject', 'overleaf-review-reject-sweep'].includes(entry.result.method))
|
|
1411
|
+
Object.assign(entry.result, { verified: true, verifiedContent: file.content });
|
|
1412
|
+
}
|
|
1413
|
+
return verified;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1450
1416
|
async function rejectRemainingTrackedChangesForExpectedFile(file, applied) {
|
|
1451
1417
|
const opened = await openFileByPath(file.path);
|
|
1452
1418
|
if (!opened.ok) {
|
|
@@ -1460,12 +1426,16 @@
|
|
|
1460
1426
|
const appliedCountBefore = applied.length;
|
|
1461
1427
|
const maxAttempts = 200;
|
|
1462
1428
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1463
|
-
|
|
1429
|
+
if (applied.some(entry => entry.trackedChange?.path === file.path
|
|
1430
|
+
&& ['overleaf-review-reject', 'overleaf-review-reject-sweep'].includes(entry.result?.method))
|
|
1431
|
+
&& getActiveFilePath() === file.path && readActiveEditorText() === file.content
|
|
1432
|
+
&& (await verifyReviewFileContent(file, applied)).ok) return { ok: true };
|
|
1433
|
+
const selection = await waitForRejectableTrackedChange(file.path);
|
|
1434
|
+
selection.assertStable();
|
|
1435
|
+
const node = selection.node;
|
|
1464
1436
|
if (!node) {
|
|
1465
|
-
const verified = await
|
|
1466
|
-
if (verified.ok) {
|
|
1467
|
-
return { ok: true };
|
|
1468
|
-
}
|
|
1437
|
+
const verified = await verifyReviewFileContent(file, applied);
|
|
1438
|
+
if (verified.ok) return { ok: true };
|
|
1469
1439
|
const rejectedAnyForFile = applied.length > appliedCountBefore;
|
|
1470
1440
|
return {
|
|
1471
1441
|
...verified,
|
|
@@ -1476,7 +1446,7 @@
|
|
|
1476
1446
|
};
|
|
1477
1447
|
}
|
|
1478
1448
|
|
|
1479
|
-
const rejectControl = findRejectControlForTrackedChangeNode(node);
|
|
1449
|
+
const rejectControl = isTrackedChangeNodeInNativeScope(node, file.path) ? findRejectControlForTrackedChangeNode(node) : null;
|
|
1480
1450
|
if (!rejectControl) {
|
|
1481
1451
|
return {
|
|
1482
1452
|
ok: false,
|
|
@@ -1487,6 +1457,7 @@
|
|
|
1487
1457
|
|
|
1488
1458
|
const trackedChange = trackedChangeRefFromNode(node, file.path);
|
|
1489
1459
|
const beforeText = readActiveEditorText();
|
|
1460
|
+
selection.assertStable();
|
|
1490
1461
|
clickNode(rejectControl);
|
|
1491
1462
|
await waitForTrackedChangeRejectProgress(trackedChange, file.path, beforeText, 1200);
|
|
1492
1463
|
applied.push({
|
|
@@ -1532,13 +1503,10 @@
|
|
|
1532
1503
|
return false;
|
|
1533
1504
|
}
|
|
1534
1505
|
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
1506
|
const captureModule = window?.CodexOverleafTrackedChangeCapture
|
|
1539
1507
|
|| (typeof module === 'object' && module.exports ? require('./trackedChangeCapture.js') : null);
|
|
1540
1508
|
const { collectTrackedChangeNodes, trackedChangeRefFromNode, collectTrackedChangeRefsForPaths,
|
|
1541
|
-
waitForTrackedChangeDiff, prepareTrackedChangeCapture, prepareTrackedChangeReview, prepareUntrackedUndo, getTrackedChangeCaptureStatus,
|
|
1509
|
+
waitForTrackedChangeDiff, prepareTrackedChangeCapture, prepareTrackedChangeReview, prepareUntrackedUndo, getTrackedChangeCaptureStatus, isTrackedChangeNodeInNativeScope, rememberTrackedChangeNodeContext,
|
|
1542
1510
|
captureTrackedWrite, reconcileTrackedChangeCapture } = captureModule.create(deps);
|
|
1543
1511
|
|
|
1544
1512
|
function orderTrackedChangesForReviewAction(refs = []) {
|
|
@@ -1561,9 +1529,56 @@
|
|
|
1561
1529
|
return nodes[0] || null;
|
|
1562
1530
|
}
|
|
1563
1531
|
|
|
1564
|
-
function
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1532
|
+
async function createReviewScopeGuard(path, expectedText) {
|
|
1533
|
+
const owner = nativeRejectPlan?.files?.find(file => file.path === path);
|
|
1534
|
+
const changed = () => {
|
|
1535
|
+
const failure = nativeReviewBlocked(path, 'native_review_scope_changed').skipped[0].result;
|
|
1536
|
+
throw Object.assign(new Error(failure.reason), { reviewFailure: failure });
|
|
1537
|
+
};
|
|
1538
|
+
let snapshot = prepareTrackedChangeCapture(path);
|
|
1539
|
+
const until = Date.now() + 1500;
|
|
1540
|
+
while ((owner || snapshot.source === 'native') && (snapshot.source !== 'native' || !snapshot.ready) && Date.now() < until) {
|
|
1541
|
+
if (getActiveFilePath() !== path || (expectedText !== undefined && readActiveEditorText() !== expectedText)) changed();
|
|
1542
|
+
await delay(Math.min(60, until - Date.now()));
|
|
1543
|
+
snapshot = prepareTrackedChangeCapture(path);
|
|
1544
|
+
}
|
|
1545
|
+
if (owner || snapshot.source === 'native') {
|
|
1546
|
+
const owned = (owner?.scope.refs || []).map(ref => JSON.stringify([ref.id, ref.kind, ref.textHash]));
|
|
1547
|
+
if (!snapshot.ready || !snapshot.refs.length || !owner || snapshot.nativeDocId !== owner.nativeDocId || snapshot.refs.some(ref => {
|
|
1548
|
+
const index = owned.indexOf(JSON.stringify([ref.id, ref.kind, ref.textHash]));
|
|
1549
|
+
if (index < 0) return true;
|
|
1550
|
+
owned.splice(index, 1); return false;
|
|
1551
|
+
})) changed();
|
|
1552
|
+
}
|
|
1553
|
+
const beforeText = expectedText ?? readActiveEditorText();
|
|
1554
|
+
const key = value => value.source !== 'native' ? '' : !value.ready ? null
|
|
1555
|
+
: JSON.stringify([value.nativeDocId, value.refs.map(ref => ref.key).sort()]);
|
|
1556
|
+
const beforeScope = key(snapshot);
|
|
1557
|
+
return () => {
|
|
1558
|
+
if (getActiveFilePath() !== path || readActiveEditorText() !== beforeText
|
|
1559
|
+
|| key(prepareTrackedChangeCapture(path)) !== beforeScope) changed();
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
async function waitForRejectableTrackedChange(path) {
|
|
1563
|
+
const until = Date.now() + 1500;
|
|
1564
|
+
const select = () => {
|
|
1565
|
+
const nodes = findTrackedChangeNodesForPath(path).reverse();
|
|
1566
|
+
for (const node of nodes) {
|
|
1567
|
+
if (!isTrackedChangeNodeInNativeScope(node, path)) continue;
|
|
1568
|
+
const control = findRejectControlForTrackedChangeNode(node);
|
|
1569
|
+
if (control) return { node, control };
|
|
1570
|
+
}
|
|
1571
|
+
return { node: nodes[0] || null, control: null };
|
|
1572
|
+
};
|
|
1573
|
+
let selection = select();
|
|
1574
|
+
if (!selection.node) return { ...selection, assertStable() {} };
|
|
1575
|
+
const assertStable = await createReviewScopeGuard(path);
|
|
1576
|
+
selection = select();
|
|
1577
|
+
while (!selection.control && Date.now() < until) {
|
|
1578
|
+
assertStable(); await delay(Math.min(60, until - Date.now())); assertStable();
|
|
1579
|
+
selection = select();
|
|
1580
|
+
}
|
|
1581
|
+
return { ...selection, assertStable };
|
|
1567
1582
|
}
|
|
1568
1583
|
|
|
1569
1584
|
function findTrackedChangeNodesForPath(path) {
|
|
@@ -1576,6 +1591,7 @@
|
|
|
1576
1591
|
}
|
|
1577
1592
|
|
|
1578
1593
|
function findRejectControlForTrackedChangeNode(node) {
|
|
1594
|
+
if (!node || node.isConnected === false) return null;
|
|
1579
1595
|
const scopes = [];
|
|
1580
1596
|
let current = node;
|
|
1581
1597
|
for (let index = 0; current && index < 6; index += 1) {
|
|
@@ -1602,17 +1618,19 @@
|
|
|
1602
1618
|
}
|
|
1603
1619
|
|
|
1604
1620
|
function isRejectTrackedChangeControl(node) {
|
|
1605
|
-
if (!node || node.disabled) {
|
|
1621
|
+
if (!node || node.disabled || node.isConnected === false) {
|
|
1606
1622
|
return false;
|
|
1607
1623
|
}
|
|
1608
1624
|
if (/^(true|disabled)$/i.test(node.getAttribute?.('aria-disabled') || '')) {
|
|
1609
1625
|
return false;
|
|
1610
1626
|
}
|
|
1611
|
-
const signal = normalizeReviewingSignalText(readNodeSignalText(node)
|
|
1612
|
-
|
|
1627
|
+
const signal = normalizeReviewingSignalText([readNodeSignalText(node),
|
|
1628
|
+
...Array.from(node.querySelectorAll?.('.visually-hidden') || [])
|
|
1629
|
+
.map(label => label.textContent || '')].join(' '));
|
|
1630
|
+
if (/\b(?:accept|approve|apply|resolve)\b|接受|批准|应用/i.test(signal)) {
|
|
1613
1631
|
return false;
|
|
1614
1632
|
}
|
|
1615
|
-
return /\b(?:reject|decline|discard|revert
|
|
1633
|
+
return /\b(?:reject|decline|discard|revert)\b|拒绝|丢弃|还原/i.test(signal);
|
|
1616
1634
|
}
|
|
1617
1635
|
return {
|
|
1618
1636
|
acceptTrackedChanges,
|