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.
Files changed (72) hide show
  1. package/README.md +45 -21
  2. package/bin/codex-overleaf-link.mjs +58 -0
  3. package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
  4. package/extension/assets/icons/codex-overleaf-icon.png +0 -0
  5. package/extension/assets/icons/icon128.png +0 -0
  6. package/extension/assets/icons/icon16.png +0 -0
  7. package/extension/assets/icons/icon32.png +0 -0
  8. package/extension/assets/icons/icon48.png +0 -0
  9. package/extension/bootstrap/background.js +404 -0
  10. package/extension/bootstrap/manifest.template.json +51 -0
  11. package/extension/bootstrap/popup.css +26 -0
  12. package/extension/bootstrap/popup.html +38 -0
  13. package/extension/bootstrap/popup.js +44 -0
  14. package/extension/bootstrap/runtimeContext.js +2 -0
  15. package/extension/runtime-manifest.json +60 -0
  16. package/extension/src/background.js +532 -0
  17. package/extension/src/content/applyResultFormatters.js +387 -0
  18. package/extension/src/content/composerAttachments.js +522 -0
  19. package/extension/src/content/composerPanel.js +254 -0
  20. package/extension/src/content/contentRuntime.js +8895 -0
  21. package/extension/src/content/contextTray.js +686 -0
  22. package/extension/src/content/diagnosticsController.js +659 -0
  23. package/extension/src/content/diagnosticsPanel.js +342 -0
  24. package/extension/src/content/diffReviewPanel.js +797 -0
  25. package/extension/src/content/localSkillsPanel.js +280 -0
  26. package/extension/src/content/markdownText.js +606 -0
  27. package/extension/src/content/mirrorHealth.js +92 -0
  28. package/extension/src/content/modelPicker.js +489 -0
  29. package/extension/src/content/nativeChannel.js +62 -0
  30. package/extension/src/content/otWarmMirror.js +964 -0
  31. package/extension/src/content/otWarmMirrorController.js +174 -0
  32. package/extension/src/content/panelMaintenance.js +367 -0
  33. package/extension/src/content/panelRenderer.js +253 -0
  34. package/extension/src/content/recentProjects.js +1063 -0
  35. package/extension/src/content/reviewHunks.js +247 -0
  36. package/extension/src/content/runController.js +242 -0
  37. package/extension/src/content/runTimelineView.js +1126 -0
  38. package/extension/src/content/sessionManager.js +487 -0
  39. package/extension/src/content/sessionPanel.js +263 -0
  40. package/extension/src/content/settingsPanel.js +527 -0
  41. package/extension/src/content/themeController.js +91 -0
  42. package/extension/src/content/updateIdle.js +67 -0
  43. package/extension/src/content/writebackController.js +300 -0
  44. package/extension/src/content/writebackOrchestrator.js +850 -0
  45. package/extension/src/contentScript.js +8 -0
  46. package/extension/src/page/compileBridge.js +460 -0
  47. package/extension/src/page/overleafCapabilities.js +100 -0
  48. package/extension/src/page/overleafEditor.js +282 -0
  49. package/extension/src/page/overleafProjectSnapshot.js +234 -0
  50. package/extension/src/page/overleafRealtimeObserver.js +491 -0
  51. package/extension/src/page/pageBridgeCapability.js +71 -0
  52. package/extension/src/page/snapshotRouter.js +948 -0
  53. package/extension/src/page/trackedChangesLifecycle.js +1506 -0
  54. package/extension/src/page/treeOperations.js +1584 -0
  55. package/extension/src/page/writeGuard.js +142 -0
  56. package/extension/src/page/writebackRouter.js +1934 -0
  57. package/extension/src/pageBridge.js +2224 -0
  58. package/extension/src/popup.js +247 -0
  59. package/extension/src/shared/compatibility.js +1 -1
  60. package/extension/styles/panel.css +4048 -0
  61. package/native-host/src/index.js +9 -2
  62. package/native-host/src/managedInstall.js +393 -0
  63. package/native-host/src/managedLauncherRuntime.js +71 -0
  64. package/native-host/src/nativeHostPlatform.js +16 -0
  65. package/native-host/src/runtimeInstaller.js +6 -1
  66. package/native-host/src/taskRunnerRuntime.js +8 -0
  67. package/native-host/src/updateArchive.js +253 -0
  68. package/native-host/src/updateManager.js +548 -0
  69. package/native-host/src/updateTrust.js +156 -0
  70. package/package.json +11 -2
  71. package/scripts/install-managed.mjs +64 -0
  72. 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
+ })();