codex-overleaf-link 1.8.6 → 1.9.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 +45 -21
- package/bin/codex-overleaf-link.mjs +58 -0
- package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
- package/extension/assets/icons/codex-overleaf-icon.png +0 -0
- package/extension/assets/icons/icon128.png +0 -0
- package/extension/assets/icons/icon16.png +0 -0
- package/extension/assets/icons/icon32.png +0 -0
- package/extension/assets/icons/icon48.png +0 -0
- package/extension/bootstrap/background.js +404 -0
- package/extension/bootstrap/manifest.template.json +51 -0
- package/extension/bootstrap/popup.css +26 -0
- package/extension/bootstrap/popup.html +38 -0
- package/extension/bootstrap/popup.js +44 -0
- package/extension/bootstrap/runtimeContext.js +2 -0
- package/extension/runtime-manifest.json +60 -0
- package/extension/src/background.js +532 -0
- package/extension/src/content/applyResultFormatters.js +387 -0
- package/extension/src/content/composerAttachments.js +522 -0
- package/extension/src/content/composerPanel.js +254 -0
- package/extension/src/content/contentRuntime.js +8895 -0
- package/extension/src/content/contextTray.js +686 -0
- package/extension/src/content/diagnosticsController.js +659 -0
- package/extension/src/content/diagnosticsPanel.js +342 -0
- package/extension/src/content/diffReviewPanel.js +797 -0
- package/extension/src/content/localSkillsPanel.js +280 -0
- package/extension/src/content/markdownText.js +606 -0
- package/extension/src/content/mirrorHealth.js +92 -0
- package/extension/src/content/modelPicker.js +489 -0
- package/extension/src/content/nativeChannel.js +62 -0
- package/extension/src/content/otWarmMirror.js +964 -0
- package/extension/src/content/otWarmMirrorController.js +174 -0
- package/extension/src/content/panelMaintenance.js +367 -0
- package/extension/src/content/panelRenderer.js +253 -0
- package/extension/src/content/recentProjects.js +1063 -0
- package/extension/src/content/reviewHunks.js +247 -0
- package/extension/src/content/runController.js +242 -0
- package/extension/src/content/runTimelineView.js +1126 -0
- package/extension/src/content/sessionManager.js +487 -0
- package/extension/src/content/sessionPanel.js +263 -0
- package/extension/src/content/settingsPanel.js +527 -0
- package/extension/src/content/themeController.js +91 -0
- package/extension/src/content/updateIdle.js +67 -0
- package/extension/src/content/writebackController.js +300 -0
- package/extension/src/content/writebackOrchestrator.js +850 -0
- package/extension/src/contentScript.js +8 -0
- package/extension/src/page/compileBridge.js +460 -0
- package/extension/src/page/overleafCapabilities.js +100 -0
- package/extension/src/page/overleafEditor.js +282 -0
- package/extension/src/page/overleafProjectSnapshot.js +234 -0
- package/extension/src/page/overleafRealtimeObserver.js +491 -0
- package/extension/src/page/pageBridgeCapability.js +71 -0
- package/extension/src/page/snapshotRouter.js +948 -0
- package/extension/src/page/trackedChangesLifecycle.js +1506 -0
- package/extension/src/page/treeOperations.js +1584 -0
- package/extension/src/page/writeGuard.js +142 -0
- package/extension/src/page/writebackRouter.js +1934 -0
- package/extension/src/pageBridge.js +2224 -0
- package/extension/src/popup.js +247 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/styles/panel.css +4048 -0
- package/native-host/src/index.js +9 -2
- package/native-host/src/managedInstall.js +393 -0
- package/native-host/src/managedLauncherRuntime.js +71 -0
- package/native-host/src/nativeHostPlatform.js +16 -0
- package/native-host/src/runtimeInstaller.js +6 -1
- package/native-host/src/taskRunnerRuntime.js +8 -0
- package/native-host/src/updateArchive.js +253 -0
- package/native-host/src/updateManager.js +548 -0
- package/native-host/src/updateTrust.js +156 -0
- package/package.json +11 -2
- package/scripts/install-managed.mjs +64 -0
- package/scripts/uninstall-managed.mjs +48 -0
|
@@ -0,0 +1,2224 @@
|
|
|
1
|
+
(function initCodexOverleafPageBridge() {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const PAGE_BRIDGE_INSTALL_VERSION = window.CodexOverleafCompatibility?.BUILD_TARGET_VERSION || 'dev';
|
|
5
|
+
const PAGE_BRIDGE_INSTALL_REVISION = '2026-05-21-editor-readiness-v10';
|
|
6
|
+
if (window.__codexOverleafPageBridgeInstalledVersion === PAGE_BRIDGE_INSTALL_VERSION
|
|
7
|
+
&& window.__codexOverleafPageBridgeInstalledRevision === PAGE_BRIDGE_INSTALL_REVISION) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (typeof window.__codexOverleafPageBridgeMessageHandler === 'function') {
|
|
11
|
+
window.removeEventListener('message', window.__codexOverleafPageBridgeMessageHandler);
|
|
12
|
+
}
|
|
13
|
+
window.__codexOverleafPageBridgeInstalled = true;
|
|
14
|
+
window.__codexOverleafPageBridgeInstalledVersion = PAGE_BRIDGE_INSTALL_VERSION;
|
|
15
|
+
window.__codexOverleafPageBridgeInstalledRevision = PAGE_BRIDGE_INSTALL_REVISION;
|
|
16
|
+
|
|
17
|
+
const PAGE_BRIDGE_CAPABILITY_METHOD = 'initializeCapability';
|
|
18
|
+
const pageBridgeCapabilityGuard = window.CodexOverleafPageBridgeCapability.create();
|
|
19
|
+
let treeOperations = null;
|
|
20
|
+
let snapshotRouter = null;
|
|
21
|
+
let projectSnapshotBridge = null;
|
|
22
|
+
let writebackRouter = null;
|
|
23
|
+
let writeGuard = null;
|
|
24
|
+
const compileBridge = window.CodexOverleafCompileBridge.create({
|
|
25
|
+
document,
|
|
26
|
+
getActiveFilePath,
|
|
27
|
+
waitForSaveState,
|
|
28
|
+
window
|
|
29
|
+
});
|
|
30
|
+
compileBridge.install();
|
|
31
|
+
const editorAdapter = window.CodexOverleafEditorAdapter.create({
|
|
32
|
+
document,
|
|
33
|
+
Event,
|
|
34
|
+
findEditorContentNode,
|
|
35
|
+
findEditorTextArea,
|
|
36
|
+
getCodeMirrorDocLength,
|
|
37
|
+
getCodeMirrorDocText,
|
|
38
|
+
getCodeMirrorEditorView,
|
|
39
|
+
getDeepActiveElement,
|
|
40
|
+
InputEvent,
|
|
41
|
+
isInsideCodexPanel,
|
|
42
|
+
normalizeTextPatches
|
|
43
|
+
});
|
|
44
|
+
treeOperations = requirePageModule('CodexOverleafTreeOperations').create({
|
|
45
|
+
activeEditorIdentityChanged,
|
|
46
|
+
document,
|
|
47
|
+
getActiveFilePathFromEditorStore,
|
|
48
|
+
getActiveEditorIdentity,
|
|
49
|
+
normalizePath: normalizeSafeProjectPath,
|
|
50
|
+
readActiveEditorText,
|
|
51
|
+
window
|
|
52
|
+
});
|
|
53
|
+
snapshotRouter = requirePageModule('CodexOverleafSnapshotRouter').create({
|
|
54
|
+
normalizePath: normalizeSafeProjectPath,
|
|
55
|
+
readActiveEditorText,
|
|
56
|
+
treeOperations,
|
|
57
|
+
window
|
|
58
|
+
});
|
|
59
|
+
writeGuard = requirePageModule('CodexOverleafWriteGuard').create({
|
|
60
|
+
document,
|
|
61
|
+
treeOperations,
|
|
62
|
+
window
|
|
63
|
+
});
|
|
64
|
+
projectSnapshotBridge = window.CodexOverleafProjectSnapshot.create({
|
|
65
|
+
buildProjectFileList: snapshotRouter.buildProjectFileList,
|
|
66
|
+
buildProjectSnapshot: snapshotRouter.buildProjectSnapshot,
|
|
67
|
+
getProjectId,
|
|
68
|
+
normalizePath: normalizeSafeProjectPath,
|
|
69
|
+
window
|
|
70
|
+
});
|
|
71
|
+
writebackRouter = requirePageModule('CodexOverleafWritebackRouter').create({
|
|
72
|
+
activeEditorIdentityChanged,
|
|
73
|
+
clickNode,
|
|
74
|
+
collectElements,
|
|
75
|
+
compact,
|
|
76
|
+
compileBridge,
|
|
77
|
+
delay,
|
|
78
|
+
diagnosticsRevision: PAGE_BRIDGE_INSTALL_REVISION,
|
|
79
|
+
ensureEditing,
|
|
80
|
+
ensureReviewing,
|
|
81
|
+
// Cross-world cancel: the router's per-op loop polls this each iteration
|
|
82
|
+
// and aborts remaining ops when the value bumps. content-side cancel
|
|
83
|
+
// invokes pageBridge.cancelActiveWrite which calls bumpWriteCancellationSequence.
|
|
84
|
+
readWriteCancellationSequence,
|
|
85
|
+
getActiveEditorIdentity,
|
|
86
|
+
getReviewingState,
|
|
87
|
+
invalidProjectPathResult,
|
|
88
|
+
isEditingConfirmedForNoTraceUndo,
|
|
89
|
+
isReviewingConfirmedForWrite,
|
|
90
|
+
isInsideCodexPanel,
|
|
91
|
+
normalizeReviewingSignalText,
|
|
92
|
+
normalizeSafeProjectPath,
|
|
93
|
+
normalizeTextPatches,
|
|
94
|
+
projectSnapshotBridge,
|
|
95
|
+
readActiveEditorText,
|
|
96
|
+
readNodeSignalText,
|
|
97
|
+
replaceActiveEditorPatches,
|
|
98
|
+
replaceActiveEditorText,
|
|
99
|
+
setReviewingEnabled,
|
|
100
|
+
summarizeReviewingToggleResult,
|
|
101
|
+
treeOperations,
|
|
102
|
+
uniqueNodes,
|
|
103
|
+
waitForSaveState,
|
|
104
|
+
window
|
|
105
|
+
});
|
|
106
|
+
const otObserver = createOtObserver();
|
|
107
|
+
|
|
108
|
+
function createOtObserver() {
|
|
109
|
+
if (!window.CodexOverleafRealtimeObserver || typeof window.CodexOverleafRealtimeObserver.create !== 'function') {
|
|
110
|
+
return createUnavailableOtObserver();
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
return window.CodexOverleafRealtimeObserver.create({
|
|
114
|
+
document,
|
|
115
|
+
window,
|
|
116
|
+
otText: window.CodexOverleafOtText,
|
|
117
|
+
getActiveFilePath,
|
|
118
|
+
readActiveEditorText,
|
|
119
|
+
getCodeMirrorEditorView,
|
|
120
|
+
collectDocRecords,
|
|
121
|
+
now: () => Date.now()
|
|
122
|
+
});
|
|
123
|
+
} catch (_error) {
|
|
124
|
+
return createUnavailableOtObserver('ot_observer_create_failed');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pageBridgeMessageHandler = async event => {
|
|
129
|
+
if (event.source !== window
|
|
130
|
+
|| event.origin !== window.location.origin
|
|
131
|
+
|| event.data?.source !== 'codex-overleaf/content') {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { id, method, params, capability } = event.data;
|
|
136
|
+
let result;
|
|
137
|
+
try {
|
|
138
|
+
if (method === PAGE_BRIDGE_CAPABILITY_METHOD) {
|
|
139
|
+
result = pageBridgeCapabilityGuard.initializePageBridgeCapability(capability);
|
|
140
|
+
} else if (!pageBridgeCapabilityGuard.hasValidPageBridgeCapability(capability)) {
|
|
141
|
+
result = window.CodexOverleafPageBridgeCapability.buildUnauthorizedBridgeResult();
|
|
142
|
+
} else {
|
|
143
|
+
result = await dispatch(method, params || {});
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
result = {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: error.message
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
window.postMessage({
|
|
153
|
+
source: 'codex-overleaf/page',
|
|
154
|
+
id,
|
|
155
|
+
pageBridgeVersion: PAGE_BRIDGE_INSTALL_VERSION,
|
|
156
|
+
pageBridgeRevision: PAGE_BRIDGE_INSTALL_REVISION,
|
|
157
|
+
result
|
|
158
|
+
}, window.location.origin);
|
|
159
|
+
};
|
|
160
|
+
window.__codexOverleafPageBridgeMessageHandler = pageBridgeMessageHandler;
|
|
161
|
+
window.addEventListener('message', pageBridgeMessageHandler);
|
|
162
|
+
|
|
163
|
+
// Cross-world cancel signal. The native-host codex.cancel aborts the Codex
|
|
164
|
+
// CLI but it can't reach into the Overleaf page world. If the user clicks
|
|
165
|
+
// cancel during the writeback phase (Codex already returned; the router
|
|
166
|
+
// is mid-flight through applyOperationsCore's per-op loop), nothing
|
|
167
|
+
// currently interrupts the dispatch. content-side cancelActiveRun now
|
|
168
|
+
// also calls 'cancelActiveWrite' which bumps this counter; applyOperationsCore
|
|
169
|
+
// captures the value at start and re-reads it between ops — a bump means
|
|
170
|
+
// the user cancelled and the remaining ops should be skipped with a
|
|
171
|
+
// codex_cancelled result so the run card settles immediately.
|
|
172
|
+
let writeCancellationSequence = 0;
|
|
173
|
+
function bumpWriteCancellationSequence() {
|
|
174
|
+
writeCancellationSequence += 1;
|
|
175
|
+
return writeCancellationSequence;
|
|
176
|
+
}
|
|
177
|
+
function readWriteCancellationSequence() {
|
|
178
|
+
return writeCancellationSequence;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function dispatch(method, params) {
|
|
182
|
+
if (method === 'probe') {
|
|
183
|
+
return probe(params);
|
|
184
|
+
}
|
|
185
|
+
if (method === 'cancelActiveWrite') {
|
|
186
|
+
const sequence = bumpWriteCancellationSequence();
|
|
187
|
+
return { ok: true, cancelled: true, sequence };
|
|
188
|
+
}
|
|
189
|
+
if (method === 'getProjectSnapshot') {
|
|
190
|
+
return projectSnapshotBridge.getProjectSnapshot(withSnapshotCacheIdentity(params));
|
|
191
|
+
}
|
|
192
|
+
if (method === 'getProjectFileList') {
|
|
193
|
+
return projectSnapshotBridge.getProjectFileList(params);
|
|
194
|
+
}
|
|
195
|
+
if (method === 'invalidateProjectSnapshot') {
|
|
196
|
+
projectSnapshotBridge.invalidateProjectSnapshot();
|
|
197
|
+
return { ok: true };
|
|
198
|
+
}
|
|
199
|
+
if (method === 'createCheckpoint') {
|
|
200
|
+
return createCheckpoint(params.label);
|
|
201
|
+
}
|
|
202
|
+
if (method === 'ensureReviewing') {
|
|
203
|
+
// Gate the Reviewing-mode toggle with the same writeGuard the per-write
|
|
204
|
+
// path uses. Without this, a mid-flight SPA navigation could leave the
|
|
205
|
+
// pre-flight toggle flipping Track Changes in a different Overleaf
|
|
206
|
+
// project than the run was bound to. params.runProjectId is set by
|
|
207
|
+
// the content-side caller (preflightWriteSafety / ensureReviewingBeforeWrite);
|
|
208
|
+
// when absent (defensive), the guard short-circuits via its own
|
|
209
|
+
// missing-runProjectId branch.
|
|
210
|
+
if (typeof params?.runProjectId === 'string' && params.runProjectId) {
|
|
211
|
+
const blocked = await writeGuard.runWriteGuard(params);
|
|
212
|
+
if (blocked) return blocked;
|
|
213
|
+
}
|
|
214
|
+
return ensureReviewing(params);
|
|
215
|
+
}
|
|
216
|
+
if (method === 'ensureEditing') {
|
|
217
|
+
if (typeof params?.runProjectId === 'string' && params.runProjectId) {
|
|
218
|
+
const blocked = await writeGuard.runWriteGuard(params);
|
|
219
|
+
if (blocked) return blocked;
|
|
220
|
+
}
|
|
221
|
+
return ensureEditing(params);
|
|
222
|
+
}
|
|
223
|
+
if (method === 'applyOperations') {
|
|
224
|
+
// Welcome-panel + write-guard:
|
|
225
|
+
// First gate: runProjectId vs page-side editorProjectId. Runs before
|
|
226
|
+
// any reviewing / save-state / open-file readiness check so a
|
|
227
|
+
// mid-run navigation cannot write into a different project. runWriteGuard
|
|
228
|
+
// is async because it retries the page-side editorProjectId reader with
|
|
229
|
+
// backoff to ride out Overleaf's hydration window.
|
|
230
|
+
const blocked = await writeGuard.runWriteGuard(params);
|
|
231
|
+
if (blocked) return blocked;
|
|
232
|
+
return applyOperations(params.operations || [], {
|
|
233
|
+
baseFiles: params.baseFiles || null,
|
|
234
|
+
reviewingPolicy: params.reviewingPolicy || '',
|
|
235
|
+
requireReviewing: params.requireReviewing === true,
|
|
236
|
+
requireEditing: params.requireEditing === true,
|
|
237
|
+
// Propagate runProjectId so the writeback router's defense-in-depth
|
|
238
|
+
// missing-runProjectId check sees it on this in-bridge dispatch path.
|
|
239
|
+
runProjectId: typeof params.runProjectId === 'string' ? params.runProjectId : ''
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (method === 'jumpToPosition') {
|
|
243
|
+
return jumpToPosition(params);
|
|
244
|
+
}
|
|
245
|
+
if (method === 'rejectTrackedChanges') {
|
|
246
|
+
const blocked = await writeGuard.runWriteGuard(params);
|
|
247
|
+
if (blocked) return blocked;
|
|
248
|
+
return rejectTrackedChanges(params);
|
|
249
|
+
}
|
|
250
|
+
if (method === 'acceptTrackedChanges') {
|
|
251
|
+
const blocked = await writeGuard.runWriteGuard(params);
|
|
252
|
+
if (blocked) return blocked;
|
|
253
|
+
return acceptTrackedChanges(params);
|
|
254
|
+
}
|
|
255
|
+
if (method === 'triggerCompile') {
|
|
256
|
+
return compileBridge.triggerCompile(params);
|
|
257
|
+
}
|
|
258
|
+
if (method === 'getCompileLog') {
|
|
259
|
+
return compileBridge.getCompileLog(params);
|
|
260
|
+
}
|
|
261
|
+
if (method === 'getCompileState') {
|
|
262
|
+
return compileBridge.getCompileState();
|
|
263
|
+
}
|
|
264
|
+
if (method === 'waitForSaveState') {
|
|
265
|
+
return waitForSaveState(params);
|
|
266
|
+
}
|
|
267
|
+
if (method === 'startOtObserver') {
|
|
268
|
+
return otObserver.start(params);
|
|
269
|
+
}
|
|
270
|
+
if (method === 'stopOtObserver') {
|
|
271
|
+
return otObserver.stop();
|
|
272
|
+
}
|
|
273
|
+
if (method === 'getOtStatus') {
|
|
274
|
+
return otObserver.getStatus();
|
|
275
|
+
}
|
|
276
|
+
if (method === 'drainOtEvents') {
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
events: otObserver.drainEvents().map(sanitizeOtEventForContent)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
error: `Unknown page bridge method: ${method}`
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function sanitizeOtEventForContent(event = {}) {
|
|
289
|
+
const sanitized = {};
|
|
290
|
+
copyOtEventField(sanitized, event, 'path');
|
|
291
|
+
copyOtEventField(sanitized, event, 'baseHash');
|
|
292
|
+
copyOtEventField(sanitized, event, 'nextHash');
|
|
293
|
+
copyOtEventField(sanitized, event, 'nextContent');
|
|
294
|
+
copyOtEventField(sanitized, event, 'observedAt');
|
|
295
|
+
copyOtEventField(sanitized, event, 'observedVersion');
|
|
296
|
+
const source = sanitizeOtEventSource(readOtEventField(event, 'source'));
|
|
297
|
+
if (source !== undefined) {
|
|
298
|
+
sanitized.source = source;
|
|
299
|
+
}
|
|
300
|
+
return sanitized;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function copyOtEventField(target, event, field) {
|
|
304
|
+
const value = readOtEventField(event, field);
|
|
305
|
+
if (value !== undefined) {
|
|
306
|
+
target[field] = value;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function readOtEventField(event, field) {
|
|
311
|
+
try {
|
|
312
|
+
return event && typeof event === 'object' ? event[field] : undefined;
|
|
313
|
+
} catch (_error) {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function sanitizeOtEventSource(source) {
|
|
319
|
+
if (source === null || typeof source === 'string' || typeof source === 'number' || typeof source === 'boolean') {
|
|
320
|
+
return source;
|
|
321
|
+
}
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function createUnavailableOtObserver(reason = 'missing_realtime_observer') {
|
|
326
|
+
const status = {
|
|
327
|
+
status: 'unavailable',
|
|
328
|
+
state: 'unavailable',
|
|
329
|
+
running: false,
|
|
330
|
+
strategy: 'active-editor',
|
|
331
|
+
activePath: '',
|
|
332
|
+
queuedEventCount: 0,
|
|
333
|
+
lastEventAt: null,
|
|
334
|
+
lastErrorCode: reason,
|
|
335
|
+
reason,
|
|
336
|
+
channelCandidates: []
|
|
337
|
+
};
|
|
338
|
+
return {
|
|
339
|
+
start() {
|
|
340
|
+
return { ...status };
|
|
341
|
+
},
|
|
342
|
+
stop() {
|
|
343
|
+
return {
|
|
344
|
+
...status,
|
|
345
|
+
status: 'off',
|
|
346
|
+
state: 'off',
|
|
347
|
+
lastErrorCode: '',
|
|
348
|
+
reason: ''
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
getStatus() {
|
|
352
|
+
return { ...status };
|
|
353
|
+
},
|
|
354
|
+
drainEvents() {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeSafeProjectPath(value) {
|
|
361
|
+
if (typeof window.CodexOverleafProjectFiles?.normalizeSafeProjectPath === 'function') {
|
|
362
|
+
return window.CodexOverleafProjectFiles.normalizeSafeProjectPath(value);
|
|
363
|
+
}
|
|
364
|
+
return window.CodexOverleafProjectFiles.normalizePath(value);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function invalidProjectPathResult(label = 'path') {
|
|
368
|
+
return {
|
|
369
|
+
ok: false,
|
|
370
|
+
code: 'invalid_project_path',
|
|
371
|
+
reason: `Invalid ${label}.`
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Inline mirror of the page-side subset of the FailureReason §9 catalog used by
|
|
376
|
+
// T4 emit sites in this module (jumpToPosition, ensureEditing, ensureReviewing,
|
|
377
|
+
// getReviewingState helpers). Kept in sync with PAGE_FAILURE_CATALOG in
|
|
378
|
+
// writebackRouter.js and FAILURE_CODE_CATALOG in shared/failureReasons.js.
|
|
379
|
+
const PAGE_BRIDGE_FAILURE_CATALOG = {
|
|
380
|
+
target_file_not_found: {
|
|
381
|
+
stage: 'navigation', severity: 'blocked', defaultRetryable: true,
|
|
382
|
+
fallbackUserMessage: 'Codex could not find the target file in this Overleaf project.',
|
|
383
|
+
fallbackNextAction: 'Check the file name/path in Overleaf and retry.'
|
|
384
|
+
},
|
|
385
|
+
target_file_open_failed: {
|
|
386
|
+
stage: 'navigation', severity: 'blocked', defaultRetryable: true,
|
|
387
|
+
fallbackUserMessage: 'Codex could not open the target file in Overleaf.',
|
|
388
|
+
fallbackNextAction: 'Expand the folder or manually open the file, then retry.'
|
|
389
|
+
},
|
|
390
|
+
target_editor_not_ready: {
|
|
391
|
+
stage: 'navigation', severity: 'blocked', defaultRetryable: true,
|
|
392
|
+
fallbackUserMessage: 'Target file is active but the editor was not ready before timeout.',
|
|
393
|
+
fallbackNextAction: 'Wait for Overleaf to finish loading, then retry.'
|
|
394
|
+
},
|
|
395
|
+
reviewing_state_unknown: {
|
|
396
|
+
stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
|
|
397
|
+
fallbackUserMessage: 'Codex could not determine whether Reviewing/Track Changes is enabled.',
|
|
398
|
+
fallbackNextAction: 'Check Overleaf mode manually before retrying.'
|
|
399
|
+
},
|
|
400
|
+
reviewing_enable_failed: {
|
|
401
|
+
stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
|
|
402
|
+
fallbackUserMessage: 'Codex could not enable Reviewing when it needed to.',
|
|
403
|
+
fallbackNextAction: 'Enable Reviewing manually or retry.'
|
|
404
|
+
},
|
|
405
|
+
reviewing_disable_failed: {
|
|
406
|
+
stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
|
|
407
|
+
fallbackUserMessage: 'Codex could not switch to Editing / no-trace mode.',
|
|
408
|
+
fallbackNextAction: 'Switch to Editing manually, wait briefly, then retry.'
|
|
409
|
+
},
|
|
410
|
+
editing_not_confirmed: {
|
|
411
|
+
stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
|
|
412
|
+
fallbackUserMessage: 'Editing mode could not be proven stable.',
|
|
413
|
+
fallbackNextAction: 'Do not write; check the mode selector and retry.'
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
function buildPageBridgeFailure(code, overrides) {
|
|
418
|
+
const entry = PAGE_BRIDGE_FAILURE_CATALOG[code];
|
|
419
|
+
if (!entry) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
const merged = overrides || {};
|
|
423
|
+
const failure = {
|
|
424
|
+
code,
|
|
425
|
+
stage: entry.stage,
|
|
426
|
+
severity: entry.severity,
|
|
427
|
+
userMessage: merged.userMessage || entry.fallbackUserMessage,
|
|
428
|
+
retryable: merged.retryable === undefined ? entry.defaultRetryable : merged.retryable === true,
|
|
429
|
+
nextAction: merged.nextAction || entry.fallbackNextAction
|
|
430
|
+
};
|
|
431
|
+
if (merged.file !== undefined) failure.file = merged.file;
|
|
432
|
+
if (merged.activeFile !== undefined) failure.activeFile = merged.activeFile;
|
|
433
|
+
if (merged.operationType !== undefined) failure.operationType = merged.operationType;
|
|
434
|
+
if (merged.changedDocument !== undefined) failure.changedDocument = merged.changedDocument === true;
|
|
435
|
+
if (merged.terminalState !== undefined) failure.terminalState = merged.terminalState;
|
|
436
|
+
if (merged.technicalMessage !== undefined) failure.technicalMessage = merged.technicalMessage;
|
|
437
|
+
if (merged.evidence !== undefined) failure.evidence = merged.evidence;
|
|
438
|
+
return failure;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function withSnapshotCacheIdentity(params = {}) {
|
|
442
|
+
if (params.restrictToRequestedPathsOnly !== true) {
|
|
443
|
+
return params;
|
|
444
|
+
}
|
|
445
|
+
const activePath = getActiveFilePath();
|
|
446
|
+
return {
|
|
447
|
+
...params,
|
|
448
|
+
requestedPathsCacheKey: getRequestedSnapshotPaths(params, activePath).join(',')
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function probe(params = {}) {
|
|
453
|
+
const reviewingSignals = collectReviewingSignals(params);
|
|
454
|
+
const editor = detectEditor();
|
|
455
|
+
const docRecords = collectDocRecords();
|
|
456
|
+
return {
|
|
457
|
+
ok: true,
|
|
458
|
+
projectId: getProjectId(),
|
|
459
|
+
reviewing: window.CodexOverleafReviewing.detectReviewingFromSignals(reviewingSignals),
|
|
460
|
+
reviewingDiagnostics: reviewingSignals.diagnostics,
|
|
461
|
+
editor,
|
|
462
|
+
capabilities: collectPageCapabilities(editor),
|
|
463
|
+
editorDiagnostics: buildEditorDiagnostics(editor),
|
|
464
|
+
projectDiagnostics: {
|
|
465
|
+
internalRootKeys: collectInternalRootKeys(),
|
|
466
|
+
docRecordCount: docRecords.length,
|
|
467
|
+
docRecords: docRecords.slice(0, 8)
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function collectPageCapabilities(editor = detectEditor()) {
|
|
473
|
+
return window.CodexOverleafPageCapabilities.collectPageCapabilities({
|
|
474
|
+
editor,
|
|
475
|
+
compileState: compileBridge.state,
|
|
476
|
+
detectEditor,
|
|
477
|
+
fileTreeMethodNames,
|
|
478
|
+
findEditorContentNode,
|
|
479
|
+
findEditorTextArea,
|
|
480
|
+
findFileTreeManager,
|
|
481
|
+
findHistoryObject,
|
|
482
|
+
findReviewingActivationControl,
|
|
483
|
+
getCodeMirrorEditorView,
|
|
484
|
+
getDeepActiveElement,
|
|
485
|
+
isInsideCodexPanel,
|
|
486
|
+
window
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function ensureReviewing(params = {}) {
|
|
491
|
+
const initial = getReviewingState(params);
|
|
492
|
+
if (isReviewingConfirmedForWrite(initial)) {
|
|
493
|
+
return {
|
|
494
|
+
ok: true,
|
|
495
|
+
activated: false,
|
|
496
|
+
reviewing: initial.reviewing
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const switched = await toggleReviewingMode(true, params);
|
|
501
|
+
if (switched.ok) {
|
|
502
|
+
return {
|
|
503
|
+
ok: true,
|
|
504
|
+
activated: true,
|
|
505
|
+
reviewing: switched.reviewing
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// §9.4: toggle attempted → reviewing_enable_failed; no control found →
|
|
510
|
+
// reviewing_state_unknown (per §14 legacy mapping for reviewing_not_enabled).
|
|
511
|
+
const reviewingFailureCode = switched.attempted ? 'reviewing_enable_failed' : 'reviewing_state_unknown';
|
|
512
|
+
return {
|
|
513
|
+
ok: false,
|
|
514
|
+
code: 'reviewing_not_enabled',
|
|
515
|
+
reason: switched.attempted
|
|
516
|
+
? 'Codex clicked the Overleaf mode control, but Overleaf still did not report Reviewing/Track Changes as enabled.'
|
|
517
|
+
: 'Overleaf Reviewing/Track Changes is not enabled, and Codex could not find a Reviewing control to activate.',
|
|
518
|
+
reviewing: switched.reviewing || initial.reviewing,
|
|
519
|
+
evidence: { toggleAttempted: switched.attempted === true },
|
|
520
|
+
failure: buildPageBridgeFailure(reviewingFailureCode, {
|
|
521
|
+
operationType: 'ensureReviewing',
|
|
522
|
+
changedDocument: false,
|
|
523
|
+
userMessage: switched.attempted
|
|
524
|
+
? 'Codex clicked the Overleaf mode control, but Overleaf still did not report Reviewing as enabled.'
|
|
525
|
+
: 'Codex could not determine whether Reviewing/Track Changes is enabled (no control found).',
|
|
526
|
+
evidence: {
|
|
527
|
+
originalCode: 'reviewing_not_enabled',
|
|
528
|
+
toggleAttempted: switched.attempted === true,
|
|
529
|
+
writeStarted: false
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function ensureEditing(params = {}) {
|
|
536
|
+
const initial = getReviewingState(params);
|
|
537
|
+
if (isEditingConfirmedForNoTraceUndo(initial)) {
|
|
538
|
+
return {
|
|
539
|
+
ok: true,
|
|
540
|
+
activated: false,
|
|
541
|
+
changed: false,
|
|
542
|
+
enabled: false,
|
|
543
|
+
reviewing: initial.reviewing
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const switched = await toggleReviewingMode(false, params);
|
|
548
|
+
if (switched.ok) {
|
|
549
|
+
return {
|
|
550
|
+
ok: true,
|
|
551
|
+
activated: true,
|
|
552
|
+
changed: true,
|
|
553
|
+
enabled: false,
|
|
554
|
+
reviewing: switched.reviewing
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
ok: false,
|
|
560
|
+
code: 'editing_not_confirmed',
|
|
561
|
+
reason: switched.attempted
|
|
562
|
+
? 'Codex clicked the Overleaf mode control, but Overleaf still did not clearly report Editing mode.'
|
|
563
|
+
: 'Overleaf Editing mode was not clearly confirmed, and Codex could not find a mode control to switch to Editing.',
|
|
564
|
+
reviewing: switched.reviewing || initial.reviewing,
|
|
565
|
+
evidence: { toggleAttempted: switched.attempted === true },
|
|
566
|
+
// §9.4 editing_not_confirmed: this fires BEFORE any write (preflight),
|
|
567
|
+
// so changedDocument:false. Distinguish from the accept-replay variant
|
|
568
|
+
// (writebackRouter) which fires after a rollback and is changedDocument:true.
|
|
569
|
+
failure: buildPageBridgeFailure('editing_not_confirmed', {
|
|
570
|
+
operationType: 'ensureEditing',
|
|
571
|
+
changedDocument: false,
|
|
572
|
+
userMessage: switched.attempted
|
|
573
|
+
? 'Codex clicked the Overleaf mode control, but Overleaf still did not confirm Editing mode.'
|
|
574
|
+
: 'Codex could not find a mode control to switch to Editing.',
|
|
575
|
+
evidence: {
|
|
576
|
+
originalCode: 'editing_not_confirmed',
|
|
577
|
+
toggleAttempted: switched.attempted === true,
|
|
578
|
+
writeStarted: false
|
|
579
|
+
}
|
|
580
|
+
})
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function jumpToPosition(params = {}) {
|
|
585
|
+
const filePath = normalizeSafeProjectPath(params.path);
|
|
586
|
+
if (!filePath) {
|
|
587
|
+
return {
|
|
588
|
+
ok: false,
|
|
589
|
+
code: 'invalid_path',
|
|
590
|
+
reason: 'jumpToPosition requires a non-empty file path'
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
if (!projectPathExists(filePath)) {
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
code: 'path_not_found',
|
|
597
|
+
reason: `Could not find ${filePath} in the Overleaf project`,
|
|
598
|
+
path: filePath,
|
|
599
|
+
failure: buildPageBridgeFailure('target_file_not_found', {
|
|
600
|
+
file: filePath,
|
|
601
|
+
operationType: 'jump',
|
|
602
|
+
changedDocument: false,
|
|
603
|
+
userMessage: `Codex could not find ${filePath} in this Overleaf project.`,
|
|
604
|
+
evidence: { originalCode: 'path_not_found', writeStarted: false }
|
|
605
|
+
})
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const navigating = getActiveFilePath() !== filePath;
|
|
610
|
+
const previousEditorIdentity = navigating ? getActiveEditorIdentity() : null;
|
|
611
|
+
|
|
612
|
+
if (navigating) {
|
|
613
|
+
const opened = await openFileByPath(filePath);
|
|
614
|
+
if (!opened.ok) {
|
|
615
|
+
return {
|
|
616
|
+
ok: false,
|
|
617
|
+
code: 'file_open_failed',
|
|
618
|
+
reason: opened.reason || `Could not open ${filePath}`,
|
|
619
|
+
path: filePath,
|
|
620
|
+
failure: buildPageBridgeFailure('target_file_open_failed', {
|
|
621
|
+
file: filePath,
|
|
622
|
+
operationType: 'jump',
|
|
623
|
+
changedDocument: false,
|
|
624
|
+
userMessage: `Codex could not open ${filePath} in Overleaf.`,
|
|
625
|
+
technicalMessage: opened.reason || '',
|
|
626
|
+
evidence: { originalCode: 'file_open_failed', writeStarted: false }
|
|
627
|
+
})
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const ready = navigating
|
|
633
|
+
? await waitForActiveEditorAfterNavigation(filePath, previousEditorIdentity, 7000)
|
|
634
|
+
: await waitForActiveEditorText(filePath, 7000);
|
|
635
|
+
if (!ready.ok) {
|
|
636
|
+
return {
|
|
637
|
+
ok: false,
|
|
638
|
+
code: 'editor_not_ready',
|
|
639
|
+
reason: ready.reason || `Editor content was not ready for ${filePath}`,
|
|
640
|
+
path: filePath,
|
|
641
|
+
failure: buildPageBridgeFailure('target_editor_not_ready', {
|
|
642
|
+
file: filePath,
|
|
643
|
+
activeFile: getActiveFilePath() || '',
|
|
644
|
+
operationType: 'jump',
|
|
645
|
+
changedDocument: false,
|
|
646
|
+
userMessage: `${filePath} became active in Overleaf, but the editor was not ready before timeout.`,
|
|
647
|
+
technicalMessage: ready.reason || '',
|
|
648
|
+
evidence: { originalCode: 'editor_not_ready', writeStarted: false }
|
|
649
|
+
})
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const range = resolveJumpToPositionRange(params, readActiveEditorText(), filePath);
|
|
654
|
+
if (!range.ok) {
|
|
655
|
+
return range;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const focused = editorAdapter.focusActiveEditorRange(range.from, range.to);
|
|
659
|
+
if (!focused.ok) {
|
|
660
|
+
return {
|
|
661
|
+
...focused,
|
|
662
|
+
path: filePath
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
ok: true,
|
|
668
|
+
method: focused.method,
|
|
669
|
+
path: filePath,
|
|
670
|
+
from: range.from,
|
|
671
|
+
to: range.to
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function resolveJumpToPositionRange(params, text, filePath) {
|
|
676
|
+
if (params.line === undefined && params.column === undefined) {
|
|
677
|
+
return {
|
|
678
|
+
ok: true,
|
|
679
|
+
from: params.from,
|
|
680
|
+
to: params.to
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const lines = collectTextLineRanges(String(text || ''));
|
|
685
|
+
const lineCount = lines.length;
|
|
686
|
+
const lineNumber = Number(params.line);
|
|
687
|
+
const lineMetadata = { lineCount };
|
|
688
|
+
if (params.line !== undefined) {
|
|
689
|
+
lineMetadata.line = Number.isInteger(lineNumber) ? lineNumber : params.line;
|
|
690
|
+
}
|
|
691
|
+
if (!Number.isInteger(lineNumber) || lineNumber < 1) {
|
|
692
|
+
return jumpPositionOutOfRange('line_out_of_range', filePath, lineMetadata);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const line = lines[lineNumber - 1];
|
|
696
|
+
if (!line) {
|
|
697
|
+
return jumpPositionOutOfRange('line_out_of_range', filePath, {
|
|
698
|
+
line: lineNumber,
|
|
699
|
+
lineCount
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const lineLength = line.end - line.start;
|
|
704
|
+
if (params.selectLine === true) {
|
|
705
|
+
return {
|
|
706
|
+
ok: true,
|
|
707
|
+
from: line.start,
|
|
708
|
+
to: line.end
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (params.column !== undefined) {
|
|
713
|
+
const columnNumber = Number(params.column);
|
|
714
|
+
const maxColumn = lineLength + 1;
|
|
715
|
+
if (!Number.isInteger(columnNumber) || columnNumber < 1 || columnNumber > maxColumn) {
|
|
716
|
+
return jumpPositionOutOfRange('column_out_of_range', filePath, {
|
|
717
|
+
line: lineNumber,
|
|
718
|
+
column: Number.isInteger(columnNumber) ? columnNumber : params.column,
|
|
719
|
+
lineLength
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
const offset = line.start + columnNumber - 1;
|
|
723
|
+
return {
|
|
724
|
+
ok: true,
|
|
725
|
+
from: offset,
|
|
726
|
+
to: offset
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
ok: true,
|
|
732
|
+
from: line.start,
|
|
733
|
+
to: line.start
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function jumpPositionOutOfRange(code, filePath, metadata = {}) {
|
|
738
|
+
return {
|
|
739
|
+
ok: false,
|
|
740
|
+
code,
|
|
741
|
+
reason: 'Requested jumpToPosition location is out of range',
|
|
742
|
+
path: filePath,
|
|
743
|
+
...metadata
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function collectTextLineRanges(text) {
|
|
748
|
+
const lines = [];
|
|
749
|
+
let start = 0;
|
|
750
|
+
|
|
751
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
752
|
+
if (text[index] === '\r') {
|
|
753
|
+
lines.push({ start, end: index });
|
|
754
|
+
if (text[index + 1] === '\n') {
|
|
755
|
+
index += 1;
|
|
756
|
+
}
|
|
757
|
+
start = index + 1;
|
|
758
|
+
} else if (text[index] === '\n') {
|
|
759
|
+
lines.push({ start, end: index });
|
|
760
|
+
start = index + 1;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
lines.push({ start, end: text.length });
|
|
765
|
+
return lines;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function waitForActiveEditorAfterNavigation(filePath, previousEditorIdentity, timeoutMs) {
|
|
769
|
+
const deadline = Date.now() + timeoutMs;
|
|
770
|
+
let lastText = '';
|
|
771
|
+
while (Date.now() < deadline) {
|
|
772
|
+
const activeFileMatches = getActiveFilePath() === filePath;
|
|
773
|
+
const text = readActiveEditorText();
|
|
774
|
+
lastText = text;
|
|
775
|
+
if (activeFileMatches
|
|
776
|
+
&& window.CodexOverleafProjectFiles.isUsableProjectFileContent(text)
|
|
777
|
+
&& activeEditorIdentityChanged(previousEditorIdentity)) {
|
|
778
|
+
return {
|
|
779
|
+
ok: true,
|
|
780
|
+
text
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
await delay(150);
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
ok: false,
|
|
787
|
+
text: lastText,
|
|
788
|
+
reason: `Editor content was not ready for ${filePath || 'active file'}; last length ${String(lastText || '').length}`
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function getActiveEditorIdentity() {
|
|
793
|
+
const editorView = getCodeMirrorEditorView();
|
|
794
|
+
if (editorView) {
|
|
795
|
+
return {
|
|
796
|
+
type: 'codemirror-view',
|
|
797
|
+
view: editorView,
|
|
798
|
+
state: editorView.state || null,
|
|
799
|
+
doc: editorView.state?.doc || null
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const active = getDeepActiveElement();
|
|
804
|
+
if (active && active.tagName === 'TEXTAREA' && !isInsideCodexPanel(active)) {
|
|
805
|
+
return {
|
|
806
|
+
type: 'textarea',
|
|
807
|
+
node: active
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const textarea = findEditorTextArea();
|
|
812
|
+
if (textarea) {
|
|
813
|
+
return {
|
|
814
|
+
type: 'textarea',
|
|
815
|
+
node: textarea
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const editable = findEditorContentNode('.cm-content') || findEditorContentNode('[contenteditable="true"]');
|
|
820
|
+
if (editable) {
|
|
821
|
+
return {
|
|
822
|
+
type: 'contenteditable',
|
|
823
|
+
node: editable
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function activeEditorIdentityChanged(previous) {
|
|
831
|
+
if (!previous) {
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
const current = getActiveEditorIdentity();
|
|
835
|
+
if (!current || current.type !== previous.type) {
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
if (current.type === 'codemirror-view') {
|
|
839
|
+
return current.view !== previous.view
|
|
840
|
+
|| current.state !== previous.state
|
|
841
|
+
|| current.doc !== previous.doc;
|
|
842
|
+
}
|
|
843
|
+
return current.node !== previous.node;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function getReviewingState(params = {}) {
|
|
847
|
+
const signals = collectReviewingSignals({ ...params, manualOverride: false });
|
|
848
|
+
return {
|
|
849
|
+
signals,
|
|
850
|
+
reviewing: window.CodexOverleafReviewing.detectReviewingFromSignals(signals)
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async function waitForReviewingState(params = {}) {
|
|
855
|
+
const waitMs = Number.isFinite(Number(params.waitMs)) ? Number(params.waitMs) : 1600;
|
|
856
|
+
const deadline = Date.now() + Math.max(0, waitMs);
|
|
857
|
+
let current = getReviewingState(params);
|
|
858
|
+
while (!isReviewingConfirmedForWrite(current) && Date.now() < deadline) {
|
|
859
|
+
await delay(120);
|
|
860
|
+
current = getReviewingState(params);
|
|
861
|
+
}
|
|
862
|
+
return current;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function setReviewingEnabled(enabled, params = {}) {
|
|
866
|
+
const initial = getReviewingState(params);
|
|
867
|
+
const initiallyReviewing = isReviewingConfirmedForWrite(initial);
|
|
868
|
+
if (isReviewingTargetStateConfirmed(enabled, initial)) {
|
|
869
|
+
return {
|
|
870
|
+
ok: true,
|
|
871
|
+
changed: false,
|
|
872
|
+
enabled,
|
|
873
|
+
reviewing: initial.reviewing
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const switched = await toggleReviewingMode(enabled, params);
|
|
878
|
+
if (switched.ok) {
|
|
879
|
+
return {
|
|
880
|
+
ok: true,
|
|
881
|
+
changed: true,
|
|
882
|
+
enabled,
|
|
883
|
+
reviewing: switched.reviewing
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const failureCode = enabled
|
|
888
|
+
? 'reviewing_enable_failed'
|
|
889
|
+
: initiallyReviewing
|
|
890
|
+
? 'reviewing_disable_failed'
|
|
891
|
+
: 'editing_not_confirmed';
|
|
892
|
+
const failureMessage = enabled
|
|
893
|
+
? (switched.attempted
|
|
894
|
+
? 'Codex clicked the Reviewing control after undo, but Overleaf still did not report Reviewing/Track Changes as enabled.'
|
|
895
|
+
: 'Codex could not find an Overleaf Reviewing/Track Changes control to restore after undo.')
|
|
896
|
+
: initiallyReviewing
|
|
897
|
+
? (switched.attempted
|
|
898
|
+
? 'Codex clicked the Reviewing control before undo, but Overleaf still did not report Reviewing/Track Changes as disabled.'
|
|
899
|
+
: 'Codex could not find an Overleaf Reviewing/Track Changes control to disable before undo.')
|
|
900
|
+
: (switched.attempted
|
|
901
|
+
? 'Codex clicked the Overleaf mode control before undo, but Overleaf still did not clearly report Editing mode. To avoid tracked undo changes, Codex did not undo.'
|
|
902
|
+
: 'Codex could not clearly confirm Overleaf Editing mode before undo. To avoid tracked undo changes, Codex did not undo.');
|
|
903
|
+
return {
|
|
904
|
+
ok: false,
|
|
905
|
+
code: failureCode,
|
|
906
|
+
reason: failureMessage,
|
|
907
|
+
reviewing: switched.reviewing || initial.reviewing,
|
|
908
|
+
evidence: { toggleAttempted: switched.attempted === true },
|
|
909
|
+
failure: buildPageBridgeFailure(failureCode, {
|
|
910
|
+
operationType: 'setReviewingEnabled',
|
|
911
|
+
changedDocument: false,
|
|
912
|
+
userMessage: failureMessage,
|
|
913
|
+
evidence: {
|
|
914
|
+
originalCode: failureCode,
|
|
915
|
+
toggleAttempted: switched.attempted === true,
|
|
916
|
+
targetEnabled: enabled === true,
|
|
917
|
+
writeStarted: false
|
|
918
|
+
}
|
|
919
|
+
})
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async function toggleReviewingMode(enabled, params = {}) {
|
|
924
|
+
const attemptedNodes = uniqueNodes([
|
|
925
|
+
findReviewingActivationControl(),
|
|
926
|
+
findReviewingModeMenuControl()
|
|
927
|
+
].filter(Boolean));
|
|
928
|
+
|
|
929
|
+
for (const control of attemptedNodes) {
|
|
930
|
+
clickNode(control);
|
|
931
|
+
let after = await waitForReviewingEnabled(enabled, {
|
|
932
|
+
...params,
|
|
933
|
+
waitMs: Math.min(resolveWaitMs(params), 180)
|
|
934
|
+
});
|
|
935
|
+
if (isReviewingTargetStateConfirmed(enabled, after)) {
|
|
936
|
+
return {
|
|
937
|
+
ok: true,
|
|
938
|
+
attempted: true,
|
|
939
|
+
reviewing: after.reviewing
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const modeOption = findReviewingModeOption(enabled, { requireMenuOption: true });
|
|
944
|
+
if (modeOption) {
|
|
945
|
+
clickNode(modeOption);
|
|
946
|
+
after = await waitForReviewingEnabled(enabled, params);
|
|
947
|
+
if (isReviewingTargetStateConfirmed(enabled, after)) {
|
|
948
|
+
return {
|
|
949
|
+
ok: true,
|
|
950
|
+
attempted: true,
|
|
951
|
+
reviewing: after.reviewing
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const visibleModeOption = findReviewingModeOption(enabled, { requireMenuOption: true });
|
|
958
|
+
if (visibleModeOption) {
|
|
959
|
+
clickNode(visibleModeOption);
|
|
960
|
+
const after = await waitForReviewingEnabled(enabled, params);
|
|
961
|
+
return {
|
|
962
|
+
ok: isReviewingTargetStateConfirmed(enabled, after),
|
|
963
|
+
attempted: true,
|
|
964
|
+
reviewing: after.reviewing
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const after = getReviewingState(params);
|
|
969
|
+
return {
|
|
970
|
+
ok: false,
|
|
971
|
+
attempted: attemptedNodes.length > 0,
|
|
972
|
+
reviewing: after.reviewing
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function waitForReviewingEnabled(enabled, params = {}) {
|
|
977
|
+
const waitMs = resolveWaitMs(params);
|
|
978
|
+
const deadline = Date.now() + Math.max(0, waitMs);
|
|
979
|
+
let current = getReviewingState(params);
|
|
980
|
+
while (!isReviewingTargetStateConfirmed(enabled, current) && Date.now() < deadline) {
|
|
981
|
+
await delay(120);
|
|
982
|
+
current = getReviewingState(params);
|
|
983
|
+
}
|
|
984
|
+
return current;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function resolveWaitMs(params = {}) {
|
|
988
|
+
return Number.isFinite(Number(params.waitMs)) ? Number(params.waitMs) : 1600;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function summarizeReviewingToggleResult(result = {}) {
|
|
992
|
+
return {
|
|
993
|
+
ok: result.ok === true,
|
|
994
|
+
changed: result.changed === true,
|
|
995
|
+
enabled: result.enabled === true,
|
|
996
|
+
code: result.code || '',
|
|
997
|
+
reason: result.reason || ''
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function isReviewingConfirmedForWrite(state = {}) {
|
|
1002
|
+
const reviewing = state.reviewing || {};
|
|
1003
|
+
if (!reviewing.ok || reviewing.status === 'manual-override') {
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
if (reviewing.source !== 'control-text') {
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
return (state.signals?.controls || []).some(control => {
|
|
1010
|
+
const text = [
|
|
1011
|
+
control.text,
|
|
1012
|
+
control.innerText,
|
|
1013
|
+
control.ariaLabel,
|
|
1014
|
+
control.title,
|
|
1015
|
+
control.id,
|
|
1016
|
+
control.className
|
|
1017
|
+
].filter(Boolean).join(' ');
|
|
1018
|
+
const active = ['ariaPressed', 'ariaSelected', 'ariaCurrent']
|
|
1019
|
+
.some(key => /^(true|page|step|location)$/i.test(control[key] || ''));
|
|
1020
|
+
return active && /\breviewing\b|\btrack(?:ed)? changes?\b|\bsuggest(?:ing|ions?)\b/i.test(text);
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function isReviewingTargetStateConfirmed(enabled, state = {}) {
|
|
1025
|
+
return enabled
|
|
1026
|
+
? isReviewingConfirmedForWrite(state)
|
|
1027
|
+
: isEditingConfirmedForNoTraceUndo(state);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function isEditingConfirmedForNoTraceUndo(state = {}) {
|
|
1031
|
+
if (isReviewingConfirmedForWrite(state)) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if (isEditingConfirmedFromControls(state.signals?.controls || [])) {
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
return (state.signals?.internalStates || []).some(value => {
|
|
1038
|
+
const text = normalizeReviewingSignalText(value);
|
|
1039
|
+
return /\b(?:mode|editingMode|reviewMode)\s*:\s*(?:editing|source editing|source mode)\b/i.test(text);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function isEditingConfirmedFromControls(controls = []) {
|
|
1044
|
+
return controls.some(control => {
|
|
1045
|
+
const text = normalizeReviewingSignalText([
|
|
1046
|
+
control.text,
|
|
1047
|
+
control.innerText,
|
|
1048
|
+
control.ariaLabel,
|
|
1049
|
+
control.title,
|
|
1050
|
+
control.value,
|
|
1051
|
+
control.role,
|
|
1052
|
+
control.id,
|
|
1053
|
+
control.className,
|
|
1054
|
+
control.dataTestId,
|
|
1055
|
+
control.dataQa,
|
|
1056
|
+
control.dataOlName,
|
|
1057
|
+
control.ariaSelected,
|
|
1058
|
+
control.ariaCurrent,
|
|
1059
|
+
control.htmlSnippet
|
|
1060
|
+
].filter(Boolean).join(' '));
|
|
1061
|
+
const active = ['ariaPressed', 'ariaSelected', 'ariaCurrent']
|
|
1062
|
+
.some(key => /^(true|page|step|location)$/i.test(control[key] || ''));
|
|
1063
|
+
const inactive = ['ariaPressed', 'ariaSelected', 'ariaCurrent']
|
|
1064
|
+
.some(key => /^false$/i.test(control[key] || ''));
|
|
1065
|
+
const menuOption = /\b(menuitem|option|dropdown-item|menu-item)\b/i.test(text);
|
|
1066
|
+
|
|
1067
|
+
if (isEditingModeText(text)) {
|
|
1068
|
+
return active || isCurrentEditingModeControl(control, text, { menuOption });
|
|
1069
|
+
}
|
|
1070
|
+
if ((/\breviewing\b|\btrack(?:ed)? changes?\b|\bsuggest(?:ing|ions?)\b/i.test(text)) && inactive) {
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
return false;
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function isCurrentEditingModeControl(control = {}, text = '', options = {}) {
|
|
1078
|
+
if (options.menuOption) {
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
const visibleLabel = normalizeReviewingSignalText([
|
|
1082
|
+
control.innerText,
|
|
1083
|
+
control.text,
|
|
1084
|
+
control.ariaLabel,
|
|
1085
|
+
control.title,
|
|
1086
|
+
control.value
|
|
1087
|
+
].filter(Boolean).join(' '));
|
|
1088
|
+
const controlIdentity = normalizeReviewingSignalText([
|
|
1089
|
+
control.role,
|
|
1090
|
+
control.id,
|
|
1091
|
+
control.className,
|
|
1092
|
+
control.dataTestId,
|
|
1093
|
+
control.dataQa,
|
|
1094
|
+
control.dataOlName
|
|
1095
|
+
].filter(Boolean).join(' '));
|
|
1096
|
+
|
|
1097
|
+
return isRepeatedEditingModeLabel(visibleLabel)
|
|
1098
|
+
&& /\b(editor-mode|mode|source|dropdown|switcher|toggle)\b/i.test(controlIdentity)
|
|
1099
|
+
&& !/\breviewing\b|\btrack(?:ed)? changes?\b|\bsuggest(?:ing|ions?)\b/i.test(text);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function isRepeatedEditingModeLabel(value) {
|
|
1103
|
+
const labels = normalizeReviewingSignalText(value)
|
|
1104
|
+
.split(/\s+(?=editing\b|source editing\b|source mode\b)/i)
|
|
1105
|
+
.map(label => normalizeReviewingSignalText(label))
|
|
1106
|
+
.filter(Boolean);
|
|
1107
|
+
return labels.length > 0 && labels.every(label => /^(editing|source editing|source mode)$/i.test(label));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function normalizeReviewingSignalText(value) {
|
|
1111
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function findReviewingModeOption(enabled, options = {}) {
|
|
1115
|
+
const candidates = collectReviewingModeOptionNodes()
|
|
1116
|
+
.filter(node => isReviewingModeOptionCandidate(node, enabled, options))
|
|
1117
|
+
.sort((left, right) => scoreReviewingModeOption(right, enabled) - scoreReviewingModeOption(left, enabled));
|
|
1118
|
+
return candidates[0] || null;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function findReviewingModeMenuControl() {
|
|
1122
|
+
const candidates = collectReviewingModeMenuControlNodes()
|
|
1123
|
+
.filter(node => isReviewingModeMenuControlCandidate(node))
|
|
1124
|
+
.sort((left, right) => scoreReviewingModeMenuControl(right) - scoreReviewingModeMenuControl(left));
|
|
1125
|
+
return candidates[0] || null;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function collectReviewingModeMenuControlNodes() {
|
|
1129
|
+
const selector = [
|
|
1130
|
+
'button',
|
|
1131
|
+
'[role="button"]',
|
|
1132
|
+
'[aria-haspopup]',
|
|
1133
|
+
'[aria-expanded]',
|
|
1134
|
+
'[aria-label]',
|
|
1135
|
+
'[title]',
|
|
1136
|
+
'[id*="mode" i]',
|
|
1137
|
+
'[class*="mode" i]',
|
|
1138
|
+
'[id*="edit" i]',
|
|
1139
|
+
'[class*="edit" i]',
|
|
1140
|
+
'[id*="source" i]',
|
|
1141
|
+
'[class*="source" i]'
|
|
1142
|
+
].join(',');
|
|
1143
|
+
return uniqueNodes([
|
|
1144
|
+
...collectElements(selector, 1500),
|
|
1145
|
+
...collectElements('*', 3500).filter(node => /editing|source mode|source editing/i.test(readNodeSignalText(node)))
|
|
1146
|
+
]).slice(0, 1500);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function isReviewingModeMenuControlCandidate(node) {
|
|
1150
|
+
if (!node || node.disabled || node.getAttribute?.('aria-disabled') === 'true') {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
const text = readNodeSignalText(node);
|
|
1154
|
+
if (/\breview panel\b|\bsyntax highlighting\b/i.test(text)) {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
if (isReviewingModeText(text) && !isEditingModeText(text) && !node.getAttribute?.('aria-haspopup') && !node.getAttribute?.('aria-expanded')) {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
return isEditingModeText(text)
|
|
1161
|
+
|| /\bmode\b/i.test(text)
|
|
1162
|
+
|| /\bsource\b/i.test(text)
|
|
1163
|
+
|| node.getAttribute?.('aria-haspopup')
|
|
1164
|
+
|| node.getAttribute?.('aria-expanded');
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function scoreReviewingModeMenuControl(node) {
|
|
1168
|
+
const text = readNodeSignalText(node);
|
|
1169
|
+
let score = 0;
|
|
1170
|
+
if (node.tagName === 'BUTTON') score += 4;
|
|
1171
|
+
if (isEditingModeText(text)) score += 8;
|
|
1172
|
+
if (/\bmode\b/i.test(text)) score += 4;
|
|
1173
|
+
if (/\bsource\b/i.test(text)) score += 3;
|
|
1174
|
+
if (node.getAttribute?.('aria-haspopup')) score += 3;
|
|
1175
|
+
if (node.getAttribute?.('aria-expanded')) score += 2;
|
|
1176
|
+
return score;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function collectReviewingModeOptionNodes() {
|
|
1180
|
+
const selector = [
|
|
1181
|
+
'button',
|
|
1182
|
+
'[role="button"]',
|
|
1183
|
+
'[role="menuitem"]',
|
|
1184
|
+
'[role="option"]',
|
|
1185
|
+
'[aria-label]',
|
|
1186
|
+
'[title]',
|
|
1187
|
+
'[id*="mode" i]',
|
|
1188
|
+
'[class*="mode" i]',
|
|
1189
|
+
'[id*="edit" i]',
|
|
1190
|
+
'[class*="edit" i]',
|
|
1191
|
+
'[id*="review" i]',
|
|
1192
|
+
'[class*="review" i]',
|
|
1193
|
+
'[id*="track" i]',
|
|
1194
|
+
'[class*="track" i]'
|
|
1195
|
+
].join(',');
|
|
1196
|
+
return uniqueNodes([
|
|
1197
|
+
...collectElements(selector, 1500),
|
|
1198
|
+
...collectElements('*', 3500).filter(node => /editing|reviewing|track changes|suggesting/i.test(readNodeSignalText(node)))
|
|
1199
|
+
]).slice(0, 1500);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function isReviewingModeOptionCandidate(node, enabled, options = {}) {
|
|
1203
|
+
if (!node || node.disabled || node.getAttribute?.('aria-disabled') === 'true') {
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
if (options.requireMenuOption && !isMenuLikeModeOption(node)) {
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
const text = readNodeSignalText(node);
|
|
1210
|
+
return enabled ? isReviewingModeText(text) : isEditingModeText(text);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function isMenuLikeModeOption(node) {
|
|
1214
|
+
const role = node.getAttribute?.('role') || '';
|
|
1215
|
+
const text = readNodeSignalText(node);
|
|
1216
|
+
return /menuitem|option/i.test(role)
|
|
1217
|
+
|| /\bdropdown-item\b|\bmenu-item\b|\bmenuitem\b|\boption\b/i.test(text);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function scoreReviewingModeOption(node, enabled) {
|
|
1221
|
+
const text = readNodeSignalText(node);
|
|
1222
|
+
let score = 0;
|
|
1223
|
+
const role = node.getAttribute?.('role') || '';
|
|
1224
|
+
if (/menuitem|option/i.test(role)) score += 8;
|
|
1225
|
+
if (node.tagName === 'BUTTON') score += 2;
|
|
1226
|
+
if (enabled) {
|
|
1227
|
+
if (/^\s*reviewing\s*$/i.test(node.innerText || node.textContent || '')) score += 8;
|
|
1228
|
+
if (/\breviewing\b/i.test(text)) score += 6;
|
|
1229
|
+
if (/\btrack(?:ed)? changes?\b/i.test(text)) score += 4;
|
|
1230
|
+
if (/\bsuggest(?:ing|ions?)\b/i.test(text)) score += 3;
|
|
1231
|
+
} else {
|
|
1232
|
+
if (/^\s*editing\s*$/i.test(node.innerText || node.textContent || '')) score += 8;
|
|
1233
|
+
if (/\bediting\b/i.test(text)) score += 6;
|
|
1234
|
+
if (/\bsource\b/i.test(text)) score += 3;
|
|
1235
|
+
}
|
|
1236
|
+
return score;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function isReviewingModeText(text) {
|
|
1240
|
+
return /\breviewing\b|\btrack(?:ed)? changes?\b|\bsuggest(?:ing|ions?)\b/i.test(text)
|
|
1241
|
+
&& !/\breview panel\b/i.test(text);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function isEditingModeText(text) {
|
|
1245
|
+
return /\bediting\b|\bsource editing\b|\bsource mode\b/i.test(text)
|
|
1246
|
+
&& !/\bsyntax highlighting\b/i.test(text);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function findReviewingActivationControl() {
|
|
1250
|
+
const nodes = collectReviewingControlNodes();
|
|
1251
|
+
const candidates = nodes
|
|
1252
|
+
.filter(node => isReviewingActivationCandidate(node))
|
|
1253
|
+
.sort((left, right) => scoreReviewingControl(right) - scoreReviewingControl(left));
|
|
1254
|
+
return candidates[0] || null;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function collectReviewingControlNodes() {
|
|
1258
|
+
const selector = [
|
|
1259
|
+
'button',
|
|
1260
|
+
'[role="button"]',
|
|
1261
|
+
'[role="menuitem"]',
|
|
1262
|
+
'[role="option"]',
|
|
1263
|
+
'[aria-label]',
|
|
1264
|
+
'[title]',
|
|
1265
|
+
'[id*="review" i]',
|
|
1266
|
+
'[class*="review" i]',
|
|
1267
|
+
'[id*="track" i]',
|
|
1268
|
+
'[class*="track" i]'
|
|
1269
|
+
].join(',');
|
|
1270
|
+
return uniqueNodes([
|
|
1271
|
+
...collectElements(selector, 1200),
|
|
1272
|
+
...collectElements('*', 3500).filter(node => /reviewing|track changes|suggesting/i.test(readNodeSignalText(node)))
|
|
1273
|
+
]).slice(0, 1500);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function isReviewingActivationCandidate(node) {
|
|
1277
|
+
if (!node || node.disabled || node.getAttribute?.('aria-disabled') === 'true') {
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
const text = readNodeSignalText(node);
|
|
1281
|
+
return /\breviewing\b|\btrack(?:ed)? changes?\b|\bsuggest(?:ing|ions?)\b/i.test(text)
|
|
1282
|
+
&& !/\breview panel\b/i.test(text);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function scoreReviewingControl(node) {
|
|
1286
|
+
const text = readNodeSignalText(node);
|
|
1287
|
+
let score = 0;
|
|
1288
|
+
if (/^\s*reviewing\s*$/i.test(node.innerText || node.textContent || '')) score += 5;
|
|
1289
|
+
if (/\breviewing\b/i.test(text)) score += 4;
|
|
1290
|
+
if (/\btrack(?:ed)? changes?\b/i.test(text)) score += 3;
|
|
1291
|
+
if (/\bsuggest(?:ing|ions?)\b/i.test(text)) score += 2;
|
|
1292
|
+
if (node.tagName === 'BUTTON') score += 1;
|
|
1293
|
+
return score;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function clickNode(node) {
|
|
1297
|
+
if (typeof node.click === 'function') {
|
|
1298
|
+
node.click();
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
node.dispatchEvent?.(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function delay(ms) {
|
|
1305
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
async function waitForSaveState(params = {}) {
|
|
1309
|
+
const deadlineMs = Number.isFinite(Number(params.deadlineMs)) ? Number(params.deadlineMs) : 5000;
|
|
1310
|
+
const pollInterval = Number.isFinite(Number(params.pollIntervalMs)) ? Number(params.pollIntervalMs) : 100;
|
|
1311
|
+
const deadline = Date.now() + Math.max(0, deadlineMs);
|
|
1312
|
+
let lastState = { state: 'unknown', reason: 'Overleaf save state has not been checked yet.' };
|
|
1313
|
+
|
|
1314
|
+
do {
|
|
1315
|
+
let current;
|
|
1316
|
+
try {
|
|
1317
|
+
current = getOverleafSaveState();
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
return {
|
|
1320
|
+
ok: false,
|
|
1321
|
+
state: 'unavailable',
|
|
1322
|
+
reason: `Could not check Overleaf save state: ${error.message}`
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
if (current.state === 'verified_saved') {
|
|
1326
|
+
return { ok: true, state: 'verified_saved' };
|
|
1327
|
+
}
|
|
1328
|
+
if (current.state === 'unavailable') {
|
|
1329
|
+
return {
|
|
1330
|
+
ok: false,
|
|
1331
|
+
state: 'unavailable',
|
|
1332
|
+
reason: current.reason || 'Overleaf save state is unavailable.'
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
lastState = current;
|
|
1336
|
+
if (Date.now() >= deadline) {
|
|
1337
|
+
break;
|
|
1338
|
+
}
|
|
1339
|
+
await delay(Math.min(Math.max(1, pollInterval), Math.max(1, deadline - Date.now())));
|
|
1340
|
+
} while (Date.now() < deadline);
|
|
1341
|
+
|
|
1342
|
+
return {
|
|
1343
|
+
ok: false,
|
|
1344
|
+
state: 'unknown_timeout',
|
|
1345
|
+
reason: buildSaveStateTimeoutReason(lastState)
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function getOverleafSaveState() {
|
|
1350
|
+
if (!document || typeof document.querySelectorAll !== 'function') {
|
|
1351
|
+
return {
|
|
1352
|
+
state: 'unavailable',
|
|
1353
|
+
reason: 'Overleaf document is unavailable for save-state detection.'
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
const selectors = [
|
|
1357
|
+
'.toolbar-header [class*="save" i]',
|
|
1358
|
+
'[class*="saving-status" i]',
|
|
1359
|
+
'[class*="save-status" i]',
|
|
1360
|
+
'[data-testid*="save" i]',
|
|
1361
|
+
'[aria-label*="save" i]'
|
|
1362
|
+
];
|
|
1363
|
+
let sawSaveCandidate = false;
|
|
1364
|
+
let sawVerifiedSaved = false;
|
|
1365
|
+
let sawNegativeState = false;
|
|
1366
|
+
let sawSavingState = false;
|
|
1367
|
+
for (const selector of selectors) {
|
|
1368
|
+
const elements = document.querySelectorAll(selector);
|
|
1369
|
+
for (const el of elements) {
|
|
1370
|
+
if (isHiddenSaveIndicatorNode(el)) {
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
const texts = readSaveIndicatorTexts(el);
|
|
1374
|
+
if (!texts.length) {
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
const combinedText = texts.join(' ');
|
|
1378
|
+
if (isAnySaveIndicatorText(texts, combinedText, isNegativeSaveStateText)) {
|
|
1379
|
+
sawNegativeState = true;
|
|
1380
|
+
sawSaveCandidate = true;
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
if (isAnySaveIndicatorText(texts, combinedText, isSavingStateText)) {
|
|
1384
|
+
sawSavingState = true;
|
|
1385
|
+
sawSaveCandidate = true;
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
if (texts.some(isVerifiedSavedText)) {
|
|
1389
|
+
sawVerifiedSaved = true;
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (isAnySaveIndicatorText(texts, combinedText, isSaveIndicatorCandidateText)) {
|
|
1393
|
+
sawSaveCandidate = true;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (sawNegativeState) {
|
|
1398
|
+
return {
|
|
1399
|
+
state: 'unknown',
|
|
1400
|
+
reason: 'Overleaf save indicator reports changes are not saved.'
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
if (sawSavingState) {
|
|
1404
|
+
return {
|
|
1405
|
+
state: 'saving',
|
|
1406
|
+
reason: 'Overleaf is still saving or syncing changes.'
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
if (sawVerifiedSaved) {
|
|
1410
|
+
return { state: 'verified_saved' };
|
|
1411
|
+
}
|
|
1412
|
+
return {
|
|
1413
|
+
state: 'unknown',
|
|
1414
|
+
reason: sawSaveCandidate
|
|
1415
|
+
? 'Overleaf save indicator was present, but did not verify that all changes are saved.'
|
|
1416
|
+
: 'Overleaf save indicator was not found.'
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function isHiddenSaveIndicatorNode(node) {
|
|
1421
|
+
if (!node) {
|
|
1422
|
+
return true;
|
|
1423
|
+
}
|
|
1424
|
+
if (node.hidden === true || node.inert === true) {
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
if (hasSaveIndicatorAttribute(node, 'hidden') || hasSaveIndicatorAttribute(node, 'inert')) {
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
if (/^true$/i.test(String(node.getAttribute?.('aria-hidden') || ''))) {
|
|
1431
|
+
return true;
|
|
1432
|
+
}
|
|
1433
|
+
if (hasHiddenSaveIndicatorAncestor(node)) {
|
|
1434
|
+
return true;
|
|
1435
|
+
}
|
|
1436
|
+
if (isHiddenStyle(node.style)) {
|
|
1437
|
+
return true;
|
|
1438
|
+
}
|
|
1439
|
+
if (isHiddenStyle(getComputedStyleForSaveIndicator(node))) {
|
|
1440
|
+
return true;
|
|
1441
|
+
}
|
|
1442
|
+
return hasZeroSaveIndicatorLayout(node);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function hasHiddenSaveIndicatorAncestor(node) {
|
|
1446
|
+
if (typeof node.closest === 'function') {
|
|
1447
|
+
try {
|
|
1448
|
+
if (node.closest('[hidden],[inert],[aria-hidden="true"]')) {
|
|
1449
|
+
return true;
|
|
1450
|
+
}
|
|
1451
|
+
} catch (_error) {
|
|
1452
|
+
// Fall back to walking parentElement below.
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
let current = node.parentElement;
|
|
1456
|
+
while (current) {
|
|
1457
|
+
if (current.hidden === true || current.inert === true) {
|
|
1458
|
+
return true;
|
|
1459
|
+
}
|
|
1460
|
+
if (hasSaveIndicatorAttribute(current, 'hidden') || hasSaveIndicatorAttribute(current, 'inert')) {
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
if (/^true$/i.test(String(current.getAttribute?.('aria-hidden') || ''))) {
|
|
1464
|
+
return true;
|
|
1465
|
+
}
|
|
1466
|
+
current = current.parentElement;
|
|
1467
|
+
}
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function hasSaveIndicatorAttribute(node, attribute) {
|
|
1472
|
+
if (typeof node.hasAttribute === 'function') {
|
|
1473
|
+
try {
|
|
1474
|
+
return node.hasAttribute(attribute);
|
|
1475
|
+
} catch (_error) {
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
const value = node.getAttribute?.(attribute);
|
|
1480
|
+
return value != null && value !== '';
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function isHiddenStyle(style) {
|
|
1484
|
+
if (!style) {
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
return /^none$/i.test(String(style.display || ''))
|
|
1488
|
+
|| /^(hidden|collapse)$/i.test(String(style.visibility || ''));
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function getComputedStyleForSaveIndicator(node) {
|
|
1492
|
+
try {
|
|
1493
|
+
const ownerWindow = node.ownerDocument?.defaultView || null;
|
|
1494
|
+
const fallbackWindow = typeof window !== 'undefined' ? window : null;
|
|
1495
|
+
const styleWindow = ownerWindow || fallbackWindow;
|
|
1496
|
+
return styleWindow?.getComputedStyle?.(node) || null;
|
|
1497
|
+
} catch (_error) {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function hasZeroSaveIndicatorLayout(node) {
|
|
1503
|
+
if (typeof node.getClientRects !== 'function') {
|
|
1504
|
+
return false;
|
|
1505
|
+
}
|
|
1506
|
+
try {
|
|
1507
|
+
const rects = node.getClientRects();
|
|
1508
|
+
if (rects && rects.length > 0) {
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
return node.offsetWidth === 0 && node.offsetHeight === 0;
|
|
1512
|
+
} catch (_error) {
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function readSaveIndicatorTexts(node) {
|
|
1518
|
+
const parts = [
|
|
1519
|
+
node.textContent,
|
|
1520
|
+
node.innerText,
|
|
1521
|
+
node.getAttribute?.('aria-label'),
|
|
1522
|
+
node.getAttribute?.('title'),
|
|
1523
|
+
node.getAttribute?.('value')
|
|
1524
|
+
].map(value => String(value || '').replace(/\s+/g, ' ').trim()).filter(Boolean);
|
|
1525
|
+
return Array.from(new Set(parts));
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function isAnySaveIndicatorText(texts, combinedText, predicate) {
|
|
1529
|
+
return predicate(combinedText) || texts.some(predicate);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function isNegativeSaveStateText(text) {
|
|
1533
|
+
return /\bnot\b[\s\S]{0,40}\bsaved\b|\b(?:unsaved|save failed|failed to save|could not save|unable to save)\b|未保存|保存失败/i.test(text);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function isSavingStateText(text) {
|
|
1537
|
+
return /\b(saving|syncing|compiling)\b|保存中|正在保存|同步中/i.test(text);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function isVerifiedSavedText(text) {
|
|
1541
|
+
if (isNegativeSaveStateText(text)) {
|
|
1542
|
+
return false;
|
|
1543
|
+
}
|
|
1544
|
+
return /^\s*(?:all changes saved|changes saved|saved)[\s.!]*$/i.test(text)
|
|
1545
|
+
|| /已保存/i.test(text);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function isSaveIndicatorCandidateText(text) {
|
|
1549
|
+
return /\bsave\b|\bsaved\b|\bsaving\b|保存/i.test(text);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function buildSaveStateTimeoutReason(lastState = {}) {
|
|
1553
|
+
if (lastState.state === 'saving') {
|
|
1554
|
+
return `Timed out waiting for Overleaf to finish saving changes. ${lastState.reason || ''}`.trim();
|
|
1555
|
+
}
|
|
1556
|
+
return `Timed out waiting for a verified Overleaf save state. ${lastState.reason || ''}`.trim();
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function getRequestedSnapshotPaths(params = {}, activePath = '') {
|
|
1560
|
+
return snapshotRouter.getRequestedSnapshotPaths(params, activePath);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
async function applyOperations(operations, options = {}) {
|
|
1564
|
+
return writebackRouter.applyOperations(operations, options);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
async function rejectTrackedChanges(params = {}) {
|
|
1568
|
+
return writebackRouter.rejectTrackedChanges(params);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async function acceptTrackedChanges(params = {}) {
|
|
1572
|
+
return writebackRouter.acceptTrackedChanges(params);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function normalizeTextPatches(patches, length) {
|
|
1576
|
+
const normalized = [];
|
|
1577
|
+
let previousTo = 0;
|
|
1578
|
+
for (const rawPatch of patches || []) {
|
|
1579
|
+
const from = Number(rawPatch?.from);
|
|
1580
|
+
const to = Number(rawPatch?.to);
|
|
1581
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to < from || to > length) {
|
|
1582
|
+
return {
|
|
1583
|
+
ok: false,
|
|
1584
|
+
code: 'invalid_patch',
|
|
1585
|
+
reason: 'Codex 生成的局部写入范围无效。'
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
if (from < previousTo) {
|
|
1589
|
+
return {
|
|
1590
|
+
ok: false,
|
|
1591
|
+
code: 'invalid_patch',
|
|
1592
|
+
reason: 'Codex 生成的局部写入范围有重叠。'
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
previousTo = to;
|
|
1596
|
+
normalized.push({
|
|
1597
|
+
from,
|
|
1598
|
+
to,
|
|
1599
|
+
expected: String(rawPatch.expected ?? ''),
|
|
1600
|
+
insert: String(rawPatch.insert ?? '')
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
ok: true,
|
|
1605
|
+
patches: normalized
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function collectReviewingSignals(params = {}) {
|
|
1610
|
+
const bodyText = document.body?.innerText || '';
|
|
1611
|
+
const bodyTextContent = document.body?.textContent || '';
|
|
1612
|
+
const controls = collectReviewingControls();
|
|
1613
|
+
const internalStates = collectReviewingInternalStates();
|
|
1614
|
+
|
|
1615
|
+
const signals = {
|
|
1616
|
+
manualOverride: params.manualOverride === true,
|
|
1617
|
+
bodyText: `${bodyText}\n${bodyTextContent}`,
|
|
1618
|
+
controls,
|
|
1619
|
+
internalStates
|
|
1620
|
+
};
|
|
1621
|
+
signals.diagnostics = buildReviewingDiagnostics({
|
|
1622
|
+
bodyText,
|
|
1623
|
+
bodyTextContent,
|
|
1624
|
+
controls,
|
|
1625
|
+
internalStates
|
|
1626
|
+
});
|
|
1627
|
+
return signals;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function collectReviewingControls() {
|
|
1631
|
+
const selector = [
|
|
1632
|
+
'button',
|
|
1633
|
+
'[role="button"]',
|
|
1634
|
+
'[aria-label]',
|
|
1635
|
+
'[title]',
|
|
1636
|
+
'[aria-pressed]',
|
|
1637
|
+
'[aria-selected]',
|
|
1638
|
+
'[data-testid]',
|
|
1639
|
+
'[data-test-id]',
|
|
1640
|
+
'[data-qa]',
|
|
1641
|
+
'[data-ol-name]',
|
|
1642
|
+
'[id*="review" i]',
|
|
1643
|
+
'[class*="review" i]',
|
|
1644
|
+
'[id*="track" i]',
|
|
1645
|
+
'[class*="track" i]'
|
|
1646
|
+
].join(',');
|
|
1647
|
+
const selected = collectElements(selector, 1200);
|
|
1648
|
+
const reviewLike = collectElements('*', 3500)
|
|
1649
|
+
.filter(node => /review|track|suggest/i.test(readNodeSignalText(node)));
|
|
1650
|
+
const nodes = uniqueNodes([...selected, ...reviewLike]).slice(0, 1500);
|
|
1651
|
+
return nodes.map(toControlSignal);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function collectElements(selector, limit) {
|
|
1655
|
+
const result = [];
|
|
1656
|
+
const roots = collectSearchRoots();
|
|
1657
|
+
for (let rootIndex = 0; rootIndex < roots.length && result.length < limit; rootIndex += 1) {
|
|
1658
|
+
let nodes = [];
|
|
1659
|
+
try {
|
|
1660
|
+
nodes = Array.from(roots[rootIndex].querySelectorAll(selector));
|
|
1661
|
+
} catch (_error) {
|
|
1662
|
+
nodes = [];
|
|
1663
|
+
}
|
|
1664
|
+
for (const node of nodes) {
|
|
1665
|
+
result.push(node);
|
|
1666
|
+
if (result.length >= limit) {
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return result;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function collectSearchRoots() {
|
|
1675
|
+
const roots = [];
|
|
1676
|
+
const seenRoots = new Set();
|
|
1677
|
+
const queue = [document];
|
|
1678
|
+
|
|
1679
|
+
for (let index = 0; index < queue.length && roots.length < 80; index += 1) {
|
|
1680
|
+
const root = queue[index];
|
|
1681
|
+
if (!root || seenRoots.has(root)) {
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
seenRoots.add(root);
|
|
1685
|
+
roots.push(root);
|
|
1686
|
+
|
|
1687
|
+
let allNodes = [];
|
|
1688
|
+
try {
|
|
1689
|
+
allNodes = Array.from(root.querySelectorAll('*')).slice(0, 4000);
|
|
1690
|
+
} catch (_error) {
|
|
1691
|
+
allNodes = [];
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
for (const node of allNodes) {
|
|
1695
|
+
if (node.shadowRoot && !seenRoots.has(node.shadowRoot)) {
|
|
1696
|
+
queue.push(node.shadowRoot);
|
|
1697
|
+
}
|
|
1698
|
+
if (node.tagName === 'IFRAME') {
|
|
1699
|
+
try {
|
|
1700
|
+
if (node.contentDocument && !seenRoots.has(node.contentDocument)) {
|
|
1701
|
+
queue.push(node.contentDocument);
|
|
1702
|
+
}
|
|
1703
|
+
} catch (_error) {
|
|
1704
|
+
// Cross-origin frames cannot be inspected from the page context.
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
return roots;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function uniqueNodes(nodes) {
|
|
1714
|
+
return Array.from(new Set(nodes));
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function toControlSignal(node) {
|
|
1718
|
+
return {
|
|
1719
|
+
tag: node.tagName?.toLowerCase() || '',
|
|
1720
|
+
text: node.textContent || '',
|
|
1721
|
+
innerText: node.innerText || '',
|
|
1722
|
+
ariaLabel: node.getAttribute('aria-label') || '',
|
|
1723
|
+
title: node.getAttribute('title') || '',
|
|
1724
|
+
value: node.getAttribute('value') || '',
|
|
1725
|
+
role: node.getAttribute('role') || '',
|
|
1726
|
+
id: node.id || '',
|
|
1727
|
+
className: typeof node.className === 'string' ? node.className : '',
|
|
1728
|
+
dataTestId: node.getAttribute('data-testid') || node.getAttribute('data-test-id') || '',
|
|
1729
|
+
dataQa: node.getAttribute('data-qa') || '',
|
|
1730
|
+
dataOlName: node.getAttribute('data-ol-name') || '',
|
|
1731
|
+
ariaPressed: node.getAttribute('aria-pressed') || '',
|
|
1732
|
+
ariaSelected: node.getAttribute('aria-selected') || '',
|
|
1733
|
+
ariaCurrent: node.getAttribute('aria-current') || '',
|
|
1734
|
+
htmlSnippet: compact(node.outerHTML || '', 220)
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function readNodeSignalText(node) {
|
|
1739
|
+
return [
|
|
1740
|
+
node.innerText,
|
|
1741
|
+
node.textContent,
|
|
1742
|
+
node.getAttribute?.('aria-label'),
|
|
1743
|
+
node.getAttribute?.('title'),
|
|
1744
|
+
node.getAttribute?.('id'),
|
|
1745
|
+
typeof node.className === 'string' ? node.className : '',
|
|
1746
|
+
node.getAttribute?.('data-testid'),
|
|
1747
|
+
node.getAttribute?.('data-test-id'),
|
|
1748
|
+
node.getAttribute?.('data-qa'),
|
|
1749
|
+
node.getAttribute?.('data-ol-name')
|
|
1750
|
+
].filter(Boolean).join(' ');
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function buildReviewingDiagnostics({ bodyText, bodyTextContent, controls, internalStates }) {
|
|
1754
|
+
const reviewLikeControls = controls
|
|
1755
|
+
.filter(control => /review|track|suggest/i.test(Object.values(control).filter(Boolean).join(' ')))
|
|
1756
|
+
.slice(0, 12)
|
|
1757
|
+
.map(control => ({
|
|
1758
|
+
tag: control.tag,
|
|
1759
|
+
text: compact(control.innerText || control.text, 120),
|
|
1760
|
+
ariaLabel: compact(control.ariaLabel, 80),
|
|
1761
|
+
title: compact(control.title, 80),
|
|
1762
|
+
role: control.role,
|
|
1763
|
+
id: compact(control.id, 80),
|
|
1764
|
+
className: compact(control.className, 120),
|
|
1765
|
+
dataTestId: compact(control.dataTestId, 80),
|
|
1766
|
+
ariaPressed: control.ariaPressed,
|
|
1767
|
+
ariaSelected: control.ariaSelected,
|
|
1768
|
+
htmlSnippet: compact(control.htmlSnippet, 160)
|
|
1769
|
+
}));
|
|
1770
|
+
|
|
1771
|
+
return {
|
|
1772
|
+
controlCount: controls.length,
|
|
1773
|
+
bodyTextHasReviewing: /\breviewing\b/i.test(bodyText),
|
|
1774
|
+
textContentHasReviewing: /\breviewing\b/i.test(bodyTextContent),
|
|
1775
|
+
bodyTextHasTrack: /\btrack(?:ed)? changes?\b/i.test(bodyText),
|
|
1776
|
+
textContentHasTrack: /\btrack(?:ed)? changes?\b/i.test(bodyTextContent),
|
|
1777
|
+
reviewLikeControls,
|
|
1778
|
+
internalStateCount: internalStates.length,
|
|
1779
|
+
reviewLikeStates: internalStates
|
|
1780
|
+
.filter(value => /review|track|suggest/i.test(value))
|
|
1781
|
+
.slice(0, 8)
|
|
1782
|
+
.map(value => compact(value, 140))
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function collectReviewingInternalStates() {
|
|
1787
|
+
const states = [];
|
|
1788
|
+
const roots = [window._ide, window.Overleaf, window.overleaf, window.OL].filter(Boolean);
|
|
1789
|
+
for (const root of roots) {
|
|
1790
|
+
collectMatchingValues(root, /review|track/i, states, 0);
|
|
1791
|
+
}
|
|
1792
|
+
return states.slice(0, 80);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function collectMatchingValues(value, keyPattern, states, depth) {
|
|
1796
|
+
if (!value || depth > 2 || states.length > 80) {
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
if (typeof value === 'string' || typeof value === 'boolean') {
|
|
1801
|
+
states.push(String(value));
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
if (typeof value !== 'object') {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
for (const key of Object.keys(value).slice(0, 80)) {
|
|
1810
|
+
if (keyPattern.test(key)) {
|
|
1811
|
+
states.push(`${key}:${String(value[key])}`);
|
|
1812
|
+
}
|
|
1813
|
+
const child = value[key];
|
|
1814
|
+
if (child && typeof child === 'object' && keyPattern.test(key)) {
|
|
1815
|
+
collectMatchingValues(child, keyPattern, states, depth + 1);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function detectEditor() {
|
|
1821
|
+
return editorAdapter.detectEditor();
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function buildEditorDiagnostics(editor) {
|
|
1825
|
+
const active = getDeepActiveElement();
|
|
1826
|
+
const editorViewMatch = findCodeMirrorEditorView();
|
|
1827
|
+
const editorView = editorViewMatch?.view || null;
|
|
1828
|
+
const textareas = collectElements('textarea', 30).filter(node => !isInsideCodexPanel(node));
|
|
1829
|
+
const editableNodes = collectElements('[contenteditable="true"], .cm-content, .CodeMirror-code', 50)
|
|
1830
|
+
.filter(node => !isInsideCodexPanel(node));
|
|
1831
|
+
const frames = Array.from(document.querySelectorAll('iframe')).map(frame => {
|
|
1832
|
+
try {
|
|
1833
|
+
return {
|
|
1834
|
+
title: frame.getAttribute('title') || '',
|
|
1835
|
+
src: frame.getAttribute('src') || '',
|
|
1836
|
+
sameOrigin: Boolean(frame.contentDocument)
|
|
1837
|
+
};
|
|
1838
|
+
} catch (_error) {
|
|
1839
|
+
return {
|
|
1840
|
+
title: frame.getAttribute('title') || '',
|
|
1841
|
+
src: frame.getAttribute('src') || '',
|
|
1842
|
+
sameOrigin: false
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
return {
|
|
1848
|
+
ok: Boolean(editor?.ok),
|
|
1849
|
+
globals: describeGlobals(),
|
|
1850
|
+
documentStats: describeDocumentStats(),
|
|
1851
|
+
unstableStore: describeUnstableStore(),
|
|
1852
|
+
codeMirrorView: editorView
|
|
1853
|
+
? {
|
|
1854
|
+
docLength: getCodeMirrorDocLength(editorView),
|
|
1855
|
+
hasDispatch: typeof editorView.dispatch === 'function',
|
|
1856
|
+
source: editorViewMatch.source
|
|
1857
|
+
}
|
|
1858
|
+
: null,
|
|
1859
|
+
active: describeEditorNode(active),
|
|
1860
|
+
textareaCount: textareas.length,
|
|
1861
|
+
textareas: textareas.slice(0, 5).map(describeEditorNode),
|
|
1862
|
+
editableCount: editableNodes.length,
|
|
1863
|
+
editables: editableNodes.slice(0, 5).map(describeEditorNode),
|
|
1864
|
+
iframeCount: frames.length,
|
|
1865
|
+
iframes: frames.slice(0, 5)
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function describeEditorNode(node) {
|
|
1870
|
+
if (!node) {
|
|
1871
|
+
return null;
|
|
1872
|
+
}
|
|
1873
|
+
const value = node.value || node.innerText || node.textContent || '';
|
|
1874
|
+
return {
|
|
1875
|
+
tag: node.tagName?.toLowerCase() || '',
|
|
1876
|
+
role: node.getAttribute?.('role') || '',
|
|
1877
|
+
ariaLabel: compact(node.getAttribute?.('aria-label') || '', 80),
|
|
1878
|
+
title: compact(node.getAttribute?.('title') || '', 80),
|
|
1879
|
+
id: compact(node.id || '', 80),
|
|
1880
|
+
className: compact(typeof node.className === 'string' ? node.className : '', 120),
|
|
1881
|
+
valueLength: String(value || '').length,
|
|
1882
|
+
likelySource: isLikelySourceEditorNode(node)
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
function readActiveEditorText() {
|
|
1887
|
+
return editorAdapter.readActiveEditorText();
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function replaceActiveEditorText(text) {
|
|
1891
|
+
return editorAdapter.replaceActiveEditorText(text);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function replaceActiveEditorPatches(patches, nextContent) {
|
|
1895
|
+
return editorAdapter.replaceActiveEditorPatches(patches, nextContent);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function getCodeMirrorEditorView() {
|
|
1899
|
+
return findCodeMirrorEditorView()?.view || null;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function findCodeMirrorEditorView() {
|
|
1903
|
+
const store = getOverleafUnstableStore();
|
|
1904
|
+
const candidates = [
|
|
1905
|
+
['store:editor.view', readStoreValue(store, 'editor.view')],
|
|
1906
|
+
['store:editor.view.camel', readStoreValue(store, 'editor.view.camel')],
|
|
1907
|
+
['store:editor', readStoreValue(store, 'editor').view],
|
|
1908
|
+
['window.overleaf.unstable.editorView', window.overleaf?.unstable?.editorView],
|
|
1909
|
+
['window.Overleaf.editorView', window.Overleaf?.editorView],
|
|
1910
|
+
['window._ide.editorView', window._ide?.editorView]
|
|
1911
|
+
];
|
|
1912
|
+
|
|
1913
|
+
for (const [source, candidate] of candidates) {
|
|
1914
|
+
if (isCodeMirrorEditorView(candidate)) {
|
|
1915
|
+
return {
|
|
1916
|
+
view: candidate,
|
|
1917
|
+
source
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function getOverleafUnstableStore() {
|
|
1926
|
+
return window.overleaf?.unstable?.store || window.Overleaf?.unstable?.store || null;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function readStoreValue(store, path) {
|
|
1930
|
+
if (!store || typeof store.get !== 'function') {
|
|
1931
|
+
return {};
|
|
1932
|
+
}
|
|
1933
|
+
try {
|
|
1934
|
+
return store.get(path) || {};
|
|
1935
|
+
} catch (_error) {
|
|
1936
|
+
return {};
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
function isCodeMirrorEditorView(value) {
|
|
1941
|
+
return Boolean(
|
|
1942
|
+
value
|
|
1943
|
+
&& typeof value === 'object'
|
|
1944
|
+
&& typeof value.dispatch === 'function'
|
|
1945
|
+
&& value.state
|
|
1946
|
+
&& value.state.doc
|
|
1947
|
+
&& typeof value.state.doc.toString === 'function'
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
function getCodeMirrorDocText(editorView) {
|
|
1952
|
+
return editorView.state.doc.toString();
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
function getCodeMirrorDocLength(editorView) {
|
|
1956
|
+
const doc = editorView.state.doc;
|
|
1957
|
+
return Number.isInteger(doc.length) ? doc.length : getCodeMirrorDocText(editorView).length;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
function describeUnstableStore() {
|
|
1961
|
+
const store = getOverleafUnstableStore();
|
|
1962
|
+
if (!store) {
|
|
1963
|
+
return {
|
|
1964
|
+
present: false
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
const paths = ['editor.view', 'editor.open_doc_id', 'editor.open_doc_name', 'settings'];
|
|
1969
|
+
const readable = [];
|
|
1970
|
+
for (const path of paths) {
|
|
1971
|
+
try {
|
|
1972
|
+
const value = store.get(path);
|
|
1973
|
+
readable.push({
|
|
1974
|
+
path,
|
|
1975
|
+
type: value === null ? 'null' : typeof value,
|
|
1976
|
+
present: value !== undefined
|
|
1977
|
+
});
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
readable.push({
|
|
1980
|
+
path,
|
|
1981
|
+
type: 'error',
|
|
1982
|
+
present: false,
|
|
1983
|
+
reason: error.message
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
return {
|
|
1989
|
+
present: true,
|
|
1990
|
+
readable
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function describeGlobals() {
|
|
1995
|
+
return {
|
|
1996
|
+
overleaf: describeGlobalObject(window.overleaf),
|
|
1997
|
+
Overleaf: describeGlobalObject(window.Overleaf),
|
|
1998
|
+
_ide: describeGlobalObject(window._ide),
|
|
1999
|
+
OL: describeGlobalObject(window.OL)
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
function describeGlobalObject(value) {
|
|
2004
|
+
if (!value) {
|
|
2005
|
+
return 'missing';
|
|
2006
|
+
}
|
|
2007
|
+
if (typeof value !== 'object' && typeof value !== 'function') {
|
|
2008
|
+
return typeof value;
|
|
2009
|
+
}
|
|
2010
|
+
let keys = [];
|
|
2011
|
+
try {
|
|
2012
|
+
keys = Object.keys(value).slice(0, 8);
|
|
2013
|
+
} catch (_error) {
|
|
2014
|
+
keys = [];
|
|
2015
|
+
}
|
|
2016
|
+
return keys.length ? keys.join('|') : 'present';
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
function describeDocumentStats() {
|
|
2020
|
+
return {
|
|
2021
|
+
elementCount: document.querySelectorAll('*').length,
|
|
2022
|
+
textareaCount: document.querySelectorAll('textarea').length,
|
|
2023
|
+
cmCount: document.querySelectorAll('.cm-content,.cm-editor,.cm-line,.CodeMirror,.CodeMirror-code').length,
|
|
2024
|
+
roleTextboxCount: document.querySelectorAll('[role="textbox"]').length
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function findEditorTextArea() {
|
|
2029
|
+
const candidates = collectElements('textarea', 120)
|
|
2030
|
+
.filter(node => !isInsideCodexPanel(node));
|
|
2031
|
+
return candidates.find(isLikelySourceEditorNode)
|
|
2032
|
+
|| candidates.find(node => window.CodexOverleafProjectFiles.isUsableProjectFileContent(node.value))
|
|
2033
|
+
|| candidates[0]
|
|
2034
|
+
|| null;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function findEditorContentNode(selector) {
|
|
2038
|
+
return collectElements(selector, 120)
|
|
2039
|
+
.find(node => !isInsideCodexPanel(node)) || null;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function getDeepActiveElement() {
|
|
2043
|
+
let active = document.activeElement;
|
|
2044
|
+
const seen = new Set();
|
|
2045
|
+
|
|
2046
|
+
while (active && !seen.has(active)) {
|
|
2047
|
+
seen.add(active);
|
|
2048
|
+
|
|
2049
|
+
if (active.shadowRoot?.activeElement) {
|
|
2050
|
+
active = active.shadowRoot.activeElement;
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
if (active.tagName === 'IFRAME') {
|
|
2055
|
+
try {
|
|
2056
|
+
if (active.contentDocument?.activeElement) {
|
|
2057
|
+
active = active.contentDocument.activeElement;
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
} catch (_error) {
|
|
2061
|
+
// Cross-origin frames cannot be inspected.
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
break;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
return active;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
function isInsideCodexPanel(node) {
|
|
2072
|
+
return Boolean(node?.closest?.('#codex-overleaf-panel'));
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
function isLikelySourceEditorNode(node) {
|
|
2076
|
+
const signal = [
|
|
2077
|
+
node.getAttribute('aria-label'),
|
|
2078
|
+
node.getAttribute('aria-describedby'),
|
|
2079
|
+
node.getAttribute('placeholder'),
|
|
2080
|
+
node.getAttribute('role'),
|
|
2081
|
+
node.id,
|
|
2082
|
+
typeof node.className === 'string' ? node.className : '',
|
|
2083
|
+
nearestClassSignal(node)
|
|
2084
|
+
].filter(Boolean).join(' ');
|
|
2085
|
+
return /source|editor|code|latex|codemirror|cm-/i.test(signal);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function nearestClassSignal(node) {
|
|
2089
|
+
const parts = [];
|
|
2090
|
+
let current = node;
|
|
2091
|
+
for (let index = 0; current && index < 5; index += 1) {
|
|
2092
|
+
if (typeof current.className === 'string') {
|
|
2093
|
+
parts.push(current.className);
|
|
2094
|
+
}
|
|
2095
|
+
current = current.parentElement;
|
|
2096
|
+
}
|
|
2097
|
+
return parts.join(' ');
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
function requirePageModule(globalName) {
|
|
2101
|
+
const moduleApi = window[globalName];
|
|
2102
|
+
if (!moduleApi || typeof moduleApi.create !== 'function') {
|
|
2103
|
+
throw new Error(globalName + ' is not installed in the Overleaf page world');
|
|
2104
|
+
}
|
|
2105
|
+
return moduleApi;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
function getProjectId() {
|
|
2109
|
+
return treeOperations?.getProjectId?.() || null;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// Writeback project-ID guard surface lives in extension/src/page/writeGuard.js
|
|
2113
|
+
// and is accessed here via `writeGuard.runWriteGuard(params)` from the
|
|
2114
|
+
// applyOperations / acceptTrackedChanges / rejectTrackedChanges dispatchers.
|
|
2115
|
+
|
|
2116
|
+
function getActiveFilePath() {
|
|
2117
|
+
return getActiveFilePathFromEditorStore()
|
|
2118
|
+
|| treeOperations?.getActiveFilePath?.()
|
|
2119
|
+
|| treeOperations?.getRecentFileTreeSelectionPath?.()
|
|
2120
|
+
|| '';
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function getActiveFilePathFromEditorStore(options = {}) {
|
|
2124
|
+
const store = getOverleafUnstableStore();
|
|
2125
|
+
const docId = normalizeDocId(firstStringValue([
|
|
2126
|
+
readStoreValue(store, 'editor.open_doc_id'),
|
|
2127
|
+
readStoreValue(store, 'editor.openDocId'),
|
|
2128
|
+
window._ide?.editor?.open_doc_id,
|
|
2129
|
+
window._ide?.editor?.openDocId
|
|
2130
|
+
]));
|
|
2131
|
+
if (docId) {
|
|
2132
|
+
const record = collectDocRecords({ includeWindowGlobals: true }).find(item => item.id === docId);
|
|
2133
|
+
if (record?.path) {
|
|
2134
|
+
return record.path;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
const docName = normalizeSafeProjectPath(firstStringValue([
|
|
2139
|
+
readStoreValue(store, 'editor.open_doc_name'),
|
|
2140
|
+
readStoreValue(store, 'editor.openDocName'),
|
|
2141
|
+
window._ide?.editor?.open_doc_name,
|
|
2142
|
+
window._ide?.editor?.openDocName
|
|
2143
|
+
]));
|
|
2144
|
+
if (!docName || !window.CodexOverleafProjectFiles.isTextProjectPath(docName)) {
|
|
2145
|
+
return '';
|
|
2146
|
+
}
|
|
2147
|
+
if (docName.includes('/')) {
|
|
2148
|
+
return docName;
|
|
2149
|
+
}
|
|
2150
|
+
if (options.passive === true) {
|
|
2151
|
+
return docName;
|
|
2152
|
+
}
|
|
2153
|
+
const matches = collectDocRecords({ includeWindowGlobals: true })
|
|
2154
|
+
.filter(item => item.path === docName || item.path.endsWith(`/${docName}`));
|
|
2155
|
+
return matches.length === 1 ? matches[0].path : docName;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function normalizeDocId(value) {
|
|
2159
|
+
const text = String(value || '').trim();
|
|
2160
|
+
return /^[a-f0-9]{24}$/i.test(text) ? text : '';
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function firstStringValue(values = []) {
|
|
2164
|
+
for (const value of values) {
|
|
2165
|
+
if (typeof value === 'string' && value.trim()) {
|
|
2166
|
+
return value;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
return '';
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
function openFileByPath(filePath) {
|
|
2173
|
+
return treeOperations.openFileByPath(filePath);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
function findFileTreeNode(filePath) {
|
|
2177
|
+
return treeOperations.findFileTreeNode(filePath);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
function projectPathExists(filePath) {
|
|
2181
|
+
return treeOperations.projectPathExists(filePath);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
function collectProjectTextPaths(activePath) {
|
|
2185
|
+
return treeOperations.collectProjectTextPaths(activePath);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
function collectDocRecords(options = {}) {
|
|
2189
|
+
return treeOperations.collectDocRecords(options);
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
function collectInternalRootKeys() {
|
|
2193
|
+
return treeOperations.collectInternalRootKeys();
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
function findFileTreeManager() {
|
|
2197
|
+
return treeOperations.findFileTreeManager();
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
function findHistoryObject() {
|
|
2201
|
+
return treeOperations.findHistoryObject();
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
function waitForActiveFile(filePath, timeoutMs) {
|
|
2205
|
+
return treeOperations.waitForActiveFile(filePath, timeoutMs);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function waitForActiveEditorText(filePath, timeoutMs, options = {}) {
|
|
2209
|
+
return treeOperations.waitForActiveEditorText(filePath, timeoutMs, options);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
function contentSignature(content) {
|
|
2213
|
+
return treeOperations.contentSignature(content);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
function fileTreeMethodNames(type) {
|
|
2217
|
+
return treeOperations.fileTreeMethodNames(type);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
function compact(value, limit) {
|
|
2221
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
2222
|
+
return text.length > limit ? text.slice(0, limit - 3) + '...' : text;
|
|
2223
|
+
}
|
|
2224
|
+
})();
|