codex-overleaf-link 2.3.6 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -34
- package/README.zh-CN.md +546 -0
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +3 -0
- package/extension/src/content/activeTurnControl.js +1 -0
- package/extension/src/content/assetTransferBroker.js +2 -0
- package/extension/src/content/contentRuntime.js +122 -118
- package/extension/src/content/contextTray.js +4 -0
- package/extension/src/content/diagnosticsController.js +4 -0
- package/extension/src/content/diagnosticsPanel.js +128 -4
- package/extension/src/content/generated/content.bundle.js +1233 -550
- package/extension/src/content/generated/content.bundle.meta.json +4 -3
- package/extension/src/content/legacyGlobalRegistry.js +1 -0
- package/extension/src/content/modelPicker.js +21 -9
- package/extension/src/content/modelPickerSupport.js +38 -1
- package/extension/src/content/nativeCompatibilityController.js +4 -1
- package/extension/src/content/otWarmMirror.js +34 -30
- package/extension/src/content/otWarmMirrorController.js +20 -0
- package/extension/src/content/pageBridgeClient.js +2 -0
- package/extension/src/content/panelMaintenance.js +8 -4
- package/extension/src/content/recentProjects.js +354 -355
- package/extension/src/content/runResultActions.js +16 -1
- package/extension/src/content/runTimelineView.js +9 -11
- package/extension/src/content/scopedPersistenceCoordinator.js +2 -1
- package/extension/src/content/scopedPersistencePanelState.js +45 -2
- package/extension/src/content/sessionForkController.js +20 -11
- package/extension/src/content/sessionPersistence.js +4 -1
- package/extension/src/content/settingsPanel.js +16 -2
- package/extension/src/content/trackedChangeCaptureController.js +153 -0
- package/extension/src/content/writebackOrchestrator.js +31 -0
- package/extension/src/page/binaryAssetUploader.js +101 -29
- package/extension/src/page/overleafRealtimeObserver.js +4 -1
- package/extension/src/page/saveState.js +44 -1
- package/extension/src/page/textFileCreator.js +493 -0
- package/extension/src/page/trackedChangeCapture.js +342 -0
- package/extension/src/page/trackedChangesLifecycle.js +149 -150
- package/extension/src/page/treeOperations.js +40 -29
- package/extension/src/page/writebackRouter.js +95 -49
- package/extension/src/pageBridge.js +65 -25
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/i18n.js +94 -40
- package/extension/src/shared/pageRpcContract.js +2 -1
- package/extension/src/shared/sessionState.js +6 -2
- package/extension/src/shared/storageDb.js +11 -12
- package/extension/src/shared/storageRunActions.js +73 -1
- package/extension/src/shared/writebackSettlement.js +19 -5
- package/extension/styles/panel.css +254 -405
- package/native-host/src/codexSessionRunner.js +41 -38
- package/native-host/src/codexSessionTiming.js +68 -0
- package/native-host/src/readProgressGuard.js +84 -17
- package/native-host/src/taskRunnerRuntime.js +25 -0
- package/package.json +5 -1
|
@@ -50,6 +50,122 @@
|
|
|
50
50
|
collectElements,
|
|
51
51
|
} = deps;
|
|
52
52
|
|
|
53
|
+
function nativeReviewBlocked(path, code, applied = [], requestStarted = false) {
|
|
54
|
+
const reason = requestStarted
|
|
55
|
+
? '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.'
|
|
56
|
+
: 'The native tracked-change scope could not be safely isolated. No review mutation was attempted. Use Overleaf Review to handle the changes individually.';
|
|
57
|
+
return { ok: false, applied, skipped: [{ trackedChange: { path }, result: {
|
|
58
|
+
ok: false, code, reason, failure: {
|
|
59
|
+
...buildPageFailure('tracked_changes_remain', { file: path, operationType: 'review',
|
|
60
|
+
changedDocument: requestStarted, userMessage: reason,
|
|
61
|
+
evidence: { originalCode: code, writeStarted: requestStarted, acceptedCount: applied.length } }),
|
|
62
|
+
// Preserve recovery instead of applying Accept's legacy partial-success policy.
|
|
63
|
+
severity: 'blocked', terminalState: 'blocked'
|
|
64
|
+
}
|
|
65
|
+
} }] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function waitForNativeReview(path, refs, untracked = false) {
|
|
69
|
+
const prepare = () => untracked ? prepareUntrackedUndo(path) : prepareTrackedChangeReview(path, refs);
|
|
70
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
71
|
+
const scope = prepare();
|
|
72
|
+
if (scope.ok) return scope;
|
|
73
|
+
await delay(150);
|
|
74
|
+
}
|
|
75
|
+
return prepare();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function prepareNativeReview(params, allowUntrackedUndo = false) {
|
|
79
|
+
const refs = normalizeTrackedChangeRefs(params.trackedChanges || []);
|
|
80
|
+
// A configured browser adapter fails closed even while its ledger is loading.
|
|
81
|
+
// Legacy adapter callers without native integration retain their original path.
|
|
82
|
+
if (typeof deps.getCodeMirrorEditorView !== 'function'
|
|
83
|
+
&& !refs.some(ref => ref.key.startsWith('native:'))) return null;
|
|
84
|
+
const untracked = allowUntrackedUndo && params.untrackedUndo === true && !refs.length;
|
|
85
|
+
if (untracked) {
|
|
86
|
+
const keys = files => Array.isArray(files) && files.length && files.every(file =>
|
|
87
|
+
normalizeSafeProjectPath(file?.path) && typeof file.content === 'string')
|
|
88
|
+
? files.map(file => normalizeSafeProjectPath(file.path)).sort() : [];
|
|
89
|
+
const before = keys(params.expectedFiles), after = keys(params.postFiles);
|
|
90
|
+
if (!before.length || new Set(before).size !== before.length || JSON.stringify(before) !== JSON.stringify(after))
|
|
91
|
+
return nativeReviewBlocked('', 'native_untracked_undo_checkpoint_unavailable');
|
|
92
|
+
}
|
|
93
|
+
const checkpoints = [...(Array.isArray(params.expectedFiles) ? params.expectedFiles : []),
|
|
94
|
+
...(Array.isArray(params.postFiles) ? params.postFiles : [])];
|
|
95
|
+
const paths = [...new Set([...refs.map(ref => ref.path),
|
|
96
|
+
...checkpoints.map(file => normalizeSafeProjectPath(file?.path))])];
|
|
97
|
+
if ((!refs.length && !untracked) || refs.some(ref => ref.invalidProjectPath || !ref.path) || paths.some(path => !path))
|
|
98
|
+
return nativeReviewBlocked('', 'native_review_identity_unavailable');
|
|
99
|
+
const files = [], deadline = Date.now() + 90000;
|
|
100
|
+
for (const path of paths) {
|
|
101
|
+
if (Date.now() >= deadline || checkWritebackRunProjectId(params))
|
|
102
|
+
return nativeReviewBlocked(path, 'native_review_context_changed');
|
|
103
|
+
if (getActiveFilePath() !== path && !(await openFileByPath(path)).ok)
|
|
104
|
+
return nativeReviewBlocked(path, 'native_review_file_unavailable');
|
|
105
|
+
const scope = await waitForNativeReview(path, refs, untracked);
|
|
106
|
+
if (!scope.ok) return nativeReviewBlocked(path, scope.reason || 'native_review_unavailable');
|
|
107
|
+
files.push({ path, nativeDocId: scope.nativeDocId, scope });
|
|
108
|
+
}
|
|
109
|
+
return { ok: true, files, refs, deadline };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function acceptNativeReview(params, plan) {
|
|
113
|
+
const applied = [];
|
|
114
|
+
let token = window?.document?.querySelector?.('meta[name="ol-csrfToken"]')?.getAttribute('content');
|
|
115
|
+
try { token = JSON.parse(token); } catch (_error) { /* Older pages expose plain text. */ }
|
|
116
|
+
if (typeof token !== 'string' || !token || token.length > 4096
|
|
117
|
+
|| typeof window?.fetch !== 'function' || typeof window?.AbortController !== 'function')
|
|
118
|
+
return nativeReviewBlocked('', 'native_accept_transport_unavailable');
|
|
119
|
+
// History-OT uses snapshot-range operations, not the classic ID endpoint.
|
|
120
|
+
// An unsupported engine must never fall through to text undo/replay.
|
|
121
|
+
const unsupported = plan.files.find(file => !file.scope.acceptByIdSupported);
|
|
122
|
+
if (unsupported) return nativeReviewBlocked(unsupported.path, 'native_accept_protocol_unsupported');
|
|
123
|
+
for (const file of plan.files) {
|
|
124
|
+
const path = file.path;
|
|
125
|
+
if (Date.now() >= plan.deadline || checkWritebackRunProjectId(params))
|
|
126
|
+
return nativeReviewBlocked(path, 'native_review_context_changed', applied, applied.length > 0);
|
|
127
|
+
if (getActiveFilePath() !== path && !(await openFileByPath(path)).ok)
|
|
128
|
+
return nativeReviewBlocked(path, 'native_review_file_unavailable', applied, applied.length > 0);
|
|
129
|
+
await waitForNativeReview(path, plan.refs);
|
|
130
|
+
const scope = prepareTrackedChangeReview(path, plan.refs), text = readActiveEditorText();
|
|
131
|
+
if (!scope.ok || !scope.acceptByIdSupported || scope.nativeDocId !== file.nativeDocId
|
|
132
|
+
|| typeof text !== 'string' || Date.now() >= plan.deadline || checkWritebackRunProjectId(params))
|
|
133
|
+
return nativeReviewBlocked(path, 'native_review_scope_changed', applied, applied.length > 0);
|
|
134
|
+
const preserved = JSON.stringify(scope.unrelated.map(ref => ref.key).sort()), ids = new Set(scope.ids);
|
|
135
|
+
const controller = new window.AbortController();
|
|
136
|
+
const timer = window.setTimeout(() => controller.abort(), Math.min(12000, Math.max(1, plan.deadline - Date.now())));
|
|
137
|
+
try {
|
|
138
|
+
// Same ID-scoped endpoint and CSRF header used by Overleaf's RangesProvider.
|
|
139
|
+
const response = await window.fetch('/project/' + encodeURIComponent(params.runProjectId)
|
|
140
|
+
+ '/doc/' + encodeURIComponent(scope.nativeDocId) + '/changes/accept', {
|
|
141
|
+
method: 'POST', credentials: 'same-origin', redirect: 'error', signal: controller.signal,
|
|
142
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-Csrf-Token': token },
|
|
143
|
+
body: JSON.stringify({ change_ids: scope.ids })
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) return nativeReviewBlocked(path, 'native_accept_http_' + response.status, applied, true);
|
|
146
|
+
} catch (_error) {
|
|
147
|
+
return nativeReviewBlocked(path, 'native_accept_request_unconfirmed', applied, true);
|
|
148
|
+
} finally { window.clearTimeout(timer); }
|
|
149
|
+
let verified = false;
|
|
150
|
+
const until = Math.min(plan.deadline, Date.now() + 8000);
|
|
151
|
+
while (Date.now() < until) {
|
|
152
|
+
if (checkWritebackRunProjectId(params) || getActiveFilePath() !== path || readActiveEditorText() !== text)
|
|
153
|
+
return nativeReviewBlocked(path, 'native_accept_content_or_context_changed', applied, true);
|
|
154
|
+
const after = prepareTrackedChangeCapture(path);
|
|
155
|
+
if (after.source === 'native' && after.ready && after.nativeDocId === scope.nativeDocId) {
|
|
156
|
+
if (JSON.stringify(after.refs.filter(ref => !ids.has(ref.id)).map(ref => ref.key).sort()) !== preserved)
|
|
157
|
+
return nativeReviewBlocked(path, 'native_accept_unrelated_changes_changed', applied, true);
|
|
158
|
+
if (!after.refs.some(ref => ids.has(ref.id))) { verified = true; break; }
|
|
159
|
+
}
|
|
160
|
+
await delay(150);
|
|
161
|
+
}
|
|
162
|
+
if (!verified) return nativeReviewBlocked(path, 'native_accept_not_verified', applied, true);
|
|
163
|
+
applied.push(...scope.targets.map(trackedChange => ({ trackedChange,
|
|
164
|
+
result: { ok: true, method: 'overleaf-native-accept', verified: true } })));
|
|
165
|
+
}
|
|
166
|
+
return { ok: true, verified: true, applied, skipped: [] };
|
|
167
|
+
}
|
|
168
|
+
|
|
53
169
|
async function rejectTrackedChanges(params = {}) {
|
|
54
170
|
// Welcome-panel + write-guard: defense-in-depth.
|
|
55
171
|
// The pageBridge wrapper already runs the runProjectId guard, but a
|
|
@@ -77,6 +193,10 @@
|
|
|
77
193
|
};
|
|
78
194
|
}
|
|
79
195
|
|
|
196
|
+
const nativeReview = await prepareNativeReview(params, true);
|
|
197
|
+
if (nativeReview && !nativeReview.ok) return nativeReview;
|
|
198
|
+
const mixedFile = nativeReview?.files.find(file => file.scope.unrelated.length);
|
|
199
|
+
if (mixedFile) return nativeReviewBlocked(mixedFile.path, 'native_reject_mixed_changes');
|
|
80
200
|
const editorUndo = await rejectTrackedChangesViaEditorUndo(expectedFiles, postFiles, applied);
|
|
81
201
|
if (editorUndo.ok) {
|
|
82
202
|
if (applied.length > 0) {
|
|
@@ -250,21 +370,8 @@
|
|
|
250
370
|
};
|
|
251
371
|
}
|
|
252
372
|
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
// 1. Editor-undo the run's tracked writeback back to its pre-write content
|
|
257
|
-
// (the reject path's primary mechanism), which makes every one of the
|
|
258
|
-
// run's tracked changes vanish.
|
|
259
|
-
// 2. Switch Overleaf to Editing mode (Track Changes OFF) and re-apply the
|
|
260
|
-
// run's post-write content as a plain, untracked edit.
|
|
261
|
-
//
|
|
262
|
-
// Net result: the run's new content lands as permanent, untracked text — the
|
|
263
|
-
// run is decisively accepted, with no DOM-control hunting.
|
|
264
|
-
//
|
|
265
|
-
// If the editor-undo cannot reach the pre-write state (content drifted — e.g.
|
|
266
|
-
// the user edited after the run), this bails WITHOUT re-writing so it never
|
|
267
|
-
// makes the document worse, mirroring Undo's safety stance.
|
|
373
|
+
// Native-ledger browsers accept exact IDs without changing document text.
|
|
374
|
+
// The original undo/replay flow remains only for legacy adapter callers.
|
|
268
375
|
async function acceptTrackedChanges(params = {}) {
|
|
269
376
|
// Welcome-panel + write-guard: defense-in-depth.
|
|
270
377
|
// The pageBridge wrapper already runs the runProjectId guard, but a
|
|
@@ -273,6 +380,8 @@
|
|
|
273
380
|
// guard uses.
|
|
274
381
|
const writeGuardBlock = checkWritebackRunProjectId(params);
|
|
275
382
|
if (writeGuardBlock) return writeGuardBlock;
|
|
383
|
+
const nativeReview = await prepareNativeReview(params);
|
|
384
|
+
if (nativeReview) return nativeReview.ok ? acceptNativeReview(params, nativeReview) : nativeReview;
|
|
276
385
|
const expectedFiles = Array.isArray(params.expectedFiles) ? params.expectedFiles : [];
|
|
277
386
|
const postFiles = Array.isArray(params.postFiles) ? params.postFiles : [];
|
|
278
387
|
const applied = [];
|
|
@@ -377,20 +486,8 @@
|
|
|
377
486
|
};
|
|
378
487
|
}
|
|
379
488
|
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
// ensureEditing() short-circuits on isEditingConfirmedForNoTraceUndo, whose
|
|
384
|
-
// "already Editing" detection is negation-based and false-positives while
|
|
385
|
-
// Track Changes is actually ON (e.g. the Reviewing mode shows only as a
|
|
386
|
-
// dropdown-trigger label with no active aria attribute, so reviewing is not
|
|
387
|
-
// positively confirmed and a stray "Editing" menu option is read as the
|
|
388
|
-
// current mode). Trusting that short-circuit replays the run while tracked.
|
|
389
|
-
//
|
|
390
|
-
// So do not trust the short-circuit here: read the reviewing state
|
|
391
|
-
// explicitly, and if Reviewing/Track Changes is on — or Editing is not
|
|
392
|
-
// positively confirmed OFF — force the toggle via setReviewingEnabled(false)
|
|
393
|
-
// rather than ensureEditing's lenient path.
|
|
489
|
+
// Legacy replay requires positively confirmed Editing. The strict
|
|
490
|
+
// detector and its rationale are kept in forceEditingForAcceptReplay.
|
|
394
491
|
const modeBefore = getReviewingState({});
|
|
395
492
|
pushDiagnostic('modeBefore', summarizeReviewingStateForDiagnostics(modeBefore));
|
|
396
493
|
const editingSwitch = await forceEditingForAcceptReplay();
|
|
@@ -1039,6 +1136,7 @@
|
|
|
1039
1136
|
const paths = Array.from(expectedByPath.keys()).filter(path => postByPath.has(path));
|
|
1040
1137
|
const rebasedExpectedByPath = new Map();
|
|
1041
1138
|
const rebasedPostByPath = new Map();
|
|
1139
|
+
const undoRebaseProofs = new Map();
|
|
1042
1140
|
if (!paths.length) {
|
|
1043
1141
|
return {
|
|
1044
1142
|
ok: false,
|
|
@@ -1084,6 +1182,9 @@
|
|
|
1084
1182
|
}
|
|
1085
1183
|
rebasedExpectedByPath.set(path, ready.expectedContent);
|
|
1086
1184
|
rebasedPostByPath.set(path, ready.postContent);
|
|
1185
|
+
if (ready.prefixLength || ready.suffixLength) {
|
|
1186
|
+
undoRebaseProofs.set(path, { version: 1, path, beforeUndoContent: ready.postContent });
|
|
1187
|
+
}
|
|
1087
1188
|
}
|
|
1088
1189
|
|
|
1089
1190
|
const operations = paths.map(path => ({
|
|
@@ -1106,7 +1207,11 @@
|
|
|
1106
1207
|
path: item.operation?.path || '',
|
|
1107
1208
|
label: 'No-trace snapshot undo'
|
|
1108
1209
|
},
|
|
1109
|
-
result: item.result
|
|
1210
|
+
result: item.result?.ok !== false && item.result?.verified === true
|
|
1211
|
+
&& undoRebaseProofs.has(item.operation?.path)
|
|
1212
|
+
&& item.result.verifiedContent === rebasedExpectedByPath.get(item.operation?.path)
|
|
1213
|
+
? { ...item.result, undoRebaseProof: undoRebaseProofs.get(item.operation.path) }
|
|
1214
|
+
: item.result
|
|
1110
1215
|
});
|
|
1111
1216
|
return {
|
|
1112
1217
|
ok: !(result.skipped || []).length,
|
|
@@ -1444,124 +1549,11 @@
|
|
|
1444
1549
|
|
|
1445
1550
|
|
|
1446
1551
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
if (!ref?.key || beforeKeys.has(ref.key) || seen.has(ref.key)) {
|
|
1453
|
-
continue;
|
|
1454
|
-
}
|
|
1455
|
-
seen.add(ref.key);
|
|
1456
|
-
added.push(ref);
|
|
1457
|
-
}
|
|
1458
|
-
return added;
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
async function waitForTrackedChangeDiff(trackedBefore, paths, options = {}) {
|
|
1462
|
-
const waitMs = Number.isFinite(Number(options.waitMs)) ? Number(options.waitMs) : 3000;
|
|
1463
|
-
const intervalMs = Number.isFinite(Number(options.intervalMs)) ? Number(options.intervalMs) : 180;
|
|
1464
|
-
const deadline = Date.now() + Math.max(0, waitMs);
|
|
1465
|
-
let latest = [];
|
|
1466
|
-
while (Date.now() <= deadline) {
|
|
1467
|
-
const trackedAfter = collectTrackedChangeRefsForPaths(paths);
|
|
1468
|
-
latest = diffTrackedChangeRefs(trackedBefore, trackedAfter);
|
|
1469
|
-
if (latest.length > 0) {
|
|
1470
|
-
return {
|
|
1471
|
-
ok: true,
|
|
1472
|
-
trackedChanges: latest,
|
|
1473
|
-
waitMs
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
1476
|
-
await delay(intervalMs);
|
|
1477
|
-
}
|
|
1478
|
-
return {
|
|
1479
|
-
ok: true,
|
|
1480
|
-
trackedChanges: latest,
|
|
1481
|
-
waitMs
|
|
1482
|
-
};
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
function collectTrackedChangeRefsForPaths(paths = []) {
|
|
1486
|
-
const pathSet = new Set((paths || []).filter(Boolean));
|
|
1487
|
-
const activePath = getActiveFilePath();
|
|
1488
|
-
return collectTrackedChangeNodes()
|
|
1489
|
-
.map(node => trackedChangeRefFromNode(node, activePath))
|
|
1490
|
-
.filter(ref => ref.key)
|
|
1491
|
-
.filter(ref => !pathSet.size || !ref.path || pathSet.has(ref.path));
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
function collectTrackedChangeNodes() {
|
|
1495
|
-
const selector = [
|
|
1496
|
-
'[data-change-id]',
|
|
1497
|
-
'[data-review-id]',
|
|
1498
|
-
'[data-track-change-id]',
|
|
1499
|
-
'[data-ol-change-id]',
|
|
1500
|
-
'[data-path][class*="change" i]',
|
|
1501
|
-
'.ol-cm-change',
|
|
1502
|
-
'.review-panel-entry-change',
|
|
1503
|
-
'[class*="track-change" i]',
|
|
1504
|
-
'[class*="review-change" i]',
|
|
1505
|
-
'[class*="suggest" i]',
|
|
1506
|
-
'[aria-label*="change" i]',
|
|
1507
|
-
'[title*="change" i]'
|
|
1508
|
-
].join(',');
|
|
1509
|
-
return uniqueNodes([
|
|
1510
|
-
...collectElements(selector, 1200),
|
|
1511
|
-
...collectElements('*', 3500).filter(isTrackedChangeNode)
|
|
1512
|
-
]).filter(isTrackedChangeNode);
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
function isTrackedChangeNode(node) {
|
|
1516
|
-
if (!node || isInsideCodexPanel(node)) {
|
|
1517
|
-
return false;
|
|
1518
|
-
}
|
|
1519
|
-
const signal = normalizeReviewingSignalText(readNodeSignalText(node));
|
|
1520
|
-
const tag = (node.tagName || '').toLowerCase();
|
|
1521
|
-
if (tag === 'button' && /\b(?:accept|reject|decline|discard|批准|接受|拒绝|丢弃)\b/i.test(signal)) {
|
|
1522
|
-
return false;
|
|
1523
|
-
}
|
|
1524
|
-
if (readTrackedChangeId(node)) {
|
|
1525
|
-
return true;
|
|
1526
|
-
}
|
|
1527
|
-
return /\b(?:tracked change|track change|review change|suggestion|insert(?:ion)?|delet(?:e|ion)|change)\b/i.test(signal)
|
|
1528
|
-
|| /留痕|建议|插入|删除|更改|修改/.test(signal);
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
function trackedChangeRefFromNode(node, fallbackPath = '') {
|
|
1532
|
-
const id = readTrackedChangeId(node);
|
|
1533
|
-
const key = id ? `id:${id}` : `sig:${compact(readNodeSignalText(node), 180)}`;
|
|
1534
|
-
const path = normalizeSafeProjectPath(
|
|
1535
|
-
node.getAttribute?.('data-path')
|
|
1536
|
-
|| node.getAttribute?.('data-file-path')
|
|
1537
|
-
|| node.getAttribute?.('data-doc-path')
|
|
1538
|
-
|| fallbackPath
|
|
1539
|
-
|| ''
|
|
1540
|
-
);
|
|
1541
|
-
return {
|
|
1542
|
-
key,
|
|
1543
|
-
id,
|
|
1544
|
-
path,
|
|
1545
|
-
label: compact(readNodeSignalText(node), 180)
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
function readTrackedChangeId(node) {
|
|
1550
|
-
for (const attribute of [
|
|
1551
|
-
'data-change-id',
|
|
1552
|
-
'data-review-id',
|
|
1553
|
-
'data-track-change-id',
|
|
1554
|
-
'data-ol-change-id',
|
|
1555
|
-
'data-id',
|
|
1556
|
-
'id'
|
|
1557
|
-
]) {
|
|
1558
|
-
const value = node.getAttribute?.(attribute) || '';
|
|
1559
|
-
if (value && /\b(?:change|review|track|suggest)|\d|[a-f0-9-]{8,}/i.test(value)) {
|
|
1560
|
-
return String(value);
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
return '';
|
|
1564
|
-
}
|
|
1552
|
+
const captureModule = window?.CodexOverleafTrackedChangeCapture
|
|
1553
|
+
|| (typeof module === 'object' && module.exports ? require('./trackedChangeCapture.js') : null);
|
|
1554
|
+
const { collectTrackedChangeNodes, trackedChangeRefFromNode, collectTrackedChangeRefsForPaths,
|
|
1555
|
+
waitForTrackedChangeDiff, prepareTrackedChangeCapture, prepareTrackedChangeReview, prepareUntrackedUndo, getTrackedChangeCaptureStatus,
|
|
1556
|
+
captureTrackedWrite, reconcileTrackedChangeCapture } = captureModule.create(deps);
|
|
1565
1557
|
|
|
1566
1558
|
function orderTrackedChangesForReviewAction(refs = []) {
|
|
1567
1559
|
return (refs || []).slice().reverse();
|
|
@@ -1573,7 +1565,8 @@
|
|
|
1573
1565
|
return null;
|
|
1574
1566
|
}
|
|
1575
1567
|
return collectTrackedChangeNodes()
|
|
1576
|
-
.find(node => trackedChangeRefFromNode(node, ref.path || getActiveFilePath()).key === targetKey
|
|
1568
|
+
.find(node => trackedChangeRefFromNode(node, ref.path || getActiveFilePath()).key === targetKey
|
|
1569
|
+
|| (targetKey.startsWith('sig:') && 'sig:' + compact(readNodeSignalText(node), 180) === targetKey))
|
|
1577
1570
|
|| null;
|
|
1578
1571
|
}
|
|
1579
1572
|
|
|
@@ -1637,8 +1630,14 @@
|
|
|
1637
1630
|
}
|
|
1638
1631
|
return {
|
|
1639
1632
|
acceptTrackedChanges,
|
|
1640
|
-
rejectTrackedChanges
|
|
1633
|
+
rejectTrackedChanges: async params => {
|
|
1634
|
+
const result = await rejectTrackedChanges(params);
|
|
1635
|
+
return deps.confirmReviewWriteback ? deps.confirmReviewWriteback(params, result) : result;
|
|
1636
|
+
},
|
|
1641
1637
|
collectTrackedChangeRefsForPaths,
|
|
1638
|
+
prepareTrackedChangeCapture, getTrackedChangeCaptureStatus,
|
|
1639
|
+
captureTrackedWrite,
|
|
1640
|
+
reconcileTrackedChangeCapture,
|
|
1642
1641
|
waitForTrackedChangeDiff
|
|
1643
1642
|
};
|
|
1644
1643
|
}
|
|
@@ -97,6 +97,25 @@
|
|
|
97
97
|
return match ? decodeURIComponent(match[1]) : null;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function readNativeTreePath(node) {
|
|
101
|
+
const row = node?.closest?.('[role="treeitem"]');
|
|
102
|
+
const root = document.querySelector?.('[data-testid="file-tree-list-root"]');
|
|
103
|
+
if (!row || !root?.contains?.(row) || !row.querySelector?.('[data-file-type="doc"][data-file-id]')) return '';
|
|
104
|
+
const parts = [row.getAttribute('aria-label')];
|
|
105
|
+
let group = row.closest('[role="tree"]');
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
while (group && group !== root) {
|
|
108
|
+
if (seen.has(group)) return '';
|
|
109
|
+
seen.add(group);
|
|
110
|
+
const folder = group.previousElementSibling;
|
|
111
|
+
if (folder?.getAttribute?.('role') !== 'treeitem' || !folder.querySelector?.('[data-file-type="folder"]')) return '';
|
|
112
|
+
parts.unshift(folder.getAttribute('aria-label'));
|
|
113
|
+
group = group.parentElement?.closest?.('[role="tree"]');
|
|
114
|
+
}
|
|
115
|
+
if (group !== root || parts.some(part => !part || /[/\\]/.test(part))) return '';
|
|
116
|
+
return normalizeSafeProjectPath(parts.join('/'));
|
|
117
|
+
}
|
|
118
|
+
|
|
100
119
|
function getActiveFilePath() {
|
|
101
120
|
const editorStorePath = getEditorStoreFilePath({ disambiguateBasename: true });
|
|
102
121
|
if (editorStorePath && window.CodexOverleafProjectFiles.isTextProjectPath(editorStorePath) && !hasMultipleTextPathExtensions(editorStorePath)) {
|
|
@@ -121,7 +140,7 @@
|
|
|
121
140
|
for (const selector of selectors) {
|
|
122
141
|
const nodes = Array.from(document.querySelectorAll?.(selector) || []);
|
|
123
142
|
for (const node of nodes) {
|
|
124
|
-
const path = node ? readProjectPathFromNode(node) : '';
|
|
143
|
+
const path = node ? readNativeTreePath(node) || readProjectPathFromNode(node) : '';
|
|
125
144
|
if (isActiveFilePathCandidate(path, node)) {
|
|
126
145
|
candidates.push({
|
|
127
146
|
node,
|
|
@@ -408,6 +427,11 @@
|
|
|
408
427
|
const pathNodes = typeof event?.composedPath === 'function'
|
|
409
428
|
? event.composedPath()
|
|
410
429
|
: [];
|
|
430
|
+
if (pathNodes.some(node => node?.id === 'codex-overleaf-panel')
|
|
431
|
+
|| event?.target?.closest?.('#codex-overleaf-panel')) return '';
|
|
432
|
+
const insideTree = pathNodes.some(node => /(?:^|\s)(?:file-tree|project-tree|file-tree-list)(?:\s|$)/.test(String(node?.className || '')))
|
|
433
|
+
|| event?.target?.closest?.('.file-tree, .project-tree, .file-tree-list');
|
|
434
|
+
if (!insideTree) return '';
|
|
411
435
|
for (const node of pathNodes) {
|
|
412
436
|
if (!node || typeof node.getAttribute !== 'function') {
|
|
413
437
|
continue;
|
|
@@ -1028,7 +1052,7 @@
|
|
|
1028
1052
|
return labelPath;
|
|
1029
1053
|
}
|
|
1030
1054
|
|
|
1031
|
-
const textPath = normalizeProjectPathCandidate(node
|
|
1055
|
+
const textPath = normalizeProjectPathCandidate(readFileTreeNodeText(node));
|
|
1032
1056
|
if (!textPath) {
|
|
1033
1057
|
const docIdPath = resolveProjectPathFromDomDocId(node);
|
|
1034
1058
|
const domTreePath = resolveProjectPathFromDomTree(node);
|
|
@@ -1157,7 +1181,7 @@
|
|
|
1157
1181
|
node.getAttribute?.('data-name'),
|
|
1158
1182
|
node.getAttribute?.('aria-label'),
|
|
1159
1183
|
node.getAttribute?.('title'),
|
|
1160
|
-
node
|
|
1184
|
+
readFileTreeNodeText(node)
|
|
1161
1185
|
];
|
|
1162
1186
|
for (const value of candidates) {
|
|
1163
1187
|
const path = normalizeProjectPathCandidate(value);
|
|
@@ -1220,21 +1244,12 @@
|
|
|
1220
1244
|
}
|
|
1221
1245
|
|
|
1222
1246
|
function normalizeProjectPathCandidate(value) {
|
|
1223
|
-
const text =
|
|
1247
|
+
const text = String(value || '').trim();
|
|
1224
1248
|
if (!text || hasMultipleTextPathExtensions(text)) {
|
|
1225
1249
|
return '';
|
|
1226
1250
|
}
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
text
|
|
1230
|
-
];
|
|
1231
|
-
for (const candidate of candidates) {
|
|
1232
|
-
const normalized = normalizeSafeProjectPath(candidate);
|
|
1233
|
-
if (normalized && window.CodexOverleafProjectFiles.isTextProjectPath(normalized)) {
|
|
1234
|
-
return normalized;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
return '';
|
|
1251
|
+
const normalized = normalizeSafeProjectPath(text);
|
|
1252
|
+
return normalized && window.CodexOverleafProjectFiles.isTextProjectPath(normalized) ? normalized : '';
|
|
1238
1253
|
}
|
|
1239
1254
|
|
|
1240
1255
|
function normalizeDomText(value) {
|
|
@@ -1251,19 +1266,11 @@
|
|
|
1251
1266
|
return false;
|
|
1252
1267
|
}
|
|
1253
1268
|
|
|
1254
|
-
function
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
.replace(/(?:more_vert\s*Menu|more_vert|Menu)$/i, '')
|
|
1260
|
-
.trim();
|
|
1261
|
-
if (next === text) {
|
|
1262
|
-
break;
|
|
1263
|
-
}
|
|
1264
|
-
text = next;
|
|
1265
|
-
}
|
|
1266
|
-
return text;
|
|
1269
|
+
function readFileTreeNodeText(node) {
|
|
1270
|
+
// Icon ligatures can be valid filename prefixes; use the actual label.
|
|
1271
|
+
const names = Array.from(node.querySelectorAll?.('.item-name') || []);
|
|
1272
|
+
if (names.length > 1) return '';
|
|
1273
|
+
return names.length === 1 ? names[0].textContent || '' : node.textContent || '';
|
|
1267
1274
|
}
|
|
1268
1275
|
|
|
1269
1276
|
function inferPathFromTreeAncestors(node, fileName) {
|
|
@@ -1355,7 +1362,10 @@
|
|
|
1355
1362
|
const activeFileMatches = !filePath || getActiveFilePath() === filePath;
|
|
1356
1363
|
const text = readActiveEditorText();
|
|
1357
1364
|
lastText = text;
|
|
1358
|
-
const
|
|
1365
|
+
const knownEmptyEditor = options.allowEmpty === true && typeof text === 'string' && /^(?:\r?\n)*$/.test(text)
|
|
1366
|
+
&& Boolean(getActiveEditorIdentity());
|
|
1367
|
+
const textReady = options.requireEmpty === true ? knownEmptyEditor
|
|
1368
|
+
: knownEmptyEditor || window.CodexOverleafProjectFiles.isUsableProjectFileContent(text);
|
|
1359
1369
|
const signatureChanged = !options.notSignature || contentSignature(text) !== options.notSignature;
|
|
1360
1370
|
if (activeFileMatches && textReady && signatureChanged) {
|
|
1361
1371
|
return {
|
|
@@ -1552,6 +1562,7 @@
|
|
|
1552
1562
|
collectInternalRootKeys,
|
|
1553
1563
|
collectProjectTextPaths,
|
|
1554
1564
|
contentSignature,
|
|
1565
|
+
invalidateDomProjectPathCache,
|
|
1555
1566
|
createFile,
|
|
1556
1567
|
deleteFile,
|
|
1557
1568
|
fileTreeMethodNames,
|