codex-overleaf-link 2.5.1 → 2.6.5

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 (111) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +148 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +46 -10
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +1114 -321
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsController.js +77 -34
  14. package/extension/src/content/diagnosticsExport.js +1 -1
  15. package/extension/src/content/diagnosticsPanel.js +147 -42
  16. package/extension/src/content/generated/content.bundle.js +5761 -1237
  17. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  18. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  19. package/extension/src/content/localSkillsPanel.js +6 -4
  20. package/extension/src/content/markdownDomRenderer.js +5 -3
  21. package/extension/src/content/markdownText.js +53 -14
  22. package/extension/src/content/modelPicker.js +29 -19
  23. package/extension/src/content/nativeCompatibilityController.js +52 -19
  24. package/extension/src/content/otWarmMirror.js +1 -1
  25. package/extension/src/content/pageBridgeClient.js +7 -7
  26. package/extension/src/content/panelMaintenance.js +10 -2
  27. package/extension/src/content/panelRenderer.js +55 -28
  28. package/extension/src/content/pendingInputView.js +1 -1
  29. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  30. package/extension/src/content/providerSettingsDialog.js +28 -19
  31. package/extension/src/content/recentProjects.js +30 -19
  32. package/extension/src/content/runActivitySummary.js +122 -14
  33. package/extension/src/content/runCompletionMeta.js +194 -6
  34. package/extension/src/content/runController.js +2 -1
  35. package/extension/src/content/runFailureNotice.js +8 -1
  36. package/extension/src/content/runPresence.js +18 -8
  37. package/extension/src/content/runQueueScheduler.js +27 -13
  38. package/extension/src/content/runRecoveryActions.js +2 -1
  39. package/extension/src/content/runResultActions.js +73 -24
  40. package/extension/src/content/runReviewController.js +102 -37
  41. package/extension/src/content/runTimelineView.js +224 -25
  42. package/extension/src/content/scopedPersistencePanelState.js +124 -46
  43. package/extension/src/content/selectionContextView.js +30 -3
  44. package/extension/src/content/sensitivePreflight.js +14 -1
  45. package/extension/src/content/sessionManager.js +49 -3
  46. package/extension/src/content/sessionMenuView.js +54 -18
  47. package/extension/src/content/sessionPersistence.js +25 -5
  48. package/extension/src/content/settingsPanel.js +15 -2
  49. package/extension/src/content/settingsWorkbench.js +127 -89
  50. package/extension/src/content/sharedSessionViewSync.js +13 -4
  51. package/extension/src/content/subagentActivityView.js +23 -7
  52. package/extension/src/content/updateNotice.js +60 -6
  53. package/extension/src/content/writebackOrchestrator.js +301 -72
  54. package/extension/src/content/writingStyleSettings.js +70 -17
  55. package/extension/src/page/compileBridge.js +193 -180
  56. package/extension/src/page/overleafCapabilities.js +4 -1
  57. package/extension/src/page/overleafEditor.js +12 -3
  58. package/extension/src/page/overleafProjectSnapshot.js +56 -3
  59. package/extension/src/page/snapshotRouter.js +75 -66
  60. package/extension/src/page/textCoordinates.js +17 -1
  61. package/extension/src/page/textFileCreator.js +9 -1
  62. package/extension/src/page/treeOperations.js +200 -19
  63. package/extension/src/page/writebackRouter.js +172 -113
  64. package/extension/src/pageBridge.js +124 -73
  65. package/extension/src/popup.js +1 -1
  66. package/extension/src/shared/agentTranscript.js +372 -18
  67. package/extension/src/shared/auditRecords.js +3 -0
  68. package/extension/src/shared/compatibility.js +1 -1
  69. package/extension/src/shared/compileAdapter.js +58 -17
  70. package/extension/src/shared/composerAttachmentDraft.js +27 -7
  71. package/extension/src/shared/contextUsage.js +125 -0
  72. package/extension/src/shared/failureReasons.js +57 -3
  73. package/extension/src/shared/i18n.js +315 -29
  74. package/extension/src/shared/lineReferences.js +38 -31
  75. package/extension/src/shared/pageRpcContract.js +1 -1
  76. package/extension/src/shared/panelIcons.js +39 -0
  77. package/extension/src/shared/projectFiles.js +2 -1
  78. package/extension/src/shared/reviewing.js +130 -1
  79. package/extension/src/shared/runActivityModel.js +70 -4
  80. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  81. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
  82. package/extension/src/shared/sessionState.js +63 -4
  83. package/extension/src/shared/sessionTitle.js +14 -1
  84. package/extension/src/shared/sharedSessionBridge.js +81 -13
  85. package/extension/src/shared/staleGuard.js +130 -63
  86. package/extension/src/shared/storageDb.js +35 -3
  87. package/extension/src/shared/storageMigration.js +111 -2
  88. package/extension/src/shared/storageRunActions.js +62 -8
  89. package/extension/src/shared/undoOperations.js +126 -28
  90. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  91. package/extension/src/shared/writebackSettlement.js +21 -3
  92. package/extension/styles/panel.css +1293 -549
  93. package/native-host/src/bootstrapUpdate.js +52 -16
  94. package/native-host/src/chatBridgeRequest.js +60 -6
  95. package/native-host/src/chatBridgeResponse.js +10 -1
  96. package/native-host/src/chatCompletionsBridge.js +29 -2
  97. package/native-host/src/codexPromptAssembly.js +86 -118
  98. package/native-host/src/codexSessionRunner.js +127 -17
  99. package/native-host/src/debugLog.js +27 -3
  100. package/native-host/src/index.js +42 -11
  101. package/native-host/src/localSkills.js +1 -1
  102. package/native-host/src/mirrorWorkspace.js +47 -3
  103. package/native-host/src/nativeEnvironment.js +27 -5
  104. package/native-host/src/providerContextError.js +20 -0
  105. package/native-host/src/providerRuntime.js +1 -0
  106. package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
  107. package/native-host/src/taskRunnerRuntime.js +28 -8
  108. package/native-host/src/textPatch.js +473 -377
  109. package/native-host/src/updateManager.js +144 -21
  110. package/native-host/src/writingStyleRuntime.js +1 -1
  111. package/package.json +5 -8
@@ -227,20 +227,20 @@
227
227
 
228
228
  function collectMarkdownTargetReferences(text, mode, refs) {
229
229
  const normalizedText = stripReferenceQueryAndHash(String(text || '').trim());
230
- const match = normalizedText.match(new RegExp(`^(.+?\\.${TEXT_EXTENSION_PATTERN}):(\\d+)(?::(\\d+))?$`, 'i'));
230
+ const match = normalizedText.match(new RegExp(`^(.+?\\.${TEXT_EXTENSION_PATTERN}):(\\d+)(?:[-–](\\d+)|:(\\d+))?$`, 'i'));
231
231
  if (!match) {
232
232
  return;
233
233
  }
234
- const line = parsePositiveInteger(match[2]);
235
- const column = match[3] ? parsePositiveInteger(match[3]) : null;
236
- if (!line || (match[3] && !column)) {
234
+ const span = parseLineSpan(match[2], match[3]);
235
+ const column = match[4] ? parsePositiveInteger(match[4]) : null;
236
+ if (!span || (match[4] && !column)) {
237
237
  return;
238
238
  }
239
239
  refs.push({
240
240
  rawText: text,
241
241
  displayText: text,
242
242
  rawPath: match[1],
243
- line,
243
+ ...span,
244
244
  column,
245
245
  source: mode,
246
246
  index: 0
@@ -248,12 +248,14 @@
248
248
  }
249
249
 
250
250
  function collectColonReferences(text, mode, refs) {
251
- const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}:(\\d+)(?::(\\d+))?(?![-:\\d])`, 'gi');
251
+ // Range/column suffixes are mutually exclusive; malformed spans must not
252
+ // backtrack into a shorter reference, including decimal/exponent endpoints.
253
+ const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}:(\\d+)(?:[-–](\\d+)(?!\\.\\d|e[+-]?\\d)|:(\\d+))?(?![-–:\\d]|(?:\\.\\d+)?(?:e[+-]?\\d+)?[-–])`, 'gi');
252
254
  let match;
253
255
  while ((match = pattern.exec(text)) !== null) {
254
- const line = parsePositiveInteger(match[3]);
255
- const column = match[4] ? parsePositiveInteger(match[4]) : null;
256
- if (!line || (match[4] && !column)) {
256
+ const span = parseLineSpan(match[3], match[4]);
257
+ const column = match[5] ? parsePositiveInteger(match[5]) : null;
258
+ if (!span || (match[5] && !column)) {
257
259
  continue;
258
260
  }
259
261
  const prefixLength = match[1].length;
@@ -262,7 +264,7 @@
262
264
  rawText,
263
265
  displayText: rawText,
264
266
  rawPath: match[2],
265
- line,
267
+ ...span,
266
268
  column,
267
269
  source: mode,
268
270
  index: match.index + prefixLength
@@ -271,20 +273,20 @@
271
273
  }
272
274
 
273
275
  function collectLineWordReferences(text, mode, refs) {
274
- const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}\\s+line\\s+(\\d+)(?![-:\\d])`, 'gi');
276
+ const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}\\s+line(?:s(?=\\s+\\d+[-–]))?\\s+(\\d+)(?:[-–](\\d+)(?!\\.\\d|e[+-]?\\d))?(?![-–:\\d]|(?:\\.\\d+)?(?:e[+-]?\\d+)?[-–])`, 'gi');
275
277
  let match;
276
278
  while ((match = pattern.exec(text)) !== null) {
277
- const line = parsePositiveInteger(match[3]);
278
- if (!line) {
279
+ const span = parseLineSpan(match[3], match[4]);
280
+ if (!span) {
279
281
  continue;
280
282
  }
281
283
  const prefixLength = match[1].length;
282
284
  const rawText = match[0].slice(prefixLength);
283
285
  refs.push({
284
286
  rawText,
285
- displayText: `${match[2]}:${line}`,
287
+ displayText: `${match[2]}:${span.line}${span.endLine ? `–${span.endLine}` : ''}`,
286
288
  rawPath: match[2],
287
- line,
289
+ ...span,
288
290
  column: null,
289
291
  source: mode,
290
292
  index: match.index + prefixLength
@@ -293,20 +295,20 @@
293
295
  }
294
296
 
295
297
  function collectChineseLineReferences(text, mode, refs) {
296
- const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}\\s+第\\s*(\\d+)\\s*行`, 'gi');
298
+ const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}\\s+第\\s*(\\d+)(?:\\s*[-–至]\\s*(\\d+))?\\s*行`, 'gi');
297
299
  let match;
298
300
  while ((match = pattern.exec(text)) !== null) {
299
- const line = parsePositiveInteger(match[3]);
300
- if (!line) {
301
+ const span = parseLineSpan(match[3], match[4]);
302
+ if (!span) {
301
303
  continue;
302
304
  }
303
305
  const prefixLength = match[1].length;
304
306
  const rawText = match[0].slice(prefixLength);
305
307
  refs.push({
306
308
  rawText,
307
- displayText: `${match[2]}:${line}`,
309
+ displayText: `${match[2]}:${span.line}${span.endLine ? `–${span.endLine}` : ''}`,
308
310
  rawPath: match[2],
309
- line,
311
+ ...span,
310
312
  column: null,
311
313
  source: mode,
312
314
  index: match.index + prefixLength
@@ -361,13 +363,13 @@
361
363
  projectFiles
362
364
  });
363
365
  if (resolved) {
364
- return `${resolved.path}:${ref.line}${ref.column ? `:${ref.column}` : ''}`;
366
+ return `${resolved.path}:${ref.line}${ref.endLine ? `-${ref.endLine}` : ref.column ? `:${ref.column}` : ''}`;
365
367
  }
366
- return formatLocalPathPlaceholder(ref.line, placeholderBrackets);
368
+ return formatLocalPathPlaceholder(ref.line, placeholderBrackets, ref.endLine);
367
369
  }
368
370
 
369
- function formatLocalPathPlaceholder(line, includeBrackets) {
370
- const value = line ? `local path:${line}` : 'local path';
371
+ function formatLocalPathPlaceholder(line, includeBrackets, endLine) {
372
+ const value = line ? `local path:${line}${endLine ? `-${endLine}` : ''}` : 'local path';
371
373
  return includeBrackets ? `[${value}]` : value;
372
374
  }
373
375
 
@@ -383,7 +385,7 @@
383
385
  }
384
386
 
385
387
  function stripReferenceQueryAndHash(value) {
386
- return String(value || '').replace(/([:.]\d+(?::\d+)?)(?:[?#].*)$/, '$1');
388
+ return String(value || '').replace(/([:.]\d+(?:[-–]\d+|:\d+)?)(?:[?#].*)$/, '$1');
387
389
  }
388
390
 
389
391
  function collectTextProjectFiles(projectFiles) {
@@ -423,18 +425,13 @@
423
425
  function shouldSkipReference(ref) {
424
426
  const rawPath = String(ref.rawPath || '');
425
427
  return /^https?:\/\//i.test(rawPath)
426
- || isEmailLikePath(rawPath)
427
- || hasUnsafeLineRange(ref.rawText);
428
+ || isEmailLikePath(rawPath);
428
429
  }
429
430
 
430
431
  function isEmailLikePath(rawPath) {
431
432
  return /^[^@\s/\\]+@[^@\s/\\]+$/.test(rawPath);
432
433
  }
433
434
 
434
- function hasUnsafeLineRange(rawText) {
435
- return /:\d+-\d+(?=$|[^\d])/.test(rawText);
436
- }
437
-
438
435
  function hasUnsafePathSegments(path) {
439
436
  return normalizeReferencePath(path)
440
437
  .split('/')
@@ -455,6 +452,15 @@
455
452
  return parsed;
456
453
  }
457
454
 
455
+ function parseLineSpan(rawLine, rawEndLine) {
456
+ const line = parsePositiveInteger(rawLine);
457
+ const endLine = rawEndLine === undefined ? null : parsePositiveInteger(rawEndLine);
458
+ if (!line || (rawEndLine !== undefined && (!endLine || endLine < line))) {
459
+ return null;
460
+ }
461
+ return { line, ...(endLine > line ? { endLine } : {}) };
462
+ }
463
+
458
464
  function getBasename(path) {
459
465
  const parts = String(path || '').split('/');
460
466
  return parts[parts.length - 1] || '';
@@ -477,6 +483,7 @@
477
483
  displayText: ref.displayText,
478
484
  rawPath: ref.rawPath,
479
485
  line: ref.line,
486
+ ...(ref.endLine ? { endLine: ref.endLine } : {}),
480
487
  column: ref.column,
481
488
  source: ref.source
482
489
  };
@@ -5,7 +5,7 @@
5
5
  })(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this), function () {
6
6
  'use strict';
7
7
 
8
- var REVISION = '2026-10-05-scope-correction-v42';
8
+ var REVISION = '2026-10-29-identity-v54';
9
9
  var METHODS = freezeCatalog({
10
10
  initializeCapability: entry('bridge.initialize', 'control', 'none', 'default', false, 'none'),
11
11
  probe: entry('bridge.probe', 'read', 'none', 'default', false, 'safe'),
@@ -0,0 +1,39 @@
1
+ (function initPanelIcons(root, factory) {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory();
3
+ else root.CodexOverleafModuleRegistry.define('PanelIcons', [], factory);
4
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function panelIconsFactory() {
5
+ 'use strict';
6
+
7
+ // 16-unit line icons drawn in the panel header's codexIcon style. Size and
8
+ // color come from the caller; `.codex-glyph` supplies stroke and caps.
9
+ const PATHS = Object.freeze({
10
+ plus: '<path d="M8 3.2v9.6M3.2 8h9.6"/>',
11
+ send: '<path d="M8 12.8V3.6"/><path d="m4.2 7.4 3.8-3.8 3.8 3.8"/>',
12
+ stop: '<rect x="4.4" y="4.4" width="7.2" height="7.2" rx="1.6" fill="currentColor" stroke="none"/>',
13
+ attach: '<path d="m12.6 7.4-4.8 4.8a3 3 0 0 1-4.2-4.2l5-5a2 2 0 0 1 2.8 2.8l-4.9 4.9a1 1 0 0 1-1.4-1.4l4.4-4.4"/>',
14
+ refresh: '<path d="M4.2 5.3A5 5 0 1 1 3.6 10"/><path d="M4.2 2.2v3.1H1.1"/>',
15
+ sparkle: '<path d="M7.4 2.6c.5 2.6 1.8 3.9 4.4 4.4-2.6.5-3.9 1.8-4.4 4.4C6.9 8.8 5.6 7.5 3 7c2.6-.5 3.9-1.8 4.4-4.4Z"/><path d="M12.3 10.5c.2 1 .7 1.5 1.7 1.7-1 .2-1.5.7-1.7 1.7-.2-1-.7-1.5-1.7-1.7 1-.2 1.5-.7 1.7-1.7Z"/>',
16
+ close: '<path d="m4.6 4.6 6.8 6.8M11.4 4.6l-6.8 6.8"/>',
17
+ pencil: '<path d="M10.4 3.1 12.9 5.6 6 12.5l-3.1.6.6-3.1Z"/><path d="m9.2 4.3 2.5 2.5"/>',
18
+ chevronDown: '<path d="m4.4 6.4 3.6 3.6 3.6-3.6"/>',
19
+ chevronLeft: '<path d="m9.8 3.6-4.4 4.4 4.4 4.4"/>',
20
+ lock: '<rect x="3.6" y="7.1" width="8.8" height="6.3" rx="1.5"/><path d="M5.6 7.1V5.3a2.4 2.4 0 0 1 4.8 0v1.8"/>',
21
+ compact: '<path d="M4.6 2.4 8 5.6l3.4-3.2"/><path d="M4.6 13.6 8 10.4l3.4 3.2"/><path d="M2.6 8h10.8"/>',
22
+ install: '<path d="M8 2.4v7.4"/><path d="m4.8 6.8 3.2 3.2 3.2-3.2"/><path d="M2.6 11v1.5c0 .6.4 1 1 1h8.8c.6 0 1-.4 1-1V11"/>',
23
+ book: '<path d="M8 4.3c-1.3-1.1-3-1.5-5.2-1.4v9.3c2.2-.1 3.9.3 5.2 1.4 1.3-1.1 3-1.5 5.2-1.4V2.9C11 2.8 9.3 3.2 8 4.3Z"/><path d="M8 4.3v9.3"/>',
24
+ terminal: '<rect x="2.1" y="2.9" width="11.8" height="10.2" rx="2"/><path d="m5 6.4 2 1.7-2 1.7"/><path d="M8.7 10h2.5"/>',
25
+ file: '<path d="M9.2 2.4H4.6a1 1 0 0 0-1 1v9.2a1 1 0 0 0 1 1h6.8a1 1 0 0 0 1-1V5.6Z"/><path d="M9.2 2.4v3.2h3.2"/>',
26
+ folder: '<path d="M2.2 4.4a1 1 0 0 1 1-1h3l1.4 1.6h5.2a1 1 0 0 1 1 1v6.4a1 1 0 0 1-1 1H3.2a1 1 0 0 1-1-1Z"/>',
27
+ alert: '<circle cx="8" cy="8" r="5.9"/><path d="M8 5v3.5"/><path d="M8 11h.01"/>',
28
+ spin: '<path d="M13.6 8A5.6 5.6 0 1 1 8 2.4"/>'
29
+ });
30
+
31
+ function svg(name, className = '') {
32
+ const paths = PATHS[name];
33
+ if (!paths) return '';
34
+ return '<svg class="codex-glyph' + (className ? ' ' + className : '') + '" viewBox="0 0 16 16" aria-hidden="true" focusable="false">'
35
+ + paths + '</svg>';
36
+ }
37
+
38
+ return { svg };
39
+ });
@@ -67,10 +67,11 @@
67
67
  continue;
68
68
  }
69
69
  seen.add(normalized);
70
- result.push(normalized);
71
70
  if (result.length >= limit) {
71
+ result.truncated = true;
72
72
  break;
73
73
  }
74
+ result.push(normalized);
74
75
  }
75
76
  return result;
76
77
  }
@@ -77,6 +77,135 @@
77
77
  };
78
78
  }
79
79
 
80
+ const UNAVAILABLE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
81
+ // Keep all upgrade-dialog selectors here: Overleaf DOM acceptance may recalibrate them.
82
+ const UPGRADE_MODAL_SELECTORS = Object.freeze({
83
+ body: '.upgrade-track-changes-modal',
84
+ modal: '.modal[role="dialog"], [role="dialog"][aria-modal="true"]',
85
+ close: '.modal-header button.btn-close, .modal-header button.close'
86
+ });
87
+
88
+ function visible(node) {
89
+ if (!node || !node.isConnected || node.hidden || node.getAttribute?.('aria-hidden') === 'true') return false;
90
+ const style = node.ownerDocument?.defaultView?.getComputedStyle(node);
91
+ return style?.display !== 'none' && style?.visibility !== 'hidden' && node.getClientRects().length > 0;
92
+ }
93
+
94
+ function findUpgradeModal(doc) {
95
+ for (const body of doc?.querySelectorAll(UPGRADE_MODAL_SELECTORS.body) || []) {
96
+ const modal = body.closest(UPGRADE_MODAL_SELECTORS.modal);
97
+ if (visible(body) && visible(modal)) return modal;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ async function dismissUpgradeModal(modal, { delay, assertCurrent }) {
103
+ const isOpen = () => visible(modal);
104
+ const waitClosed = async () => {
105
+ for (let i = 0; i < 6 && isOpen(); i++) { await delay(80); assertCurrent(); }
106
+ };
107
+ assertCurrent();
108
+ const close = modal.querySelector(UPGRADE_MODAL_SELECTORS.close);
109
+ if (visible(close) && !close.disabled) close.click();
110
+ await waitClosed();
111
+ if (isOpen()) {
112
+ assertCurrent();
113
+ const KeyboardEvent = modal.ownerDocument.defaultView.KeyboardEvent;
114
+ modal.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true }));
115
+ await waitClosed();
116
+ }
117
+ return !isOpen();
118
+ }
119
+
120
+ // Page owns state/identity; this controller owns the click/poll/upgrade stop sequence.
121
+ async function toggleMode(enabled, params, deps) {
122
+ const { assertCurrent, delay, getState, isConfirmed, click, findControls, findOption } = deps;
123
+ let attempted = false;
124
+ const inspect = async () => {
125
+ assertCurrent();
126
+ const modal = findUpgradeModal(deps.document);
127
+ if (enabled && modal) {
128
+ const evidence = { upgradeModal: true, dismissed: await dismissUpgradeModal(modal, deps) };
129
+ return { ok: false, code: 'reviewing_unavailable', attempted, evidence,
130
+ failure: deps.buildFailure('reviewing_unavailable', {
131
+ operationType: 'ensureReviewing', changedDocument: false, evidence
132
+ }) };
133
+ }
134
+ const state = getState();
135
+ return { ok: isConfirmed(enabled, state), attempted, reviewing: state.reviewing };
136
+ };
137
+ const poll = async waitMs => {
138
+ let result = await inspect();
139
+ for (let elapsed = 0; !result.ok && !result.code && elapsed < waitMs; elapsed += 120) {
140
+ await delay(120);
141
+ result = await inspect();
142
+ }
143
+ return result;
144
+ };
145
+ const waitMs = Number.isFinite(Number(params.waitMs)) ? Math.max(0, Number(params.waitMs)) : 1600;
146
+ const menuWaitMs = Math.min(waitMs, 1200);
147
+ const selectVisibleOption = async () => {
148
+ const option = findOption(enabled);
149
+ if (!option) return null;
150
+ assertCurrent(); click(option); attempted = true;
151
+ return poll(waitMs);
152
+ };
153
+ let result = await inspect();
154
+ if (result.ok || result.code) return result;
155
+ // An open menu must be consumed before a toggle can close it.
156
+ let selected = await selectVisibleOption();
157
+ if (selected) return selected;
158
+ for (const control of [...new Set(findControls().filter(Boolean))]) {
159
+ result = await inspect();
160
+ if (result.ok || result.code) return result;
161
+ selected = await selectVisibleOption();
162
+ if (selected) return selected;
163
+ if (control.getAttribute?.('aria-expanded') !== 'true') {
164
+ assertCurrent(); click(control); attempted = true;
165
+ }
166
+ // Menu mounting and target-mode confirmation have separate bounded waits.
167
+ for (let elapsed = 0; ; ) {
168
+ result = await inspect();
169
+ if (result.ok || result.code) return result;
170
+ selected = await selectVisibleOption();
171
+ if (selected) return selected;
172
+ if (elapsed >= menuWaitMs) break;
173
+ const step = Math.min(100, menuWaitMs - elapsed);
174
+ await delay(step);
175
+ elapsed += step;
176
+ }
177
+ }
178
+ return inspect();
179
+ }
180
+
181
+ function knownUnavailable(record, now = Date.now()) {
182
+ return record?.status === 'unavailable' && Number.isFinite(record.observedAt)
183
+ && now >= record.observedAt && now - record.observedAt < UNAVAILABLE_TTL_MS;
184
+ }
185
+
186
+ // Content preflight invokes this before dispatch. Only explicit choices can lower the policy.
187
+ async function resolveWritePolicy({ requireReviewing, knownUnavailable: unavailable }, deps) {
188
+ deps.assertCurrent();
189
+ let result = unavailable && requireReviewing
190
+ ? { ok: false, code: 'reviewing_unavailable', evidence: { upgradeModal: true, remembered: true } }
191
+ : await deps.ensureMode(requireReviewing);
192
+ deps.assertCurrent();
193
+ if (requireReviewing && result.ok) await deps.observe('available', 'reviewing_confirmed');
194
+ if (requireReviewing && result.code === 'reviewing_unavailable') {
195
+ if (!unavailable) await deps.observe('unavailable', 'upgrade_modal');
196
+ deps.assertCurrent();
197
+ const choice = await deps.choose(result);
198
+ deps.assertCurrent();
199
+ if (choice !== 'continue' && choice !== 'disable') return { ...result, ok: false };
200
+ await deps.applyUntracked(choice);
201
+ deps.assertCurrent();
202
+ result = await deps.ensureMode(false);
203
+ deps.assertCurrent();
204
+ return { ...result, requireReviewing: false, trackingDecision: choice };
205
+ }
206
+ return { ...result, requireReviewing };
207
+ }
208
+
80
209
  function matchesControlReview(text) {
81
210
  return CONTROL_REVIEW_PATTERNS.some(pattern => pattern.test(text));
82
211
  }
@@ -94,6 +223,6 @@
94
223
  }
95
224
 
96
225
  return {
97
- detectReviewingFromSignals
226
+ detectReviewingFromSignals, findUpgradeModal, toggleMode, knownUnavailable, resolveWritePolicy, UNAVAILABLE_TTL_MS
98
227
  };
99
228
  });
@@ -86,6 +86,27 @@
86
86
  written: Number.isSafeInteger(fileCount) ? fileCount : undefined, output: version }, sanitize);
87
87
  }
88
88
 
89
+ function contextCompactionState(event = {}) {
90
+ if (event.activity?.kind === 'lifecycle' && event.activity.target === 'context_compaction') {
91
+ return event.activity.state === 'running' ? 'running' : 'completed';
92
+ }
93
+ const raw = event.technicalDetail || (event.kind === 'technical' ? event.detail : null) || event;
94
+ const type = text(raw.type);
95
+ const method = text(raw.detail?.method || (type === 'codex.session.event' ? raw.title : ''));
96
+ if (raw.detail?.params?.item?.type === 'contextCompaction') {
97
+ if (method === 'item/started') return 'running';
98
+ if (method === 'item/completed') return 'completed';
99
+ }
100
+ // The legacy completion notification can carry an incorrectly inferred
101
+ // `running` status. The protocol method is authoritative in both versions.
102
+ if (method === 'thread/compacted') return 'completed';
103
+ const label = type + ' ' + method;
104
+ if (!/(compact|compaction|compacted)/i.test(label)
105
+ || !/(context|thread|turn|conversation|codex)/i.test(label)) return '';
106
+ if (/(completed|compacted|finished)/i.test(label)) return 'completed';
107
+ return raw.status === 'running' || /(started|starting|begin|prepar)/i.test(label) ? 'running' : 'completed';
108
+ }
109
+
89
110
  function capture(event = {}, sanitize = text) {
90
111
  if (event.activity?.v === 1) return normalize(event.activity, sanitize);
91
112
  if (event.kind === 'report' || event.kind === 'guidance' || event.kind === 'stream') return undefined;
@@ -103,6 +124,10 @@
103
124
  const scope = JSON.stringify([thread, turn]);
104
125
  const id = itemId ? JSON.stringify([thread, turn, itemId]) : '';
105
126
  const base = { id, scope, state: event.status };
127
+ const compactState = contextCompactionState(event);
128
+ if (compactState) {
129
+ return normalize({ ...base, kind: 'lifecycle', target: 'context_compaction', state: compactState }, sanitize);
130
+ }
106
131
  if (NOTICE_SOURCES.has(method)) {
107
132
  return normalize({ ...base, kind: 'notice', noticeSource: method,
108
133
  output: text(params.details), state: event.status === 'failed' ? 'failed' : 'warning' }, sanitize);
@@ -219,13 +244,31 @@
219
244
  && /(?:^|[\/\s"';&|])(?:latexmk|pdflatex|xelatex|lualatex|bibtex|biber)(?=\s|["']|$)/i.test(meta.command);
220
245
  }
221
246
 
247
+ // Read the completion watermark without building a full timeline for every
248
+ // older run when the composer refreshes its context indicator.
249
+ function getCompactedAt(run = {}) {
250
+ return (Array.isArray(run.events) ? run.events : []).reduce((latest, event) => {
251
+ if (!event || event.kind === 'report') return latest;
252
+ const meta = event.activity?.v === 1 ? event.activity : capture(event);
253
+ return meta?.kind === 'lifecycle' && meta.target === 'context_compaction' && meta.state === 'completed'
254
+ ? Math.max(latest, Date.parse(event.timestamp || '') || 0) : latest;
255
+ }, 0);
256
+ }
257
+
222
258
  function project(run = {}, incoming, options = {}) {
223
259
  const saved = Array.isArray(run.events) ? run.events : [];
224
- const events = incoming && !saved.includes(incoming) ? saved.concat(incoming) : saved;
260
+ // Stream updates are saved before rendering; buffering and localization copy
261
+ // their objects. Keep the current saved event instead of its incoming copy.
262
+ const incomingStreamKey = incoming?.kind === 'stream' ? text(incoming.streamKey) : '';
263
+ const alreadySaved = incoming && saved.some(event => event === incoming
264
+ || (incomingStreamKey && event?.kind === 'stream' && event.streamKey === incomingStreamKey));
265
+ const events = incoming && !alreadySaved ? saved.concat(incoming) : saved;
225
266
  const ordered = [];
226
267
  const diagnostics = [];
227
268
  const diagnosticNotices = new Set();
228
269
  const byId = new Map();
270
+ const compactions = new Map();
271
+ let compactedAt = 0;
229
272
  const report = events.filter(event => event?.kind === 'report').at(-1);
230
273
  const finalText = text(report?.detailStructured?.conclusion
231
274
  || report?.detail?.Conclusion || report?.detail?.['结论']).trim();
@@ -296,6 +339,23 @@
296
339
  }
297
340
  const meta = event.activity?.v === 1 ? event.activity : capture(event);
298
341
  clipped ||= meta?.clipped === true;
342
+ if (meta?.kind === 'lifecycle' && meta.target === 'context_compaction') {
343
+ startWork(event);
344
+ const identity = meta.id || meta.scope;
345
+ if (meta.state !== 'running') {
346
+ if (meta.state === 'completed') compactedAt = Math.max(compactedAt, Date.parse(event.timestamp || '') || 0);
347
+ compactions.set(identity, { scope: meta.scope, running: false });
348
+ // Legacy completions have no item id and close the scope's active item.
349
+ if (!meta.id) for (const entry of compactions.values()) {
350
+ if (entry.scope === meta.scope) entry.running = false;
351
+ }
352
+ } else if (!meta.id || !compactions.has(identity)) {
353
+ compactions.set(identity, { scope: meta.scope, running: true });
354
+ }
355
+ diagnostics.push({ key, event });
356
+ if (stage === 'preparing') stage = 'working';
357
+ return;
358
+ }
299
359
  if (isDiagnosticNotice(event, meta)) {
300
360
  const noticeKey = JSON.stringify([meta?.noticeSource || '', event.title]);
301
361
  if (!diagnosticNotices.has(noticeKey)) diagnostics.push({ key, event });
@@ -312,7 +372,10 @@
312
372
  } else if (!meta && /^(?:Note: |提示:)/.test(text(event.title))) addSetupStep('note', event);
313
373
  if (!meta || meta.kind === 'lifecycle' || event.kind === 'technical') {
314
374
  diagnostics.push({ key, event });
315
- if (meta?.kind === 'lifecycle') stage = meta.target;
375
+ if (meta?.kind === 'lifecycle') {
376
+ stage = meta.target;
377
+ if (stage === 'local_completed') compactions.clear();
378
+ }
316
379
  return;
317
380
  }
318
381
  const identity = meta.id ? 'item:' + meta.scope + ':' + meta.id : 'event:' + key;
@@ -354,8 +417,11 @@
354
417
  }
355
418
  const legacy = diagnostics.some(({ event }) => event.kind !== 'technical' && !event.technicalDetail && !event.activity);
356
419
  if (stage === 'local_completed') startWork({});
357
- return { blocks, diagnostics, notices, stage, setup, fileCount: files.size, clipped, hasReport: Boolean(report), legacy };
420
+ const compacting = run.status === 'running' && stage !== 'local_completed'
421
+ && [...compactions.values()].some(entry => entry.running);
422
+ // Latest completed compaction in this run, in epoch milliseconds; 0 when none completed.
423
+ return { blocks, diagnostics, notices, stage, compacting, compactedAt, setup, fileCount: files.size, clipped, hasReport: Boolean(report), legacy };
358
424
  }
359
425
 
360
- return { normalize, capture, project, normalizePhase: phase, isRoutineCancellationEvent };
426
+ return { normalize, capture, project, getCompactedAt, contextCompactionState, normalizePhase: phase, isRoutineCancellationEvent };
361
427
  });
@@ -132,6 +132,18 @@
132
132
  };
133
133
  }
134
134
 
135
+ // Commit the explicit preflight decision to both live and durable run consumers.
136
+ // The originally captured (possibly queued) snapshot stays immutable.
137
+ function applyUntrackedDecision(view, record, choice) {
138
+ if (!['continue', 'disable'].includes(choice) || !view?.recordId || record?.id !== view.recordId) {
139
+ throw Codec.snapshotError('invalid_reviewing_decision', 'An explicit decision for the current run is required.');
140
+ }
141
+ const snapshot = create({ ...snapshotOf(view), requireReviewing: false });
142
+ view.executionSnapshot = cloneSnapshot(snapshot);
143
+ Object.assign(record, toRunCompatibilityTuple(snapshot));
144
+ return snapshot;
145
+ }
146
+
135
147
  function projectForDisplay(value = {}) {
136
148
  const snapshot = snapshotOf(value);
137
149
  return {
@@ -174,6 +186,7 @@
174
186
  SCHEMA_VERSION: Codec.SCHEMA_VERSION,
175
187
  TUPLE_FIELDS: Codec.TUPLE_FIELDS,
176
188
  applyToState,
189
+ applyUntrackedDecision,
177
190
  capture,
178
191
  captureRawQueueTuple,
179
192
  cloneSnapshot,
@@ -12,6 +12,7 @@
12
12
  'use strict';
13
13
 
14
14
  var META_PREFIX = 'codex-overleaf-scoped-persistence-v1:';
15
+ var MIRROR_FENCE_PREFIX = 'codex-overleaf-mirror-reuse-fence-v1:';
15
16
  var fallbackMeta = new Map();
16
17
  var cleanPart = QueuePolicy.cleanPart;
17
18
  var normalizeMeta = QueuePolicy.normalizeMeta;
@@ -74,7 +75,40 @@
74
75
  };
75
76
  }
76
77
 
78
+ async function getMirrorReuseFences(scope, chromeApi) {
79
+ chromeApi = chromeApi || (typeof chrome !== 'undefined' ? chrome : null);
80
+ if (!normalizeScope(scope).projectId || !chromeApi?.storage?.local) throw new Error('Mirror fence storage is unavailable.');
81
+ var prefix = MIRROR_FENCE_PREFIX + scopeKey(scope).slice(META_PREFIX.length) + ':';
82
+ var stored = await chromeApi.storage.local.get(null);
83
+ return Object.keys(stored || {}).filter(key => key.startsWith(prefix)).map(key => {
84
+ var generation = key.slice(prefix.length);
85
+ if (!/^[a-zA-Z0-9_-]{1,160}$/.test(generation) || stored[key]?.v !== 1 || stored[key].generation !== generation) {
86
+ throw new Error('Mirror fence storage contains an unreadable generation.');
87
+ }
88
+ return generation;
89
+ }).sort();
90
+ }
91
+
92
+ async function setMirrorReuseFence(scope, generation, chromeApi) {
93
+ chromeApi = chromeApi || (typeof chrome !== 'undefined' ? chrome : null);
94
+ if (!normalizeScope(scope).projectId || !chromeApi?.storage?.local
95
+ || !/^[a-zA-Z0-9_-]{1,160}$/.test(generation)) throw new Error('Mirror fence storage is unavailable.');
96
+ var key = MIRROR_FENCE_PREFIX + scopeKey(scope).slice(META_PREFIX.length) + ':' + generation;
97
+ await chromeApi.storage.local.set({ [key]: { v: 1, generation } });
98
+ }
99
+
100
+ async function clearMirrorReuseFences(scope, generations, chromeApi) {
101
+ chromeApi = chromeApi || (typeof chrome !== 'undefined' ? chrome : null);
102
+ if (!normalizeScope(scope).projectId || !chromeApi?.storage?.local || !Array.isArray(generations)
103
+ || generations.some(generation => !/^[a-zA-Z0-9_-]{1,160}$/.test(generation))) throw new Error('Mirror fence storage is unavailable.');
104
+ var prefix = MIRROR_FENCE_PREFIX + scopeKey(scope).slice(META_PREFIX.length) + ':';
105
+ await chromeApi.storage.local.remove(generations.map(generation => prefix + generation));
106
+ }
107
+
77
108
  return Object.freeze({
109
+ getMirrorReuseFences: getMirrorReuseFences,
110
+ setMirrorReuseFence: setMirrorReuseFence,
111
+ clearMirrorReuseFences: clearMirrorReuseFences,
78
112
  createBrowserAdapter: createBrowserAdapter,
79
113
  normalizeScope: normalizeScope,
80
114
  scopeKey: scopeKey