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,964 @@
1
+ (function initCodexOverleafOtWarmMirror() {
2
+ 'use strict';
3
+
4
+ const otWarmMirrorController = window.CodexOverleafOtWarmMirrorController;
5
+ const runController = window.CodexOverleafRunController;
6
+ const mirrorHealth = window.CodexOverleafMirrorHealth;
7
+
8
+ // Experimental OT warm-mirror glue carved out of contentRuntime.js (v1.4.9
9
+ // structural-debt phase 5): the per-project enable toggle flow, the
10
+ // poll/flush timers and patch queue, mirror prefetch, warm-start
11
+ // resolution, and the OT status display. Code moved verbatim; runtime
12
+ // collaborators are factory-injected and mutable runtime state is read
13
+ // through lazy getters (panel state writes go back through setState). The
14
+ // OT/prefetch view state below moved here with the code that owns it; the
15
+ // runtime reads it through the exported accessors and clears it on project
16
+ // navigation via clearMirrorPrefetchTimer()/releaseOtWarmMirrorProject().
17
+ function create(deps = {}) {
18
+ const {
19
+ tr,
20
+ tx,
21
+ closeDiagnosticsMenu,
22
+ syncCustomInstructionsEditorForProject,
23
+ throwIfRunCancellationRequested,
24
+ showPluginConfirm,
25
+ showPluginToast,
26
+ getCurrentProjectId,
27
+ updateProbeStatusOtSuffix,
28
+ sanitizeRunProjectSnapshot,
29
+ getMirrorFreshness,
30
+ buildSnapshotFileOverlays,
31
+ normalizeSnapshotPath,
32
+ formatProjectSnapshotUserLog,
33
+ formatProjectSnapshotWarning,
34
+ getProjectSnapshotWarnings,
35
+ sendBackgroundNative,
36
+ callPageBridge,
37
+ saveStateSoon,
38
+ finishRunView,
39
+ appendRunEvent,
40
+ appendCompletionReport,
41
+ appendLog,
42
+ appendPlainLog,
43
+ getPanel,
44
+ getState,
45
+ setState,
46
+ getCurrentRunView,
47
+ RUN_SNAPSHOT_ZIP_TIMEOUT_MS
48
+ } = deps;
49
+
50
+ let currentOtStatus = 'off';
51
+ let otSyncRequestId = 0;
52
+ let otWarmMirrorProjectId = '';
53
+ let lastExperimentalOtProjectId = '';
54
+ let otWarmMirrorState = {
55
+ projectId: '',
56
+ pollTimer: null,
57
+ flushTimer: null,
58
+ flushing: false,
59
+ patchQueue: [],
60
+ lastStatus: 'off',
61
+ lastPatchAt: 0,
62
+ lastErrorCode: ''
63
+ };
64
+ let mirrorPrefetchState = {
65
+ inFlight: null,
66
+ lastSuccessAt: 0,
67
+ lastErrorAt: 0,
68
+ lastError: null,
69
+ timer: null,
70
+ projectId: ''
71
+ };
72
+
73
+ function getCurrentOtStatus() {
74
+ return currentOtStatus;
75
+ }
76
+
77
+ function getOtWarmMirrorState() {
78
+ return otWarmMirrorState;
79
+ }
80
+
81
+ function getLastExperimentalOtProjectId() {
82
+ return lastExperimentalOtProjectId;
83
+ }
84
+
85
+ // Clear the mirror-prefetch retry timer (called by the runtime when leaving
86
+ // a project, so a stale timer can't fire against the next project).
87
+ function clearMirrorPrefetchTimer() {
88
+ if (mirrorPrefetchState && mirrorPrefetchState.timer) {
89
+ window.clearTimeout(mirrorPrefetchState.timer);
90
+ mirrorPrefetchState.timer = null;
91
+ }
92
+ }
93
+
94
+ // Reset the OT warm-mirror project binding when navigating away, so a stale
95
+ // projectId can't trick the canPollOtWarmMirror check after navigation.
96
+ function releaseOtWarmMirrorProject(prevProjectId) {
97
+ if (otWarmMirrorProjectId === prevProjectId) {
98
+ otWarmMirrorProjectId = '';
99
+ }
100
+ if (otWarmMirrorState && otWarmMirrorState.projectId === prevProjectId) {
101
+ otWarmMirrorState.projectId = '';
102
+ }
103
+ }
104
+
105
+ function isExperimentalOtEnabled() {
106
+ const projectId = getCurrentProjectId();
107
+ return isExperimentalOtEnabledForProject(projectId);
108
+ }
109
+
110
+ function isExperimentalOtEnabledForProject(projectId) {
111
+ return getState()?.experimentalOtByProject?.[projectId] === true;
112
+ }
113
+
114
+ function setExperimentalOtEnabled(enabled) {
115
+ const projectId = getCurrentProjectId();
116
+ setExperimentalOtEnabledForProject(projectId, enabled);
117
+ }
118
+
119
+ function setExperimentalOtEnabledForProject(projectId, enabled) {
120
+ if (!projectId) {
121
+ return;
122
+ }
123
+ setState({
124
+ ...getState(),
125
+ experimentalOtByProject: {
126
+ ...normalizeExperimentalOtByProject(getState()?.experimentalOtByProject),
127
+ [projectId]: enabled === true
128
+ }
129
+ });
130
+ }
131
+
132
+ function normalizeExperimentalOtByProject(value) {
133
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
134
+ return {};
135
+ }
136
+ const normalized = {};
137
+ for (const key of Object.keys(value)) {
138
+ if (key) {
139
+ normalized[key] = value[key] === true;
140
+ }
141
+ }
142
+ return normalized;
143
+ }
144
+
145
+ function syncExperimentalOtToggleForProject(projectId = getCurrentProjectId()) {
146
+ lastExperimentalOtProjectId = projectId;
147
+ const enabled = isExperimentalOtEnabledForProject(projectId);
148
+ const experimentalOtCheckbox = getPanel()?.querySelector('[data-experimental-ot]');
149
+ if (experimentalOtCheckbox) {
150
+ experimentalOtCheckbox.checked = enabled;
151
+ }
152
+ updateExperimentalOtToggleControl(enabled);
153
+ updateOtStatusDisplay(enabled
154
+ ? (currentOtStatus === 'off' ? 'starting' : currentOtStatus)
155
+ : 'off');
156
+ return enabled;
157
+ }
158
+
159
+ async function handleExperimentalOtToggleClick(event) {
160
+ // The handler is bound to the visible checkbox: by the time click fires,
161
+ // pre-click activation has ALREADY flipped .checked — that flipped value
162
+ // is the user's intended target. preventDefault then rolls the visual
163
+ // state back (synchronously, when dispatch completes), and the confirm
164
+ // flow applies the target only after approval.
165
+ const clicked = event?.currentTarget || event?.target;
166
+ const targetEnabled = clicked && typeof clicked.checked === 'boolean' ? clicked.checked : undefined;
167
+ event.preventDefault();
168
+ await toggleExperimentalOtCheckbox(targetEnabled);
169
+ }
170
+
171
+ async function handleExperimentalOtToggleKeydown(event) {
172
+ if (event.key !== 'Enter' && event.key !== ' ') {
173
+ return;
174
+ }
175
+ event.preventDefault();
176
+ await toggleExperimentalOtCheckbox();
177
+ }
178
+
179
+ async function toggleExperimentalOtCheckbox(targetEnabled) {
180
+ const checkbox = getPanel()?.querySelector('[data-experimental-ot]');
181
+ if (!checkbox) {
182
+ return;
183
+ }
184
+ // Explicit target from the click interceptor wins; the keyboard/legacy
185
+ // path (no argument, un-flipped checkbox) falls back to inversion.
186
+ const nextEnabled = typeof targetEnabled === 'boolean' ? targetEnabled : !checkbox.checked;
187
+ if (nextEnabled) {
188
+ closeDiagnosticsMenu();
189
+ const approved = await showPluginConfirm({
190
+ title: tr('experimentalOtConfirmTitle'),
191
+ message: tr('experimentalOtConfirmMessage'),
192
+ confirmLabel: tr('experimentalOtConfirmEnable'),
193
+ cancelLabel: tr('confirmDefaultCancel')
194
+ });
195
+ if (!approved) {
196
+ return;
197
+ }
198
+ } else {
199
+ closeDiagnosticsMenu();
200
+ }
201
+ checkbox.checked = nextEnabled;
202
+ handleExperimentalOtToggleChange({ currentTarget: checkbox });
203
+ if (nextEnabled) {
204
+ showPluginToast(tr('experimentalOtEnabledToast'), { status: 'info' });
205
+ }
206
+ }
207
+
208
+ function handleExperimentalOtToggleChange(event) {
209
+ const checkbox = event?.currentTarget || getPanel()?.querySelector('[data-experimental-ot]');
210
+ if (!checkbox) {
211
+ return;
212
+ }
213
+ const projectId = getCurrentProjectId();
214
+ lastExperimentalOtProjectId = projectId;
215
+ setExperimentalOtEnabledForProject(projectId, checkbox.checked);
216
+ updateExperimentalOtToggleControl(checkbox.checked);
217
+ // Per-card saved feedback, same contract as every other settings card.
218
+ const savedBadge = getPanel()?.querySelector('details[data-set-group="experimental"] [data-set-saved]');
219
+ if (savedBadge) {
220
+ savedBadge.hidden = false;
221
+ if (savedBadge._savedFlashTimer) {
222
+ clearTimeout(savedBadge._savedFlashTimer);
223
+ }
224
+ savedBadge._savedFlashTimer = setTimeout(() => {
225
+ savedBadge.hidden = true;
226
+ }, 1600);
227
+ }
228
+ updateOtStatusDisplay(checkbox.checked ? (currentOtStatus === 'off' ? 'starting' : currentOtStatus) : 'off');
229
+ syncOtWarmMirrorController().catch(error => {
230
+ updateOtStatusDisplay('unavailable');
231
+ appendPlainLog(tx(`Experimental OT warm mirror unavailable: ${error.message}`, `实验性 OT 预热镜像不可用:${error.message}`));
232
+ });
233
+ saveStateSoon();
234
+ }
235
+
236
+ function updateExperimentalOtToggleControl(enabled) {
237
+ // The visible switch (the checkbox itself) is the single control; keep it
238
+ // in sync for programmatic changes, then refresh the status line.
239
+ const checkbox = getPanel()?.querySelector('[data-experimental-ot]');
240
+ if (checkbox) {
241
+ checkbox.checked = enabled === true;
242
+ }
243
+ updateExperimentalOtMenuStatus();
244
+ }
245
+
246
+ async function syncOtWarmMirrorController() {
247
+ const projectId = getCurrentProjectId();
248
+ const requestId = ++otSyncRequestId;
249
+ const enabled = isExperimentalOtEnabledForProject(projectId);
250
+ otWarmMirrorProjectId = projectId;
251
+ ensureOtWarmMirrorStateProject(projectId);
252
+ if (enabled) {
253
+ const pause = otWarmMirrorController?.shouldPauseOtWarmMirror?.({ running: Boolean(getCurrentRunView()) }) || { pause: false };
254
+ if (pause.pause) {
255
+ clearOtEventPolling({ clearPatchQueue: true });
256
+ otWarmMirrorState.lastErrorCode = pause.reason || 'paused';
257
+ updateOtStatusDisplay('stale');
258
+ return { ok: true, skipped: true, reason: pause.reason || 'paused' };
259
+ }
260
+ updateOtStatusDisplay('starting');
261
+ } else {
262
+ clearOtEventPolling({ clearPatchQueue: true });
263
+ }
264
+ const response = enabled
265
+ ? await callPageBridge('startOtObserver', { projectId })
266
+ : await callPageBridge('stopOtObserver', {});
267
+ if (!isCurrentOtSync(requestId, projectId) || getCurrentProjectId() !== projectId) {
268
+ if (enabled) {
269
+ await handleStaleOtStartResponse(projectId, requestId);
270
+ }
271
+ return response;
272
+ }
273
+ if (!enabled) {
274
+ if (!isSuccessfulOtBridgeResponse(response)) {
275
+ updateOtStatusDisplay('unavailable');
276
+ return response;
277
+ }
278
+ updateOtStatusDisplay('off');
279
+ return response;
280
+ }
281
+ const status = readOtBridgeStatus(response);
282
+ if (!isSuccessfulOtBridgeResponse(response) || status === 'unavailable') {
283
+ handleFailedOtStart(projectId, requestId);
284
+ return response;
285
+ }
286
+ const pauseAfterStart = otWarmMirrorController?.shouldPauseOtWarmMirror?.({ running: Boolean(getCurrentRunView()) }) || { pause: false };
287
+ if (pauseAfterStart.pause) {
288
+ await pauseOtWarmMirror(pauseAfterStart.reason || 'paused');
289
+ return response;
290
+ }
291
+ updateOtStatusDisplay(status);
292
+ scheduleOtEventPolling(projectId, { immediate: true });
293
+ return response;
294
+ }
295
+
296
+ function isCurrentOtSync(requestId, projectId) {
297
+ return requestId === otSyncRequestId && projectId === otWarmMirrorProjectId;
298
+ }
299
+
300
+ function isSuccessfulOtBridgeResponse(response) {
301
+ return Boolean(response) && response.ok !== false;
302
+ }
303
+
304
+ async function handleStaleOtStartResponse(projectId, requestId) {
305
+ if (isCurrentOtSync(requestId, projectId) && getCurrentProjectId() === projectId) {
306
+ return;
307
+ }
308
+ clearOtEventPolling({ clearPatchQueue: true });
309
+ await callPageBridge('stopOtObserver', {});
310
+ if (isExperimentalOtEnabled()) {
311
+ await syncOtWarmMirrorController();
312
+ }
313
+ }
314
+
315
+ function handleFailedOtStart(projectId, requestId) {
316
+ if (!isCurrentOtSync(requestId, projectId) || getCurrentProjectId() !== projectId) {
317
+ return;
318
+ }
319
+ clearOtEventPolling({ clearPatchQueue: true });
320
+ setExperimentalOtEnabledForProject(projectId, false);
321
+ const experimentalOtCheckbox = getPanel()?.querySelector('[data-experimental-ot]');
322
+ if (experimentalOtCheckbox) {
323
+ experimentalOtCheckbox.checked = false;
324
+ }
325
+ updateOtStatusDisplay('unavailable');
326
+ saveStateSoon();
327
+ }
328
+
329
+ function readOtBridgeStatus(response) {
330
+ if (!response || response.ok === false) {
331
+ return 'unavailable';
332
+ }
333
+ return response.getState() || response.status || response.result?.getState() || response.result?.status || 'unavailable';
334
+ }
335
+
336
+ function ensureOtWarmMirrorStateProject(projectId) {
337
+ if (!projectId || otWarmMirrorState.projectId === projectId) {
338
+ return;
339
+ }
340
+ clearOtEventPolling({ clearPatchQueue: true });
341
+ otWarmMirrorState.projectId = projectId;
342
+ otWarmMirrorState.lastPatchAt = 0;
343
+ otWarmMirrorState.lastErrorCode = '';
344
+ otWarmMirrorState.lastStatus = currentOtStatus;
345
+ }
346
+
347
+ function clearOtEventPolling(options = {}) {
348
+ if (otWarmMirrorState.pollTimer) {
349
+ window.clearTimeout(otWarmMirrorState.pollTimer);
350
+ otWarmMirrorState.pollTimer = null;
351
+ }
352
+ if (otWarmMirrorState.flushTimer) {
353
+ window.clearTimeout(otWarmMirrorState.flushTimer);
354
+ otWarmMirrorState.flushTimer = null;
355
+ }
356
+ if (options.clearPatchQueue !== false) {
357
+ otWarmMirrorState.patchQueue = [];
358
+ }
359
+ }
360
+
361
+ function scheduleOtEventPolling(projectId = getCurrentProjectId(), options = {}) {
362
+ if (!canPollOtWarmMirror(projectId)) {
363
+ return;
364
+ }
365
+ ensureOtWarmMirrorStateProject(projectId);
366
+ if (otWarmMirrorState.pollTimer) {
367
+ window.clearTimeout(otWarmMirrorState.pollTimer);
368
+ }
369
+ const delayMs = options.immediate
370
+ ? 0
371
+ : Math.max(0, Number(otWarmMirrorController?.OT_POLL_INTERVAL_MS) || 1000);
372
+ otWarmMirrorState.pollTimer = window.setTimeout(() => {
373
+ otWarmMirrorState.pollTimer = null;
374
+ pollOtEvents(projectId).catch(error => {
375
+ otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'ot_poll_failed';
376
+ updateOtStatusDisplay('unavailable');
377
+ });
378
+ }, delayMs);
379
+ }
380
+
381
+ function canPollOtWarmMirror(projectId) {
382
+ return Boolean(projectId
383
+ && otWarmMirrorController?.shouldPauseOtWarmMirror
384
+ && otWarmMirrorController?.buildPatchFilesRequest
385
+ && isExperimentalOtEnabledForProject(projectId)
386
+ && otWarmMirrorProjectId === projectId
387
+ && getCurrentProjectId() === projectId);
388
+ }
389
+
390
+ async function pollOtEvents(projectId = otWarmMirrorState.projectId || getCurrentProjectId()) {
391
+ if (!canPollOtWarmMirror(projectId)) {
392
+ clearOtEventPolling({ clearPatchQueue: true });
393
+ return { ok: false, skipped: true, reason: 'not_current_project' };
394
+ }
395
+
396
+ const pause = otWarmMirrorController.shouldPauseOtWarmMirror({ running: Boolean(getCurrentRunView()) });
397
+ if (pause.pause) {
398
+ otWarmMirrorState.lastErrorCode = pause.reason || 'paused';
399
+ updateOtStatusDisplay('stale');
400
+ clearOtEventPolling({ clearPatchQueue: false });
401
+ return { ok: false, skipped: true, reason: pause.reason || 'paused' };
402
+ }
403
+
404
+ try {
405
+ const statusResponse = await callPageBridge('getOtStatus', { projectId });
406
+ if (!canPollOtWarmMirror(projectId)) {
407
+ return { ok: false, skipped: true, reason: 'project_changed' };
408
+ }
409
+ if (!isSuccessfulOtBridgeResponse(statusResponse)) {
410
+ otWarmMirrorState.lastErrorCode = readOtBridgeErrorCode(statusResponse, 'get_ot_status_failed');
411
+ updateOtStatusDisplay('unavailable');
412
+ return statusResponse;
413
+ }
414
+ const status = readOtBridgeStatus(statusResponse);
415
+ if (status) {
416
+ updateOtStatusDisplay(status);
417
+ }
418
+
419
+ const drainResponse = await callPageBridge('drainOtEvents', { projectId });
420
+ if (!canPollOtWarmMirror(projectId)) {
421
+ return { ok: false, skipped: true, reason: 'project_changed' };
422
+ }
423
+ if (!isSuccessfulOtBridgeResponse(drainResponse)) {
424
+ otWarmMirrorState.lastErrorCode = readOtBridgeErrorCode(drainResponse, 'drain_ot_events_failed');
425
+ updateOtStatusDisplay('unavailable');
426
+ return drainResponse;
427
+ }
428
+ const events = readOtBridgeEvents(drainResponse);
429
+ if (events.length) {
430
+ queueOtPatchEvents(events, projectId);
431
+ }
432
+ return drainResponse;
433
+ } catch (error) {
434
+ otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'ot_poll_failed';
435
+ updateOtStatusDisplay('unavailable');
436
+ return { ok: false, error };
437
+ } finally {
438
+ if (canPollOtWarmMirror(projectId) && !getCurrentRunView()) {
439
+ scheduleOtEventPolling(projectId);
440
+ }
441
+ }
442
+ }
443
+
444
+ function readOtBridgeEvents(response) {
445
+ if (Array.isArray(response)) {
446
+ return response;
447
+ }
448
+ if (Array.isArray(response?.events)) {
449
+ return response.events;
450
+ }
451
+ if (Array.isArray(response?.result?.events)) {
452
+ return response.result.events;
453
+ }
454
+ return [];
455
+ }
456
+
457
+ function readOtBridgeErrorCode(response, fallback) {
458
+ const value = response?.lastErrorCode
459
+ || response?.reason
460
+ || response?.error?.code
461
+ || response?.error
462
+ || fallback;
463
+ return typeof value === 'string' ? value : fallback;
464
+ }
465
+
466
+ function queueOtPatchEvents(events, projectId = getCurrentProjectId()) {
467
+ if (!Array.isArray(events) || !events.length || !canPollOtWarmMirror(projectId)) {
468
+ return;
469
+ }
470
+ ensureOtWarmMirrorStateProject(projectId);
471
+ otWarmMirrorState.patchQueue.push(...events);
472
+ scheduleOtPatchFlush(projectId);
473
+ }
474
+
475
+ function scheduleOtPatchFlush(projectId = otWarmMirrorState.projectId || getCurrentProjectId(), options = {}) {
476
+ if (!canPollOtWarmMirror(projectId) || !otWarmMirrorState.patchQueue.length) {
477
+ return;
478
+ }
479
+ if (otWarmMirrorState.flushTimer) {
480
+ window.clearTimeout(otWarmMirrorState.flushTimer);
481
+ }
482
+ const delayMs = options.immediate
483
+ ? 0
484
+ : Math.max(0, Number(otWarmMirrorController?.OT_PATCH_DEBOUNCE_MS) || 500);
485
+ otWarmMirrorState.flushTimer = window.setTimeout(() => {
486
+ otWarmMirrorState.flushTimer = null;
487
+ flushOtPatchBatch(projectId).catch(error => {
488
+ otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'ot_patch_failed';
489
+ updateOtStatusDisplay('inconsistent');
490
+ });
491
+ }, delayMs);
492
+ }
493
+
494
+ async function flushOtPatchBatch(projectId = otWarmMirrorState.projectId || getCurrentProjectId()) {
495
+ if (otWarmMirrorState.flushing) {
496
+ return { ok: false, skipped: true, reason: 'ot_patch_in_flight' };
497
+ }
498
+ if (!canPollOtWarmMirror(projectId)) {
499
+ clearOtEventPolling({ clearPatchQueue: true });
500
+ return { ok: false, skipped: true, reason: 'not_current_project' };
501
+ }
502
+
503
+ const pause = otWarmMirrorController.shouldPauseOtWarmMirror({ running: Boolean(getCurrentRunView()) });
504
+ if (pause.pause) {
505
+ otWarmMirrorState.lastErrorCode = pause.reason || 'paused';
506
+ updateOtStatusDisplay('stale');
507
+ clearOtEventPolling({ clearPatchQueue: false });
508
+ return { ok: false, skipped: true, reason: pause.reason || 'paused' };
509
+ }
510
+
511
+ const maxBatch = Math.max(1, Number(otWarmMirrorController?.OT_MAX_PATCH_BATCH) || 25);
512
+ const batch = otWarmMirrorState.patchQueue.splice(0, maxBatch);
513
+ if (!batch.length) {
514
+ return { ok: true, skipped: true, reason: 'empty_batch' };
515
+ }
516
+
517
+ otWarmMirrorState.flushing = true;
518
+ try {
519
+ const request = {
520
+ ...otWarmMirrorController.buildPatchFilesRequest({ projectId, events: batch }),
521
+ method: 'mirror.patchFiles'
522
+ };
523
+ if (!Array.isArray(request.params?.files) || request.params.files.length === 0) {
524
+ return { ok: true, skipped: true, reason: 'empty_filtered_batch' };
525
+ }
526
+ if (!canPollOtWarmMirror(projectId)) {
527
+ return { ok: false, skipped: true, reason: 'project_changed' };
528
+ }
529
+
530
+ const response = await sendBackgroundNative(request);
531
+ if (!response?.ok) {
532
+ otWarmMirrorState.lastErrorCode = response?.error?.code || response?.error?.message || 'mirror_patch_failed';
533
+ updateOtStatusDisplay('inconsistent');
534
+ return response;
535
+ }
536
+
537
+ const result = response.result || {};
538
+ const skippedFiles = Array.isArray(result?.skippedFiles) ? result.skippedFiles : [];
539
+ const skippedCount = Number.isFinite(Number(result?.skippedCount))
540
+ ? Number(result.skippedCount)
541
+ : skippedFiles.length;
542
+ if (skippedFiles.length || skippedCount > 0) {
543
+ otWarmMirrorState.lastErrorCode = 'mirror_patch_skipped';
544
+ updateOtStatusDisplay('inconsistent');
545
+ return response;
546
+ }
547
+ const appliedFiles = Array.isArray(result?.appliedFiles) ? result.appliedFiles : null;
548
+ const appliedCount = Number.isFinite(Number(result?.appliedCount))
549
+ ? Number(result.appliedCount)
550
+ : NaN;
551
+ if (!appliedFiles || !Number.isFinite(appliedCount) || appliedCount <= 0 || appliedFiles.length !== appliedCount) {
552
+ otWarmMirrorState.lastErrorCode = 'mirror_patch_invalid_result';
553
+ updateOtStatusDisplay('inconsistent');
554
+ return response;
555
+ }
556
+
557
+ otWarmMirrorState.lastPatchAt = Date.now();
558
+ otWarmMirrorState.lastErrorCode = '';
559
+ if (canPollOtWarmMirror(projectId)) {
560
+ updateOtStatusDisplay('observing');
561
+ }
562
+ return response;
563
+ } catch (error) {
564
+ otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'mirror_patch_failed';
565
+ updateOtStatusDisplay('inconsistent');
566
+ return { ok: false, error };
567
+ } finally {
568
+ otWarmMirrorState.flushing = false;
569
+ if (otWarmMirrorState.patchQueue.length && canPollOtWarmMirror(projectId) && !getCurrentRunView()) {
570
+ scheduleOtPatchFlush(projectId, { immediate: true });
571
+ }
572
+ }
573
+ }
574
+
575
+ async function pauseOtWarmMirror(reason = 'pause') {
576
+ const projectId = getCurrentProjectId();
577
+ clearOtEventPolling({ clearPatchQueue: true });
578
+ if (!isExperimentalOtEnabledForProject(projectId)) {
579
+ return { ok: true, skipped: true, reason: 'disabled' };
580
+ }
581
+ ensureOtWarmMirrorStateProject(projectId);
582
+ otWarmMirrorState.lastErrorCode = reason;
583
+ updateOtStatusDisplay('stale');
584
+ try {
585
+ const response = await callPageBridge('stopOtObserver', { projectId, reason });
586
+ if (!isSuccessfulOtBridgeResponse(response)) {
587
+ otWarmMirrorState.lastErrorCode = readOtBridgeErrorCode(response, 'pause_ot_observer_failed');
588
+ updateOtStatusDisplay('unavailable');
589
+ }
590
+ return response;
591
+ } catch (error) {
592
+ otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'pause_ot_observer_failed';
593
+ updateOtStatusDisplay('unavailable');
594
+ return { ok: false, error };
595
+ }
596
+ }
597
+
598
+ async function resumeOtWarmMirror(reason = 'resume') {
599
+ const projectId = getCurrentProjectId();
600
+ if (!isExperimentalOtEnabledForProject(projectId)) {
601
+ return { ok: true, skipped: true, reason: 'disabled' };
602
+ }
603
+ try {
604
+ return await syncOtWarmMirrorController({ reason });
605
+ } catch (error) {
606
+ otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'resume_ot_observer_failed';
607
+ updateOtStatusDisplay('unavailable');
608
+ return { ok: false, error };
609
+ }
610
+ }
611
+
612
+ function updateOtStatusDisplay(status = currentOtStatus) {
613
+ const normalized = normalizeOtStatus(status);
614
+ currentOtStatus = normalized;
615
+ otWarmMirrorState.lastStatus = normalized;
616
+ const statusElement = getPanel()?.querySelector('[data-ot-status]');
617
+ if (statusElement) {
618
+ const label = formatOtStatusLabel(normalized);
619
+ statusElement.textContent = formatOtToggleStatusText(label);
620
+ statusElement.dataset.otStatus = normalized;
621
+ statusElement.title = formatOtToggleStatusText(label);
622
+ }
623
+ updateExperimentalOtMenuStatus();
624
+ updateProbeStatusOtSuffix();
625
+ }
626
+
627
+ function formatOtStatusLabel(status) {
628
+ const labels = {
629
+ off: 'otStatusOff',
630
+ starting: 'otStatusStarting',
631
+ observing: 'otStatusObserving',
632
+ unavailable: 'otStatusUnavailable',
633
+ stale: 'otStatusStale',
634
+ inconsistent: 'otStatusInconsistent'
635
+ };
636
+ return tr(labels[normalizeOtStatus(status)] || 'otStatusUnavailable');
637
+ }
638
+
639
+ function formatOtToggleStatusText(label) {
640
+ return `OT: ${label}`;
641
+ }
642
+
643
+ function updateExperimentalOtMenuStatus() {
644
+ const statusElement = getPanel()?.querySelector('[data-experimental-ot-menu-status]');
645
+ if (!statusElement) {
646
+ return;
647
+ }
648
+ statusElement.textContent = formatOtToggleStatusText(formatOtStatusLabel(currentOtStatus));
649
+ }
650
+
651
+ function normalizeOtStatus(status) {
652
+ const value = typeof status === 'string' ? status : '';
653
+ return ['off', 'starting', 'observing', 'unavailable', 'stale', 'inconsistent'].includes(value)
654
+ ? value
655
+ : 'unavailable';
656
+ }
657
+
658
+ function scheduleMirrorPrefetch(options = {}) {
659
+ syncMirrorPrefetchStateForProject();
660
+ if (!mirrorHealth?.shouldStartPrefetch) {
661
+ return;
662
+ }
663
+ const delayMs = Math.max(0, Number(options.delayMs) || 0);
664
+ if (mirrorPrefetchState.timer) {
665
+ window.clearTimeout(mirrorPrefetchState.timer);
666
+ mirrorPrefetchState.timer = null;
667
+ }
668
+ mirrorPrefetchState.timer = window.setTimeout(() => {
669
+ mirrorPrefetchState.timer = null;
670
+ syncMirrorPrefetch({ reason: options.reason || 'deferred' }).catch(error => {
671
+ mirrorPrefetchState.lastErrorAt = Date.now();
672
+ mirrorPrefetchState.lastError = error?.message || String(error);
673
+ });
674
+ }, delayMs);
675
+ }
676
+
677
+ function isExpectedPrefetchSkip(errorOrResponse) {
678
+ const code = errorOrResponse?.error?.code || errorOrResponse?.code || '';
679
+ const message = errorOrResponse?.error?.message || errorOrResponse?.message || String(errorOrResponse || '');
680
+ return code === 'project_locked'
681
+ || code === 'project_changed'
682
+ || /project_locked|project changed|project_changed|navigation/i.test(message);
683
+ }
684
+
685
+ async function syncMirrorPrefetch(options = {}) {
686
+ syncMirrorPrefetchStateForProject();
687
+ const projectId = getCurrentProjectId();
688
+ const now = Date.now();
689
+ const readiness = mirrorHealth?.shouldStartPrefetch?.({
690
+ ...mirrorPrefetchState,
691
+ busy: Boolean(getCurrentRunView()),
692
+ now
693
+ }) || { ok: false, reason: 'unavailable' };
694
+ if (!readiness.ok) {
695
+ return { ok: false, skipped: true, reason: readiness.reason };
696
+ }
697
+
698
+ mirrorPrefetchState.projectId = projectId;
699
+ mirrorPrefetchState.lastAttemptAt = now;
700
+ const inFlight = (async () => {
701
+ const project = sanitizeRunProjectSnapshot(await callPageBridge('getProjectSnapshot', {
702
+ force: false,
703
+ maxAgeMs: 30000,
704
+ preferLightweight: false,
705
+ allowZipFallback: true,
706
+ allowEditorNavigation: false,
707
+ requireFullProject: true,
708
+ includeBinaryFiles: true,
709
+ zipOnly: true,
710
+ zipTimeoutMs: RUN_SNAPSHOT_ZIP_TIMEOUT_MS
711
+ }));
712
+ if (getCurrentProjectId() !== projectId) {
713
+ return { ok: false, skipped: true, reason: 'project_changed' };
714
+ }
715
+ if (project?.capabilities?.fullProjectSnapshot !== true) {
716
+ return { ok: false, skipped: true, reason: 'not_full_project' };
717
+ }
718
+ const response = await sendBackgroundNative({
719
+ method: 'mirror.sync',
720
+ params: {
721
+ projectId,
722
+ project,
723
+ reason: options.reason || 'prefetch'
724
+ }
725
+ });
726
+ if (!response?.ok) {
727
+ if (isExpectedPrefetchSkip(response)) {
728
+ return { ok: false, skipped: true, reason: response?.error?.code || 'expected_busy' };
729
+ }
730
+ throw new Error(response?.error?.message || 'mirror.sync failed');
731
+ }
732
+ if (getCurrentProjectId() !== projectId) {
733
+ return { ok: false, skipped: true, reason: 'project_changed' };
734
+ }
735
+ mirrorPrefetchState.lastSuccessAt = Date.now();
736
+ mirrorPrefetchState.lastError = null;
737
+ return { ok: true };
738
+ })();
739
+
740
+ mirrorPrefetchState.inFlight = inFlight;
741
+ try {
742
+ return await inFlight;
743
+ } catch (error) {
744
+ if (isExpectedPrefetchSkip(error)) {
745
+ return { ok: false, skipped: true, reason: error?.code || error?.message || 'expected_busy' };
746
+ }
747
+ mirrorPrefetchState.lastErrorAt = Date.now();
748
+ mirrorPrefetchState.lastError = error?.message || String(error);
749
+ return { ok: false, error };
750
+ } finally {
751
+ if (mirrorPrefetchState.inFlight === inFlight) {
752
+ mirrorPrefetchState.inFlight = null;
753
+ }
754
+ }
755
+ }
756
+
757
+ async function settleMirrorPrefetchBeforeRun() {
758
+ syncMirrorPrefetchStateForProject();
759
+ if (mirrorPrefetchState.timer) {
760
+ window.clearTimeout(mirrorPrefetchState.timer);
761
+ mirrorPrefetchState.timer = null;
762
+ }
763
+ if (!mirrorPrefetchState.inFlight) {
764
+ return;
765
+ }
766
+ try {
767
+ await mirrorPrefetchState.inFlight;
768
+ } catch (error) {
769
+ if (!isExpectedPrefetchSkip(error)) {
770
+ mirrorPrefetchState.lastErrorAt = Date.now();
771
+ mirrorPrefetchState.lastError = error?.message || String(error);
772
+ }
773
+ }
774
+ }
775
+
776
+ function isMirrorReusable(mirrorStatus) {
777
+ const health = mirrorHealth?.classifyMirrorHealth?.(mirrorStatus) || { reusable: false };
778
+ return health.reusable === true;
779
+ }
780
+
781
+ async function resolveWarmRunStart(taskContext = {}) {
782
+ const focusFiles = taskContext.focusFiles || [];
783
+ const mode = taskContext.mode || getState()?.mode;
784
+ const mirrorStatus = await getMirrorFreshness();
785
+ const otWarmStart = otWarmMirrorController.canUseOtWarmStart({
786
+ enabled: isExperimentalOtEnabled(),
787
+ focusFiles,
788
+ mirrorStatus
789
+ });
790
+ if (otWarmStart.ok) {
791
+ return {
792
+ useExistingMirror: true,
793
+ warmStart: true,
794
+ otWarmStart: true,
795
+ reason: 'ot_focus_fresh',
796
+ mirrorStatus,
797
+ focusFiles: otWarmStart.focusFiles,
798
+ project: {
799
+ capabilities: {
800
+ fullProjectSnapshot: false,
801
+ method: 'ot-warm-mirror'
802
+ },
803
+ files: []
804
+ }
805
+ };
806
+ }
807
+ if (!mirrorStatus?.exists || !isMirrorReusable(mirrorStatus)) {
808
+ return { useExistingMirror: false, reason: 'mirror_not_fresh', mirrorStatus };
809
+ }
810
+
811
+ let overlayProject = null;
812
+ try {
813
+ overlayProject = await callPageBridge('getProjectSnapshot', {
814
+ force: true,
815
+ maxAgeMs: 0,
816
+ preferLightweight: true,
817
+ allowZipFallback: false,
818
+ allowEditorNavigation: false,
819
+ restrictToRequestedPathsOnly: true,
820
+ requireFullProject: false,
821
+ includeBinaryFiles: false,
822
+ focusFiles
823
+ });
824
+ } catch (error) {
825
+ return { useExistingMirror: false, reason: 'overlay_probe_failed', mirrorStatus };
826
+ }
827
+ const fileOverlays = buildSnapshotFileOverlays(overlayProject, focusFiles);
828
+ if (!fileOverlays.length) {
829
+ return { useExistingMirror: false, reason: 'missing_current_overlay', mirrorStatus };
830
+ }
831
+ if (focusFiles.length && !focusFiles.every(path => {
832
+ const normalized = normalizeSnapshotPath(path);
833
+ return fileOverlays.some(file => normalizeSnapshotPath(file.path) === normalized);
834
+ })) {
835
+ return { useExistingMirror: false, reason: 'missing_focus_overlay', mirrorStatus };
836
+ }
837
+
838
+ return {
839
+ useExistingMirror: true,
840
+ warmStart: true,
841
+ reason: mode === 'ask' ? 'warm_whole_project_ask' : 'warm_whole_project',
842
+ mirrorStatus,
843
+ fileOverlays,
844
+ project: overlayProject
845
+ };
846
+ }
847
+
848
+ async function prepareMirrorStaleRetry({ project = {}, focusFiles = [] } = {}) {
849
+ throwIfRunCancellationRequested();
850
+ appendLog(formatProjectSnapshotUserLog(project));
851
+ const snapshotWarnings = getProjectSnapshotWarnings(project);
852
+ const focusedPartialSnapshot = runController.canUseFocusedPartialSnapshot({
853
+ project,
854
+ snapshotWarnings,
855
+ focusFiles,
856
+ isUsableProjectFileContent: window.CodexOverleafProjectFiles.isUsableProjectFileContent
857
+ });
858
+ if (snapshotWarnings.blocking.length && !focusedPartialSnapshot) {
859
+ for (const warning of snapshotWarnings.blocking) {
860
+ appendLog(tx(`Cannot continue: ${formatProjectSnapshotWarning(warning)}`, `无法继续:${formatProjectSnapshotWarning(warning)}`));
861
+ }
862
+ appendCompletionReport({
863
+ conclusion: tr('warmMirrorStaleBlockedConclusion'),
864
+ status: 'blocked',
865
+ operations: [],
866
+ applyResults: [],
867
+ nextStep: tr('warmMirrorStaleBlockedNextStep')
868
+ });
869
+ finishRunView(tr('warmMirrorStaleBlockedTitle'), 'failed');
870
+ return { ok: false };
871
+ }
872
+ if (focusedPartialSnapshot) {
873
+ appendRunEvent({
874
+ title: tx(
875
+ `Only selected context files were read: ${focusFiles.join(', ')}. This retry will read and write only these files.`,
876
+ `只读到你选择的上下文文件:${focusFiles.join(', ')};本次重试将只基于这些文件运行和写回。`
877
+ ),
878
+ status: 'completed'
879
+ });
880
+ }
881
+ return {
882
+ ok: true,
883
+ project,
884
+ restrictToFocusFiles: runController.shouldRestrictWritebackToFocus({ focusFiles })
885
+ };
886
+ }
887
+
888
+ function syncMirrorPrefetchStateForProject() {
889
+ const projectId = getCurrentProjectId();
890
+ const projectChanged = Boolean(mirrorPrefetchState.projectId && mirrorPrefetchState.projectId !== projectId);
891
+ syncCustomInstructionsEditorForProject(projectId, { force: projectChanged });
892
+ syncOtWarmMirrorStateForProject();
893
+ if (!mirrorPrefetchState.projectId || !projectChanged) {
894
+ return;
895
+ }
896
+ if (mirrorPrefetchState.timer) {
897
+ window.clearTimeout(mirrorPrefetchState.timer);
898
+ }
899
+ mirrorPrefetchState = {
900
+ inFlight: mirrorPrefetchState.inFlight,
901
+ lastSuccessAt: 0,
902
+ lastErrorAt: 0,
903
+ lastError: null,
904
+ timer: null,
905
+ projectId
906
+ };
907
+ }
908
+
909
+ function syncOtWarmMirrorStateForProject() {
910
+ const projectId = getCurrentProjectId();
911
+ syncExperimentalOtToggleForProject(projectId);
912
+ if (!otWarmMirrorProjectId) {
913
+ otWarmMirrorProjectId = projectId;
914
+ return;
915
+ }
916
+ if (otWarmMirrorProjectId === projectId) {
917
+ return;
918
+ }
919
+ clearOtEventPolling({ clearPatchQueue: true });
920
+ otWarmMirrorProjectId = projectId;
921
+ otSyncRequestId++;
922
+ callPageBridge('stopOtObserver', {}).then(response => {
923
+ if (!isSuccessfulOtBridgeResponse(response)) {
924
+ updateOtStatusDisplay('unavailable');
925
+ }
926
+ return syncOtWarmMirrorController();
927
+ }).catch(error => {
928
+ updateOtStatusDisplay('unavailable');
929
+ appendPlainLog(tx(`Experimental OT warm mirror unavailable: ${error.message}`, `实验性 OT 预热镜像不可用:${error.message}`));
930
+ });
931
+ }
932
+
933
+ return {
934
+ getCurrentOtStatus,
935
+ getOtWarmMirrorState,
936
+ getLastExperimentalOtProjectId,
937
+ clearMirrorPrefetchTimer,
938
+ releaseOtWarmMirrorProject,
939
+ isExperimentalOtEnabled,
940
+ setExperimentalOtEnabledForProject,
941
+ normalizeExperimentalOtByProject,
942
+ syncExperimentalOtToggleForProject,
943
+ handleExperimentalOtToggleClick,
944
+ handleExperimentalOtToggleKeydown,
945
+ handleExperimentalOtToggleChange,
946
+ syncOtWarmMirrorController,
947
+ readOtBridgeStatus,
948
+ clearOtEventPolling,
949
+ canPollOtWarmMirror,
950
+ pauseOtWarmMirror,
951
+ resumeOtWarmMirror,
952
+ updateOtStatusDisplay,
953
+ formatOtStatusLabel,
954
+ updateExperimentalOtMenuStatus,
955
+ normalizeOtStatus,
956
+ scheduleMirrorPrefetch,
957
+ settleMirrorPrefetchBeforeRun,
958
+ resolveWarmRunStart,
959
+ prepareMirrorStaleRetry
960
+ };
961
+ }
962
+
963
+ window.CodexOverleafOtWarmMirror = { create };
964
+ })();