codex-overleaf-link 2.3.6 → 2.4.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 (41) hide show
  1. package/README.md +59 -34
  2. package/README.zh-CN.md +543 -0
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +3 -0
  5. package/extension/src/content/activeTurnControl.js +1 -0
  6. package/extension/src/content/assetTransferBroker.js +2 -0
  7. package/extension/src/content/contentRuntime.js +53 -50
  8. package/extension/src/content/contextTray.js +4 -0
  9. package/extension/src/content/diagnosticsController.js +4 -0
  10. package/extension/src/content/diagnosticsPanel.js +128 -4
  11. package/extension/src/content/generated/content.bundle.js +583 -112
  12. package/extension/src/content/generated/content.bundle.meta.json +4 -3
  13. package/extension/src/content/legacyGlobalRegistry.js +1 -0
  14. package/extension/src/content/modelPicker.js +21 -9
  15. package/extension/src/content/modelPickerSupport.js +38 -1
  16. package/extension/src/content/nativeCompatibilityController.js +4 -1
  17. package/extension/src/content/otWarmMirror.js +34 -30
  18. package/extension/src/content/otWarmMirrorController.js +20 -0
  19. package/extension/src/content/pageBridgeClient.js +2 -0
  20. package/extension/src/content/runResultActions.js +16 -1
  21. package/extension/src/content/runTimelineView.js +1 -1
  22. package/extension/src/content/sessionForkController.js +20 -11
  23. package/extension/src/content/trackedChangeCaptureController.js +153 -0
  24. package/extension/src/content/writebackOrchestrator.js +31 -0
  25. package/extension/src/page/binaryAssetUploader.js +101 -29
  26. package/extension/src/page/overleafRealtimeObserver.js +4 -1
  27. package/extension/src/page/textFileCreator.js +493 -0
  28. package/extension/src/page/trackedChangeCapture.js +342 -0
  29. package/extension/src/page/trackedChangesLifecycle.js +145 -149
  30. package/extension/src/page/treeOperations.js +30 -2
  31. package/extension/src/page/writebackRouter.js +94 -48
  32. package/extension/src/pageBridge.js +62 -25
  33. package/extension/src/shared/compatibility.js +1 -1
  34. package/extension/src/shared/pageRpcContract.js +2 -1
  35. package/extension/src/shared/sessionState.js +6 -2
  36. package/extension/src/shared/storageDb.js +10 -12
  37. package/extension/src/shared/storageRunActions.js +29 -1
  38. package/extension/src/shared/writebackSettlement.js +19 -5
  39. package/extension/styles/panel.css +55 -10
  40. package/native-host/src/taskRunnerRuntime.js +25 -0
  41. package/package.json +5 -1
@@ -51,8 +51,7 @@
51
51
  'codex.providers.delete',
52
52
  'task.run',
53
53
  'mirror.sync',
54
- 'mirror.patchFiles',
55
- 'mirror.confirmWriteback',
54
+ 'mirror.patchFiles', 'mirror.confirmWriteback', 'mirror.invalidate',
56
55
  'mirror.scanSensitive',
57
56
  'codex.history.clearPlugin',
58
57
  'skills.list',
@@ -583,6 +582,7 @@
583
582
  RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
584
583
  getState: () => state,
585
584
  getCurrentRunView: () => currentRunView,
585
+ appendRunRecordEvent,
586
586
  onMirrorRefreshSettled: runSettlementPersistence.settleMirrorRefresh
587
587
  });
588
588
  const {
@@ -790,6 +790,7 @@
790
790
  },
791
791
  saveState,
792
792
  saveStateSoon,
793
+ onProjectFilesReady: refreshHistoricalProjectReferences,
793
794
  updateActiveSession,
794
795
  callPageBridge,
795
796
  getCurrentProjectId,
@@ -1776,9 +1777,7 @@
1776
1777
  return contextTrayController.setContextStatus(text);
1777
1778
  }
1778
1779
 
1779
- function resetContextProject() {
1780
- return contextTrayController.resetContextProject();
1781
- }
1780
+ function resetContextProject() { return contextTrayController.resetContextProject(); }
1782
1781
 
1783
1782
  async function runTask(options = {}) {
1784
1783
  // Barrier on a still-flying background mirror refresh from the previous
@@ -2485,14 +2484,15 @@
2485
2484
  setRunning(false);
2486
2485
  nativeChannel.clearActiveRequest();
2487
2486
  stopRunElapsedTick();
2488
- if (isExperimentalOtEnabled()) {
2489
- await resumeOtWarmMirror('run-settled');
2490
- }
2491
2487
  await flushQueuedSaveState().catch(() => {});
2492
2488
  currentRunView = null;
2493
2489
  runCancellationRequested = false;
2494
2490
  runCancellationController = null;
2495
2491
  activeTurnControl.release(settlement.requestId);
2492
+ if (isExperimentalOtEnabled()) {
2493
+ try { await resumeOtWarmMirror('run-settled'); }
2494
+ catch (_error) { updateOtStatusDisplay('unavailable'); }
2495
+ }
2496
2496
  if (settlement.requestId) {
2497
2497
  activeTurnControl.acknowledge(settlement.requestId).catch(() => {});
2498
2498
  }
@@ -5570,6 +5570,7 @@
5570
5570
  title: session.title || '',
5571
5571
  titleSource: session.titleSource || 'auto',
5572
5572
  focusFiles: session.focusFiles || [],
5573
+ projectReferenceFiles: session.projectReferenceFiles || [],
5573
5574
  codexThreadId: session.codexThreadId || '',
5574
5575
  createdAt: session.createdAt,
5575
5576
  updatedAt: session.updatedAt,
@@ -5977,15 +5978,7 @@
5977
5978
  }
5978
5979
 
5979
5980
  function getRenderedModelEntries() {
5980
- return Array.from(panel?.querySelector('[data-model]')?.options || []).map(option => ({
5981
- id: option.value,
5982
- label: option.textContent,
5983
- reasoningEfforts: (option.dataset.reasoningEfforts || '').split(',').filter(Boolean),
5984
- defaultReasoningEffort: option.dataset.defaultReasoningEffort || '',
5985
- reasoningPresentation: option.dataset.reasoningPresentation || '',
5986
- speedTiers: (option.dataset.speedTiers || 'standard').split(',').filter(Boolean),
5987
- defaultSpeedTier: option.dataset.defaultSpeedTier || 'standard'
5988
- }));
5981
+ return Modules.ModelPickerSupport.getRenderedModelEntries(panel);
5989
5982
  }
5990
5983
 
5991
5984
  async function selectMode(mode) {
@@ -6123,6 +6116,7 @@
6123
6116
  applySessionLabel();
6124
6117
  renderSessionList();
6125
6118
  renderRunHistory(options);
6119
+ getTrackedChangeCaptureController().schedule();
6126
6120
  renderContextSelection();
6127
6121
  renderContextSummary();
6128
6122
  // v1.8.0 C4: refresh recovery — small attachments persisted in state are
@@ -6212,7 +6206,7 @@
6212
6206
  undoExpectedFiles: [],
6213
6207
  undoStatus: '',
6214
6208
  queueItemId,
6215
- nativeRequestId: '',
6209
+ nativeRequestId: '', codexThreadId: '',
6216
6210
  codexTurnId: '',
6217
6211
  nativeEventSeq: 0,
6218
6212
  executionSnapshot: executionSnapshot
@@ -6379,7 +6373,7 @@
6379
6373
  currentRunView.activeTurn = { threadId, turnId };
6380
6374
  const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
6381
6375
  if (record) {
6382
- record.codexTurnId = turnId;
6376
+ record.codexThreadId = threadId; record.codexTurnId = turnId;
6383
6377
  record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(journalSeq || 0));
6384
6378
  }
6385
6379
  if (threadId) {
@@ -6753,6 +6747,13 @@
6753
6747
  return files;
6754
6748
  }
6755
6749
 
6750
+ function refreshHistoricalProjectReferences(project) {
6751
+ if (!project?.id || String(project.id) !== getCurrentProjectId()) return;
6752
+ state = updateActiveSession(state, { projectReferenceFiles: captureProjectReferenceFiles(project) });
6753
+ saveStateSoon();
6754
+ if (!currentRunView && !trackedChangeInFlight.size) renderRunHistory({ preserveScroll: true });
6755
+ }
6756
+
6756
6757
  function persistCurrentProjectReferenceFiles(project) {
6757
6758
  const projectFiles = captureProjectReferenceFiles(project);
6758
6759
  if (currentRunView) {
@@ -6884,6 +6885,7 @@
6884
6885
  runProjectId: getRunProjectIdForWriteback(run)
6885
6886
  });
6886
6887
  const undoApplied = isUndoResultEffectivelyApplied(run, result);
6888
+ writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
6887
6889
  appendUndoReviewingPolicyEvent(runId, result.reviewingPolicy);
6888
6890
  appendRunRecordEvent(runId, {
6889
6891
  title: tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
@@ -6946,11 +6948,16 @@
6946
6948
  });
6947
6949
 
6948
6950
  let result;
6951
+ let trackedUndoPostFilesAtDispatch;
6949
6952
  try {
6953
+ trackedUndoPostFilesAtDispatch = buildTrackedUndoPostFiles(run);
6950
6954
  result = await callPageBridge('rejectTrackedChanges', {
6951
6955
  trackedChanges: run.undoTrackedChanges || [],
6952
6956
  expectedFiles: run.undoExpectedFiles || [],
6953
- postFiles: buildTrackedUndoPostFiles(run),
6957
+ postFiles: trackedUndoPostFilesAtDispatch,
6958
+ untrackedUndo: run.executionSnapshot
6959
+ ? run.executionSnapshot.requireReviewing === false && ['submitted', 'legacy-captured'].includes(run.executionSnapshot.source)
6960
+ : getTrackedChangeCaptureController().hasLegacyUntrackedCheckpoint(run),
6954
6961
  // Welcome-panel + write-guard:
6955
6962
  // bind the reject to the run's original project. If the user has
6956
6963
  // navigated away the page-side guard refuses with
@@ -6962,6 +6969,7 @@
6962
6969
  trackedChangeInFlight.delete(runId);
6963
6970
  }
6964
6971
  }
6972
+ writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
6965
6973
  appendRunRecordEvent(runId, {
6966
6974
  title: trackedUndo
6967
6975
  ? tr('undoTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 })
@@ -6981,7 +6989,8 @@
6981
6989
  });
6982
6990
  if (lifecycleReject) {
6983
6991
  WritebackSettlement.attachUndoNotVerifiedFailure(run, result, {
6984
- buildFailure: buildContentFailure
6992
+ buildFailure: buildContentFailure,
6993
+ postFiles: trackedUndoPostFilesAtDispatch
6985
6994
  });
6986
6995
  applyTrackedChangeSettlement(runId, 'reject', result);
6987
6996
  return;
@@ -7195,6 +7204,25 @@
7195
7204
  return WritebackSettlement.attachVerifiedContentToOperation(operation, result);
7196
7205
  }
7197
7206
 
7207
+ let trackedChangeCaptureController = null;
7208
+ function getTrackedChangeCaptureController() {
7209
+ return trackedChangeCaptureController ||= Modules.TrackedChangeCaptureController.create({
7210
+ getRuns: () => (state.sessions || []).flatMap(session => session.runs || []),
7211
+ getProjectId: () => window.location.pathname.match(/^\/project\/([^/]+)/)?.[1] || '',
7212
+ buildExpectedFilesAfterOperations, normalizeTrackedChanges: normalizeApplyTrackedChanges, callPageBridge,
7213
+ persist: () => flushQueuedSaveState({ preserveRunActionPayload: true }),
7214
+ refresh: run => refreshRunCardControls(run.id),
7215
+ notify: (run, kind, detail) => appendRunRecordEvent(run.id, {
7216
+ title: {
7217
+ pending: tx('Files written; confirming tracked changes.', '文件已写入,正在确认留痕记录。'),
7218
+ observed: tx('Tracked changes confirmed; Accept is available.', '留痕记录已确认,可以接受改动。'),
7219
+ needs_review: tx('Tracked changes could not be safely attributed. Review them in Overleaf; Undo is preserved.', '留痕归属尚未确认,请在 Overleaf 审阅;原有撤销能力已保留。')
7220
+ }[kind], status: 'info', detail
7221
+ }),
7222
+ onError: error => appendPlainLog(tx('Tracked-change confirmation unavailable: ', '留痕确认暂不可用:') + error.message)
7223
+ });
7224
+ }
7225
+
7198
7226
  function recordUndoFromApply(project, applyResult) {
7199
7227
  const appliedEntries = getAppliedEntries(applyResult);
7200
7228
  if (!currentRunView?.recordId || !appliedEntries.length) {
@@ -7238,6 +7266,7 @@
7238
7266
  if (combinedTrackedChanges.length) {
7239
7267
  record.trackedChangeStatus = 'pending';
7240
7268
  }
7269
+ getTrackedChangeCaptureController().record(record, applyResult?.trackedChangeCaptures);
7241
7270
  refreshRunCardControls(record.id);
7242
7271
  if (combinedTrackedChanges.length) {
7243
7272
  appendRunEvent({
@@ -7302,38 +7331,12 @@
7302
7331
  return WritebackSettlement.normalizeApplyTrackedChanges(changes);
7303
7332
  }
7304
7333
 
7305
- function selectExpectedFilesForTrackedUndo(
7306
- project,
7307
- operations = [],
7308
- trackedChanges = [],
7309
- previousExpectedFiles = []
7310
- ) {
7311
- return WritebackSettlement.selectExpectedFilesForTrackedUndo(
7312
- project,
7313
- operations,
7314
- trackedChanges,
7315
- previousExpectedFiles
7316
- );
7334
+ function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = [], previousExpectedFiles = []) {
7335
+ return WritebackSettlement.selectExpectedFilesForTrackedUndo(project, operations, trackedChanges, previousExpectedFiles);
7317
7336
  }
7318
7337
 
7319
7338
  function buildTrackedUndoPostFiles(run) {
7320
- const expectedFiles = Array.isArray(run?.undoExpectedFiles) ? run.undoExpectedFiles : [];
7321
- const appliedOperations = Array.isArray(run?.appliedOperations) ? run.appliedOperations : [];
7322
- if (!expectedFiles.length || !appliedOperations.length) {
7323
- return [];
7324
- }
7325
-
7326
- const postFilesByPath = buildExpectedFilesAfterOperations(
7327
- { files: expectedFiles },
7328
- appliedOperations
7329
- );
7330
- const expectedPaths = new Set(expectedFiles.map(file => file?.path).filter(Boolean));
7331
- return Array.from(postFilesByPath.entries())
7332
- .filter(([path, content]) => expectedPaths.has(path) && typeof content === 'string')
7333
- .map(([path, content]) => ({
7334
- path,
7335
- content
7336
- }));
7339
+ return getTrackedChangeCaptureController().buildPostFiles(run);
7337
7340
  }
7338
7341
 
7339
7342
  function hasTrackedEditorUndo(run) {
@@ -22,6 +22,7 @@
22
22
  setState = () => {},
23
23
  saveState = null,
24
24
  saveStateSoon = () => {},
25
+ onProjectFilesReady = () => {},
25
26
  updateActiveSession = defaultUpdateActiveSession,
26
27
  callPageBridge = () => Promise.resolve({ ok: false }),
27
28
  getCurrentProjectId = () => '',
@@ -201,6 +202,7 @@
201
202
  renderContextFiles(contextProject);
202
203
  if (isExactContextFileListProject(contextProject)) {
203
204
  setContextSyncState('exact-ready');
205
+ onProjectFilesReady(contextProject);
204
206
  } else if (contextSyncState !== 'partial') {
205
207
  setContextSyncState('exact-loading');
206
208
  void enhanceContextFilesFromExactSnapshot({ loadId: contextLoadId });
@@ -224,6 +226,7 @@
224
226
  contextProject = project;
225
227
  setContextSyncState(isExactContextFileListProject(project) ? 'exact-ready' : 'exact-loading');
226
228
  renderContextFiles(project);
229
+ if (isExactContextFileListProject(project)) onProjectFilesReady(project);
227
230
  if (!isExactContextFileListProject(project)) {
228
231
  void enhanceContextFilesFromExactSnapshot({
229
232
  force: Boolean(options.force),
@@ -292,6 +295,7 @@
292
295
  contextProject = project;
293
296
  setContextSyncState('exact-ready');
294
297
  renderContextFiles(project);
298
+ onProjectFilesReady(project);
295
299
  } catch (_error) {
296
300
  // The page file tree remains usable when the optional exact ZIP list is unavailable.
297
301
  if (loadId === contextLoadId) {
@@ -641,6 +641,10 @@
641
641
  lines.push(`CodeMirror: docLength=${view.docLength || 0}; dispatch=${Boolean(view.hasDispatch)}; source=${view.source || 'unknown'}`);
642
642
  }
643
643
  }
644
+ const capture = probe?.trackedChangeCapture;
645
+ if (capture) lines.push(tx('Tracked ledger: ', '留痕记录:')
646
+ + 'source=' + capture.source + '; ready=' + capture.ready + '; count=' + capture.count
647
+ + '; doc=' + (capture.docId || 'none') + '; adapter=' + capture.adapter + '; reason=' + (capture.reason || 'none'));
644
648
  const projectDiagnostics = probe?.projectDiagnostics;
645
649
  if (projectDiagnostics) {
646
650
  lines.push(`Project records: ${projectDiagnostics.docRecordCount || 0}; roots=${(projectDiagnostics.internalRootKeys || []).slice(0, 6).join(', ') || 'none'}`);
@@ -10,7 +10,8 @@
10
10
  container,
11
11
  callbacks: options.callbacks || {},
12
12
  i18n: options.i18n || {},
13
- dismissInstalled: false
13
+ dismissInstalled: false,
14
+ cleanup: []
14
15
  };
15
16
 
16
17
  container.innerHTML = `
@@ -99,6 +100,11 @@
99
100
  instance.callbacks.onBeforeOpen?.();
100
101
  }
101
102
  popover.hidden = !open;
103
+ if (open) {
104
+ openFloating(instance, popover, 300);
105
+ } else {
106
+ hideFloating(popover);
107
+ }
102
108
  button.dataset.active = open ? 'true' : 'false';
103
109
  button.setAttribute('aria-expanded', open ? 'true' : 'false');
104
110
  }
@@ -110,7 +116,7 @@
110
116
  if (!popover || !button) {
111
117
  return;
112
118
  }
113
- popover.hidden = true;
119
+ hideFloating(popover);
114
120
  button.dataset.active = 'false';
115
121
  button.setAttribute('aria-expanded', 'false');
116
122
  }
@@ -119,7 +125,7 @@
119
125
  const instance = target?._instance || target;
120
126
  const result = instance?.container?.querySelector('[data-diagnostics-result]');
121
127
  if (result) {
122
- result.hidden = true;
128
+ hideFloating(result);
123
129
  }
124
130
  }
125
131
 
@@ -181,6 +187,73 @@
181
187
  details.open = false;
182
188
  details.hidden = !technicalText;
183
189
  technical.textContent = technicalText;
190
+ closeMenu(instance);
191
+ openFloating(instance, root, 560);
192
+ }
193
+
194
+ // Keep DOM ownership (theme, locale and callbacks), but render above clipping
195
+ // ancestors where the browser supports the top layer. Fixed positioning is
196
+ // the viewport-based fallback for browsers without the Popover API.
197
+ function openFloating(instance, element, preferredWidth) {
198
+ if (element.isConnected === false) return;
199
+ element.hidden = false;
200
+ if (typeof element.showPopover === 'function') {
201
+ element.setAttribute('popover', 'manual');
202
+ try {
203
+ if (!element.matches(':popover-open')) element.showPopover();
204
+ } catch (_error) {
205
+ element.removeAttribute('popover');
206
+ }
207
+ }
208
+ positionFloating(instance, element, preferredWidth);
209
+ }
210
+
211
+ function hideFloating(element) {
212
+ if (!element) return;
213
+ if (typeof element.hidePopover === 'function' && element.getAttribute('popover') === 'manual') {
214
+ try {
215
+ if (element.matches(':popover-open')) element.hidePopover();
216
+ } catch (_error) {
217
+ element.removeAttribute('popover');
218
+ }
219
+ }
220
+ element.hidden = true;
221
+ }
222
+
223
+ function positionFloating(instance, element, preferredWidth) {
224
+ if (!element || element.hidden) return;
225
+ const button = instance.container.querySelector('[data-diagnostics-menu]');
226
+ if (!button?.getBoundingClientRect || !element.getBoundingClientRect) return;
227
+ if (button.getClientRects && !button.getClientRects().length) {
228
+ closeMenu(instance);
229
+ closeResult(instance);
230
+ return;
231
+ }
232
+ const doc = instance.container.ownerDocument || document;
233
+ const view = doc.defaultView || window;
234
+ const viewport = view.visualViewport;
235
+ const width = viewport?.width || view.innerWidth || doc.documentElement?.clientWidth;
236
+ const height = viewport?.height || view.innerHeight || doc.documentElement?.clientHeight;
237
+ if (!(width > 0 && height > 0)) return;
238
+ const margin = 12;
239
+ const gap = 8;
240
+ const leftEdge = (viewport?.offsetLeft || 0) + margin;
241
+ const topEdge = (viewport?.offsetTop || 0) + margin;
242
+ const rightEdge = leftEdge + Math.max(0, width - margin * 2);
243
+ const bottomEdge = topEdge + Math.max(0, height - margin * 2);
244
+ element.style.width = `${Math.min(preferredWidth, Math.max(0, width - margin * 2))}px`;
245
+ element.style.maxHeight = `${Math.min(640, Math.max(0, height - margin * 2))}px`;
246
+ const anchor = button.getBoundingClientRect();
247
+ const bounds = element.getBoundingClientRect();
248
+ const below = Math.max(0, bottomEdge - anchor.bottom - gap);
249
+ const above = Math.max(0, anchor.top - topEdge - gap);
250
+ const desiredTop = bounds.height > below && above > below
251
+ ? anchor.top - gap - bounds.height
252
+ : anchor.bottom + gap;
253
+ element.style.left = `${Math.max(leftEdge, Math.min(anchor.right - bounds.width, rightEdge - bounds.width))}px`;
254
+ element.style.top = `${Math.max(topEdge, Math.min(desiredTop, bottomEdge - bounds.height))}px`;
255
+ element.style.right = 'auto';
256
+ element.style.bottom = 'auto';
184
257
  }
185
258
 
186
259
  function renderInstallCommand(instance, container, command) {
@@ -272,13 +345,60 @@
272
345
  return;
273
346
  }
274
347
  instance.dismissInstalled = true;
275
- document.addEventListener('click', event => {
348
+ const doc = instance.container.ownerDocument || document;
349
+ const view = doc.defaultView || window;
350
+ const listen = (target, type, handler, options) => {
351
+ if (!target?.addEventListener) return;
352
+ target.addEventListener(type, handler, options);
353
+ instance.cleanup.push(() => target.removeEventListener(type, handler, options));
354
+ };
355
+ listen(doc, 'click', event => {
276
356
  const wrap = instance.container?.querySelector('.codex-diagnostics-wrap');
277
357
  if (!wrap || wrap.contains(event.target)) {
278
358
  return;
279
359
  }
280
360
  closeMenu(instance);
281
361
  }, true);
362
+ listen(doc, 'keydown', event => {
363
+ if (event.key !== 'Escape') return;
364
+ const menu = instance.container.querySelector('[data-diagnostics-popover]');
365
+ const result = instance.container.querySelector('[data-diagnostics-result]');
366
+ if (menu && !menu.hidden) closeMenu(instance);
367
+ else if (result && !result.hidden) closeResult(instance);
368
+ else return;
369
+ event.preventDefault();
370
+ event.stopPropagation();
371
+ instance.container.querySelector('[data-diagnostics-menu]')?.focus();
372
+ }, true);
373
+ let frame = null;
374
+ const reposition = () => {
375
+ frame = null;
376
+ positionFloating(instance, instance.container.querySelector('[data-diagnostics-popover]'), 300);
377
+ positionFloating(instance, instance.container.querySelector('[data-diagnostics-result]'), 560);
378
+ };
379
+ const schedule = () => {
380
+ if (frame !== null) return;
381
+ if (view.requestAnimationFrame) frame = view.requestAnimationFrame(reposition);
382
+ else reposition();
383
+ };
384
+ listen(view, 'resize', schedule);
385
+ listen(doc, 'scroll', schedule, true);
386
+ listen(view.visualViewport, 'resize', schedule);
387
+ listen(view.visualViewport, 'scroll', schedule);
388
+ if (typeof view.ResizeObserver === 'function') {
389
+ const observer = new view.ResizeObserver(schedule);
390
+ for (const node of [
391
+ instance.container.closest('#codex-overleaf-panel') || instance.container,
392
+ instance.container.querySelector('[data-diagnostics-popover]'),
393
+ instance.container.querySelector('[data-diagnostics-result]')
394
+ ]) {
395
+ if (node) observer.observe(node);
396
+ }
397
+ instance.cleanup.push(() => observer.disconnect());
398
+ }
399
+ instance.cleanup.push(() => {
400
+ if (frame !== null) view.cancelAnimationFrame?.(frame);
401
+ });
282
402
  }
283
403
 
284
404
  // Drive the trigger's health dot. `health` is one of ok / warn / fail /
@@ -450,6 +570,10 @@
450
570
  }
451
571
 
452
572
  function destroy(instance) {
573
+ closeMenu(instance);
574
+ closeResult(instance);
575
+ for (const cleanup of instance.cleanup || []) cleanup();
576
+ instance.cleanup = [];
453
577
  instance.container.textContent = '';
454
578
  }
455
579