codex-overleaf-link 2.3.5 → 2.3.6

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 (33) hide show
  1. package/README.md +31 -24
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/background.js +4 -0
  4. package/extension/src/backgroundUpdateCoordinator.js +88 -4
  5. package/extension/src/content/contentRuntime.js +24 -7
  6. package/extension/src/content/diagnosticsController.js +13 -3
  7. package/extension/src/content/diagnosticsPanel.js +1 -1
  8. package/extension/src/content/generated/content.bundle.js +848 -245
  9. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  10. package/extension/src/content/legacyGlobalRegistry.js +2 -0
  11. package/extension/src/content/otWarmMirror.js +95 -65
  12. package/extension/src/content/otWarmMirrorController.js +102 -1
  13. package/extension/src/content/panelMaintenance.js +26 -15
  14. package/extension/src/content/providerSettingsCoordinator.js +1 -0
  15. package/extension/src/content/runFailureNotice.js +104 -0
  16. package/extension/src/content/runScrollLayout.js +124 -0
  17. package/extension/src/content/runTimelineView.js +36 -10
  18. package/extension/src/content/settingsPanel.js +28 -11
  19. package/extension/src/content/updateNotice.js +24 -4
  20. package/extension/src/page/overleafRealtimeObserver.js +116 -24
  21. package/extension/src/shared/compatibility.js +1 -1
  22. package/extension/src/shared/i18n.js +77 -37
  23. package/extension/src/shared/managedUpdateProjection.js +1 -1
  24. package/extension/src/shared/otText.js +0 -1
  25. package/extension/src/shared/sessionState.js +17 -21
  26. package/extension/src/shared/sessionTitle.js +120 -0
  27. package/extension/src/shared/updateRuntimeIdentity.js +32 -0
  28. package/extension/styles/panel.css +73 -14
  29. package/native-host/src/mirrorWorkspace.js +20 -1
  30. package/native-host/src/releaseProxy.js +177 -0
  31. package/native-host/src/releaseTransport.js +154 -0
  32. package/native-host/src/updateManager.js +40 -39
  33. package/package.json +1 -1
@@ -2,6 +2,9 @@
2
2
  if (typeof module === 'object' && module.exports) {
3
3
  module.exports = factory(root);
4
4
  } else {
5
+ if (typeof root.CodexOverleafRealtimeObserver?.dispose === 'function') {
6
+ root.CodexOverleafRealtimeObserver.dispose();
7
+ }
5
8
  root.CodexOverleafRealtimeObserver = factory(root);
6
9
  }
7
10
  })(typeof globalThis !== 'undefined' ? globalThis : window, function overleafRealtimeObserverFactory(root) {
@@ -12,8 +15,34 @@
12
15
  const BASELINE_REFRESH_EVENT_TYPES = ['click', 'focusin', 'change'];
13
16
  const CHANNEL_KEY_PATTERN = /socket|websocket|channel|realtime|collab|share|ot|doc|editor|connection|event|broadcast|presence/i;
14
17
  const SENSITIVE_KEY_PATTERN = /^(content|previousContent|nextContent|text|body|raw|rawContent|source|sourceText)$/i;
18
+ const LIMITS = Object.freeze({ maxFileBytes: 1024 * 1024, maxEvents: 64, maxQueuedBytes: 4 * 1024 * 1024 });
19
+ const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
20
+ const ownedObservers = new WeakMap();
15
21
 
16
22
  function create(deps = {}) {
23
+ const documentRef = Object.prototype.hasOwnProperty.call(deps, 'document')
24
+ ? deps.document
25
+ : getDefaultDocument();
26
+ dispose(documentRef);
27
+ const observer = createObserver(deps);
28
+ if (documentRef && ['object', 'function'].includes(typeof documentRef)) {
29
+ ownedObservers.set(documentRef, observer);
30
+ }
31
+ return observer;
32
+ }
33
+
34
+ function dispose(documentRef = getDefaultDocument()) {
35
+ if (!documentRef || !['object', 'function'].includes(typeof documentRef)) return;
36
+ const previous = ownedObservers.get(documentRef);
37
+ if (!previous) return;
38
+ try {
39
+ previous.stop();
40
+ } finally {
41
+ ownedObservers.delete(documentRef);
42
+ }
43
+ }
44
+
45
+ function createObserver(deps = {}) {
17
46
  const pageWindow = deps.window || getDefaultWindow();
18
47
  const pageDocument = Object.prototype.hasOwnProperty.call(deps, 'document')
19
48
  ? deps.document
@@ -21,6 +50,9 @@
21
50
  const events = [];
22
51
  let activePath = '';
23
52
  let lastContent = '';
53
+ let baselineByteCount = 0;
54
+ let queuedTextBytes = 0;
55
+ let coalescedEventCount = 0;
24
56
  let running = false;
25
57
  let listenersAttached = false;
26
58
  let statusName = 'off';
@@ -31,6 +63,7 @@
31
63
 
32
64
  function start(_params = {}) {
33
65
  clearQueuedEvents();
66
+ coalescedEventCount = 0;
34
67
  channelCandidates = collectChannelCandidates(pageWindow);
35
68
  if (!canAttachDocumentListeners()) {
36
69
  running = false;
@@ -40,13 +73,15 @@
40
73
 
41
74
  running = true;
42
75
  refreshActiveBaseline();
43
- attachDocumentListeners();
76
+ if (running) attachDocumentListeners();
44
77
  return getStatus();
45
78
  }
46
79
 
47
80
  function stop() {
48
81
  detachDocumentListeners();
49
82
  clearQueuedEvents();
83
+ lastContent = '';
84
+ baselineByteCount = 0;
50
85
  running = false;
51
86
  statusName = 'off';
52
87
  statusReason = '';
@@ -61,6 +96,9 @@
61
96
  strategy: STRATEGY_ACTIVE_EDITOR,
62
97
  activePath,
63
98
  queuedEventCount: events.length,
99
+ queuedTextBytes,
100
+ baselineByteCount,
101
+ coalescedEventCount,
64
102
  lastEventAt,
65
103
  lastErrorCode,
66
104
  channelCandidates: cloneChannelCandidates(channelCandidates)
@@ -72,13 +110,60 @@
72
110
  }
73
111
 
74
112
  function drainEvents() {
113
+ // Capture-phase input can precede the editor transaction; paste, undo
114
+ // and collaboration updates may dispatch no DOM input at all. The
115
+ // existing bounded poll samples committed text before draining.
116
+ if (running) handleEditorInput();
75
117
  const drained = events.slice();
76
118
  clearQueuedEvents();
77
- return drained;
119
+ if (!drained.length) return [];
120
+ const otText = resolveOtText(deps, pageWindow, root);
121
+ if (typeof otText?.normalizeObservedTextEvent !== 'function') {
122
+ stopObservation('missing_ot_text');
123
+ return [];
124
+ }
125
+ const normalized = [];
126
+ for (const pending of drained) {
127
+ let event;
128
+ const { previousBytes, nextBytes, ...input } = pending;
129
+ try { event = otText.normalizeObservedTextEvent(input); } catch (_error) { /* fail closed below */ }
130
+ if (!event || event.ok !== true) {
131
+ stopObservation(event?.reason || 'normalize_observed_text_event_failed');
132
+ return [];
133
+ }
134
+ normalized.push(sanitizeObservedEvent(event));
135
+ }
136
+ return normalized;
78
137
  }
79
138
 
80
139
  function clearQueuedEvents() {
81
140
  events.length = 0;
141
+ queuedTextBytes = 0;
142
+ }
143
+
144
+ function stopObservation(reason) {
145
+ stop();
146
+ markUnavailable(reason);
147
+ }
148
+
149
+ function queueObservedChange(event, previousBytes, nextBytes) {
150
+ const tail = events[events.length - 1];
151
+ const merge = tail?.path === event.path && tail.nextContent === event.previousContent;
152
+ const pending = { ...event, previousContent: merge ? tail.previousContent : event.previousContent,
153
+ previousBytes: merge ? tail.previousBytes : previousBytes, nextBytes };
154
+ const remainingBytes = queuedTextBytes - (merge ? tail.previousBytes + tail.nextBytes : 0);
155
+ const noChange = pending.previousContent === pending.nextContent;
156
+ const nextLength = events.length - (merge ? 1 : 0) + (noChange ? 0 : 1);
157
+ const nextTotal = remainingBytes + (noChange ? 0 : pending.previousBytes + pending.nextBytes);
158
+ if (nextLength > LIMITS.maxEvents || nextTotal > LIMITS.maxQueuedBytes) {
159
+ stopObservation('ot_queue_limit');
160
+ return false;
161
+ }
162
+ if (merge) { events.pop(); coalescedEventCount += 1; }
163
+ if (!noChange) events.push(pending);
164
+ queuedTextBytes = nextTotal;
165
+ lastEventAt = event.observedAt;
166
+ return true;
82
167
  }
83
168
 
84
169
  function canAttachDocumentListeners() {
@@ -118,12 +203,14 @@
118
203
 
119
204
  const textResult = readEditorText();
120
205
  if (!textResult.ok) {
121
- markUnavailable(textResult.reason);
206
+ if (textResult.reason === 'ot_file_limit') stopObservation(textResult.reason);
207
+ else markUnavailable(textResult.reason);
122
208
  return false;
123
209
  }
124
210
 
125
211
  activePath = pathResult.path;
126
212
  lastContent = textResult.text;
213
+ baselineByteCount = textResult.bytes;
127
214
  markObserving();
128
215
  return true;
129
216
  }
@@ -144,16 +231,19 @@
144
231
 
145
232
  const textResult = readEditorText();
146
233
  if (!textResult.ok) {
147
- markUnavailable(textResult.reason);
234
+ if (textResult.reason === 'ot_file_limit') stopObservation(textResult.reason);
235
+ else markUnavailable(textResult.reason);
148
236
  return;
149
237
  }
150
238
 
151
239
  activePath = pathResult.path;
152
240
  lastContent = textResult.text;
241
+ baselineByteCount = textResult.bytes;
153
242
  markObserving();
154
243
  }
155
244
 
156
- function handleEditorInput() {
245
+ function handleEditorInput(inputEvent) {
246
+ if (inputEvent?.target?.closest?.('#codex-overleaf-panel')) return;
157
247
  if (!running) {
158
248
  return;
159
249
  }
@@ -166,7 +256,8 @@
166
256
 
167
257
  const textResult = readEditorText();
168
258
  if (!textResult.ok) {
169
- markUnavailable(textResult.reason);
259
+ if (textResult.reason === 'ot_file_limit') stopObservation(textResult.reason);
260
+ else markUnavailable(textResult.reason);
170
261
  return;
171
262
  }
172
263
 
@@ -175,6 +266,7 @@
175
266
  // Fallback for file switches that were not preceded by a selection/focus event.
176
267
  activePath = pathResult.path;
177
268
  lastContent = nextContent;
269
+ baselineByteCount = textResult.bytes;
178
270
  markObserving();
179
271
  return;
180
272
  }
@@ -185,34 +277,25 @@
185
277
  }
186
278
 
187
279
  const observedAt = resolveObservedAt();
188
- const otText = resolveOtText(deps, pageWindow, root);
189
- if (!otText || typeof otText.normalizeObservedTextEvent !== 'function') {
190
- markUnavailable('missing_ot_text');
191
- return;
192
- }
193
-
194
- const event = otText.normalizeObservedTextEvent({
280
+ const queued = queueObservedChange({
195
281
  path: activePath,
196
282
  previousContent: lastContent,
197
283
  nextContent,
198
284
  observedAt,
199
285
  source: STRATEGY_ACTIVE_EDITOR
200
- });
201
- if (event && event.ok === true) {
202
- const queuedEvent = sanitizeObservedEvent(event);
203
- events.push(queuedEvent);
204
- lastEventAt = queuedEvent.observedAt || observedAt;
286
+ }, baselineByteCount, textResult.bytes);
287
+ if (queued) {
288
+ lastContent = nextContent;
289
+ baselineByteCount = textResult.bytes;
205
290
  markObserving();
206
- } else {
207
- lastErrorCode = event?.reason || 'normalize_observed_text_event_failed';
208
291
  }
209
- lastContent = nextContent;
210
292
  }
211
293
 
212
294
  function handleActivePathUnavailable(reason) {
213
295
  if (reason === 'missing_active_path') {
214
296
  activePath = '';
215
297
  lastContent = '';
298
+ baselineByteCount = 0;
216
299
  }
217
300
  markUnavailable(reason);
218
301
  }
@@ -252,9 +335,17 @@
252
335
  };
253
336
  }
254
337
  try {
338
+ const text = deps.readActiveEditorText();
339
+ if (typeof text !== 'string') {
340
+ return { ok: false, reason: 'missing_editor_content' };
341
+ }
342
+ const bytes = text.length > LIMITS.maxFileBytes ? LIMITS.maxFileBytes + 1
343
+ : encoder ? encoder.encode(text).byteLength : text.length * 3;
344
+ if (bytes > LIMITS.maxFileBytes) return { ok: false, reason: 'ot_file_limit' };
255
345
  return {
256
346
  ok: true,
257
- text: String(deps.readActiveEditorText() ?? '')
347
+ text,
348
+ bytes
258
349
  };
259
350
  } catch (_error) {
260
351
  return {
@@ -444,7 +535,6 @@
444
535
 
445
536
  function normalizePath(value) {
446
537
  return String(value || '')
447
- .replace(/\s+/g, ' ')
448
538
  .replace(/\\/g, '/')
449
539
  .trim()
450
540
  .replace(/^\/+/, '');
@@ -485,7 +575,9 @@
485
575
  }
486
576
 
487
577
  return {
578
+ LIMITS,
488
579
  collectChannelCandidates,
489
- create
580
+ create,
581
+ dispose
490
582
  };
491
583
  });
@@ -12,7 +12,7 @@
12
12
  const MIN_NATIVE_VERSION = '1.0.0';
13
13
  const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
14
14
  const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
15
- const BUILD_TARGET_VERSION = '2.3.5';
15
+ const BUILD_TARGET_VERSION = '2.3.6';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -40,20 +40,31 @@
40
40
  diagnosticsPageSubtitle: 'Current file, write access, track changes',
41
41
  diagnosticsSnapshotTitle: 'Check Project Read',
42
42
  diagnosticsSnapshotSubtitle: 'Full project, assets, read source',
43
- diagnosticsOtTitle: 'Check Experimental OT Mirror',
43
+ diagnosticsOtTitle: 'Check OT Mirror',
44
44
  diagnosticsOtSubtitle: 'Status, fresh files, fallback',
45
45
  diagnosticsExportTitle: 'Export Diagnostics',
46
46
  diagnosticsExportSubtitle: 'Redacted audit and environment bundle',
47
47
  diagnosticsExportDone: 'Diagnostics bundle exported.',
48
- diagnosticsOtSummaryEnabled: 'Experimental OT is enabled. Codex will use the OT-warmed mirror only when matching files are fresh; otherwise it falls back to the normal project read path.',
49
- diagnosticsOtSummaryDisabled: 'Experimental OT is disabled, so Codex is using the normal project read path.',
50
- diagnosticsOtNextStep: 'If you expected OT warm reads, turn on OT and wait for the status to show Live before relying on it.',
51
- experimentalOtMenuTitle: 'Experimental OT Mirror',
52
- experimentalOtMenuSubtitle: 'Experimental — speeds up reading the file you are editing; Codex falls back to normal reading whenever unsure.',
53
- experimentalOtConfirmTitle: 'Experimental OT Mirror',
54
- experimentalOtConfirmMessage: 'Experimental: tracks the current Overleaf editor and warms the local mirror so Codex can read recent focused-file edits faster. Codex uses it only when file scope is clear and the mirror is fresh; otherwise it falls back to normal project reading.',
48
+ diagnosticsOtSummaryEnabled: 'OT Mirror is enabled. Codex will use the OT-warmed mirror only when matching files are fresh; otherwise it falls back to the normal project read path.',
49
+ diagnosticsOtSummaryDisabled: 'OT Mirror is disabled, so Codex is using the normal project read path.',
50
+ diagnosticsOtNextStep: 'Observer status describes warming availability. File scope and freshness are checked again before each run.',
51
+ experimentalOtMenuTitle: 'OT Mirror',
52
+ otSettingsTitle: 'Project cache',
53
+ contextLoadingTitle: 'Context loading',
54
+ runFeedbackFailedTitle: 'Needs attention',
55
+ runFeedbackPartialTitle: 'Partially completed',
56
+ runFeedbackUnconfirmedTitle: 'Write status needs review',
57
+ runFeedbackGeneric: 'Open the run details for the reported cause and the next step.',
58
+ runFeedbackUnconfirmed: 'Some changes may already be in Overleaf. Check the document before retrying.',
59
+ runFeedbackUnconfirmedResult: 'Not fully confirmed. Review Overleaf before retrying.',
60
+ runFeedbackReviewFile: 'Review {file} in Overleaf',
61
+ runFeedbackReviewDetails: 'Review run details',
62
+ runFeedbackDetailsUnavailable: 'Detailed steps are unavailable for this older run.',
63
+ experimentalOtMenuSubtitle: 'Warms recent edits to the active text file locally. File scope and freshness are checked before reuse.',
64
+ experimentalOtConfirmTitle: 'Enable OT Mirror?',
65
+ experimentalOtConfirmMessage: 'Observes the active text editor and warms its local mirror. This is read-only with respect to Overleaf and does not upload assets. Each run checks file scope and freshness before reuse; normal project reading remains the fallback.',
55
66
  experimentalOtConfirmEnable: 'Turn on',
56
- experimentalOtEnabledToast: 'Experimental OT Mirror is on. Use @file for the cleanest warm-mirror test.',
67
+ experimentalOtEnabledToast: 'OT Mirror requested for this project. Its status shows whether warming is available.',
57
68
  diagnosticsSnapshotLoading: 'Checking whether the extension can read the full Overleaf project.',
58
69
  diagnosticsSnapshotErrorSummary: 'Overleaf may still be loading, or the page API did not return project files yet.',
59
70
  diagnosticsNativeLoading: 'Checking whether local Codex, Native Host, and LaTeX tools are available.',
@@ -326,18 +337,27 @@
326
337
  requireReviewingTitle: 'When enabled, Codex checks or switches Overleaf Reviewing/Track Changes before writing. Deletes still require confirmation.',
327
338
  autoCompile: 'Compile',
328
339
  autoCompileTitle: 'After Codex writes, click Overleaf Recompile and record the compile result for this task. Ask mode will not trigger it.',
329
- experimentalOtWarmMirror: 'Experimental OT warm mirror',
340
+ experimentalOtWarmMirror: 'OT warm mirror',
330
341
  experimentalOtWarmMirrorShort: 'OT',
331
- experimentalOtWarmMirrorTitle: 'Experimental read-only OT mirror warming. If unavailable, stale, or inconsistent, Codex falls back to the normal project read path.',
342
+ experimentalOtWarmMirrorTitle: 'Read-only local text warming. File scope and freshness are checked before reuse; normal project reading remains the fallback.',
332
343
  otStatus: 'OT status',
333
- otFreshFiles: 'OT fresh files',
334
- otFallback: 'Fallback',
344
+ otFreshFiles: 'Files with recent OT evidence',
345
+ otFallback: 'Read fallback',
335
346
  otStatusOff: 'Off',
336
- otStatusStarting: 'Start',
337
- otStatusObserving: 'Live',
338
- otStatusUnavailable: 'N/A',
339
- otStatusStale: 'Stale',
340
- otStatusInconsistent: 'Check',
347
+ otStatusStarting: 'Connecting',
348
+ otStatusObserving: 'Observing current editor',
349
+ otStatusUnavailable: 'Unavailable',
350
+ otStatusStale: 'Warming paused; awaiting verification',
351
+ otStatusInconsistent: 'Needs verification',
352
+ otFailureBridge: 'The page connection needs to be restored. Reload the extension and Overleaf before retrying.',
353
+ otFailureObserver: 'The editor observer is unavailable. The next run will try normal project reading.',
354
+ otFailureEditor: 'The active editor could not be read. Warming is paused; the next run will try normal project reading.',
355
+ otFailureFileLimit: 'This file exceeds the 1 MiB warming limit. The next run will try normal project reading.',
356
+ otFailureQueueLimit: 'The mirror queue reached its limit. Warming is paused; the next run will try normal project reading.',
357
+ otFailureExpired: 'The observation is too old to reuse. The next run will try normal project reading.',
358
+ otFailureMirror: 'The mirror update was not fully confirmed. The next run will try normal project reading.',
359
+ otFailureGeneric: 'Mirror warming could not continue. The next run will try normal project reading.',
360
+ otFailureNextStep: 'Resolve the reported cause, then enable OT again. If the page connection is unavailable, reload the extension and Overleaf first.',
341
361
  modelSourceFallback: 'fallback',
342
362
  modelSourceDiscovered: 'discovered',
343
363
  modelSourceFailed: 'failed',
@@ -560,20 +580,31 @@
560
580
  diagnosticsPageSubtitle: '当前文件、写入能力、留痕状态',
561
581
  diagnosticsSnapshotTitle: '检查项目读取',
562
582
  diagnosticsSnapshotSubtitle: '完整项目、资源文件、读取来源',
563
- diagnosticsOtTitle: '检查实验性 OT Mirror',
583
+ diagnosticsOtTitle: '检查 OT Mirror',
564
584
  diagnosticsOtSubtitle: '状态、新鲜文件、回退路径',
565
585
  diagnosticsExportTitle: '导出诊断信息',
566
586
  diagnosticsExportSubtitle: '已脱敏的审计和环境信息包',
567
587
  diagnosticsExportDone: '诊断信息已导出。',
568
- diagnosticsOtSummaryEnabled: '实验性 OT 已开启。只有匹配文件是新鲜状态时,Codex 才会使用 OT 预热 mirror;否则会回退到常规项目读取路径。',
569
- diagnosticsOtSummaryDisabled: '实验性 OT 已关闭,Codex 会使用常规项目读取路径。',
570
- diagnosticsOtNextStep: '如果你希望使用 OT 预热读取,请开启 OT,并等状态显示为“实时”后再依赖它。',
571
- experimentalOtMenuTitle: '实验性 OT Mirror',
572
- experimentalOtMenuSubtitle: '实验性——加快读取你正在编辑的文件;拿不准时自动回退到普通读取。',
573
- experimentalOtConfirmTitle: '实验性 OT Mirror',
574
- experimentalOtConfirmMessage: '实验性:实时跟踪当前 Overleaf 编辑器,把改动预热到本地 mirror,让 Codex 更快读到最近的当前文件改动。只有文件范围明确且 mirror 新鲜时才会使用;否则自动回退到普通项目读取。',
588
+ diagnosticsOtSummaryEnabled: 'OT Mirror 已开启。只有匹配文件是新鲜状态时,Codex 才会使用 OT 预热镜像;否则会回退到常规项目读取路径。',
589
+ diagnosticsOtSummaryDisabled: 'OT Mirror 已关闭,Codex 会使用常规项目读取路径。',
590
+ diagnosticsOtNextStep: '观测状态说明预热是否可用;每次任务开始前仍会重新校验文件范围和新鲜度。',
591
+ experimentalOtMenuTitle: 'OT Mirror',
592
+ otSettingsTitle: '项目缓存',
593
+ contextLoadingTitle: '上下文加载',
594
+ runFeedbackFailedTitle: '需要处理',
595
+ runFeedbackPartialTitle: '部分完成',
596
+ runFeedbackUnconfirmedTitle: '写入状态待确认',
597
+ runFeedbackGeneric: '可在本轮详情中查看具体原因和后续处理方式。',
598
+ runFeedbackUnconfirmed: '部分改动可能已经写入 Overleaf,重试前请先检查文档。',
599
+ runFeedbackUnconfirmedResult: '结果尚未完全确认,请检查 Overleaf 后再决定是否重试。',
600
+ runFeedbackReviewFile: '在 Overleaf 检查 {file}',
601
+ runFeedbackReviewDetails: '查看运行步骤',
602
+ runFeedbackDetailsUnavailable: '这条历史记录没有可展开的详细步骤。',
603
+ experimentalOtMenuSubtitle: '在本地预热当前文本文件的最近改动,复用前重新校验文件范围和新鲜度。',
604
+ experimentalOtConfirmTitle: '开启 OT Mirror?',
605
+ experimentalOtConfirmMessage: '观测当前文本编辑器并预热本地镜像。此功能不会写回 Overleaf,也不负责上传资产。每次任务复用前都会校验文件范围和新鲜度,不满足条件时尝试常规项目读取。',
575
606
  experimentalOtConfirmEnable: '开启',
576
- experimentalOtEnabledToast: '实验性 OT Mirror 已开启。使用 @file 可以最清楚地测试 warm mirror。',
607
+ experimentalOtEnabledToast: '已为当前项目请求开启 OT Mirror,可用情况见状态提示。',
577
608
  diagnosticsSnapshotLoading: '确认插件能否读取完整 Overleaf 项目。',
578
609
  diagnosticsSnapshotErrorSummary: '这通常是 Overleaf 页面还没加载完成,或者当前页面接口暂时没有返回项目文件。',
579
610
  diagnosticsNativeLoading: '确认本机 Codex、Native Host 和 LaTeX 工具是否可用。',
@@ -846,18 +877,27 @@
846
877
  requireReviewingTitle: '开启后,写入前会确认并尝试切到 Overleaf Reviewing/Track Changes;删除仍需确认。',
847
878
  autoCompile: '编译',
848
879
  autoCompileTitle: 'Codex 写入后自动点击 Overleaf Recompile,并把编译结果记录到本轮任务。只问不改时不会触发。',
849
- experimentalOtWarmMirror: '实验性 OT 预热镜像',
880
+ experimentalOtWarmMirror: 'OT 预热镜像',
850
881
  experimentalOtWarmMirrorShort: 'OT',
851
- experimentalOtWarmMirrorTitle: '实验性只读 OT 镜像预热;不可用、过期或不一致时会回退到常规项目读取。',
882
+ experimentalOtWarmMirrorTitle: '只读本地文本预热;复用前校验文件范围和新鲜度,常规项目读取作为回退路径。',
852
883
  otStatus: 'OT 状态',
853
- otFreshFiles: 'OT 新鲜文件',
854
- otFallback: '回退',
855
- otStatusOff: '关',
856
- otStatusStarting: '启动',
857
- otStatusObserving: '实时',
858
- otStatusUnavailable: '不可用',
859
- otStatusStale: '过期',
860
- otStatusInconsistent: '需检查',
884
+ otFreshFiles: '近期有 OT 校验证据的文件',
885
+ otFallback: '读取回退',
886
+ otStatusOff: '未开启',
887
+ otStatusStarting: '正在连接',
888
+ otStatusObserving: '正在观测当前编辑器',
889
+ otStatusUnavailable: '暂不可用',
890
+ otStatusStale: '预热暂缓,等待重新校验',
891
+ otStatusInconsistent: '需重新校验',
892
+ otFailureBridge: '页面连接需要恢复,请先重新加载扩展并刷新 Overleaf,再重试。',
893
+ otFailureObserver: '编辑器观察器暂不可用,下次任务将尝试常规项目读取。',
894
+ otFailureEditor: '当前编辑器内容尚不可读,预热已暂停;下次任务将尝试常规项目读取。',
895
+ otFailureFileLimit: '文件超过 1 MiB 预热上限,下次任务将尝试常规项目读取。',
896
+ otFailureQueueLimit: '镜像队列达到容量上限,预热已暂停;下次任务将尝试常规项目读取。',
897
+ otFailureExpired: '观测记录已过期,下次任务将尝试常规项目读取。',
898
+ otFailureMirror: '镜像更新尚未完整确认,下次任务将尝试常规项目读取。',
899
+ otFailureGeneric: '镜像预热未能继续,下次任务将尝试常规项目读取。',
900
+ otFailureNextStep: '处理所显示的原因后重新开启 OT;若页面连接不可用,请先重新加载扩展并刷新 Overleaf。',
861
901
  modelSourceFallback: '备用列表',
862
902
  modelSourceDiscovered: '已发现',
863
903
  modelSourceFailed: '获取失败',
@@ -26,7 +26,7 @@
26
26
  ]);
27
27
 
28
28
  const PHASE_TIMEOUTS = Object.freeze({
29
- checking: 30 * 1000,
29
+ checking: 60 * 1000,
30
30
  downloading: 2 * 60 * 1000,
31
31
  applying: 90 * 1000,
32
32
  awaiting_health: 2 * 60 * 1000,
@@ -279,7 +279,6 @@
279
279
 
280
280
  function normalizePath(value) {
281
281
  return String(value || '')
282
- .replace(/\s+/g, ' ')
283
282
  .replace(/\\/g, '/')
284
283
  .trim()
285
284
  .replace(/^\/+/, '');
@@ -5,12 +5,13 @@
5
5
  require('./settlementFacts'),
6
6
  require('./runInputQueue'),
7
7
  require('./lineReferences'),
8
- require('./pathRedaction')
8
+ require('./pathRedaction'),
9
+ require('./sessionTitle')
9
10
  );
10
11
  } else {
11
12
  root.CodexOverleafModuleRegistry.define(
12
13
  'SessionState',
13
- ['I18n', 'SettlementFacts', 'RunInputQueue', 'LineReferences', 'PathRedaction'],
14
+ ['I18n', 'SettlementFacts', 'RunInputQueue', 'LineReferences', 'PathRedaction', 'SessionTitle'],
14
15
  factory
15
16
  );
16
17
  }
@@ -19,7 +20,8 @@
19
20
  SettlementFacts,
20
21
  RunInputQueue,
21
22
  LineReferences,
22
- PathRedaction
23
+ PathRedaction,
24
+ SessionTitle
23
25
  ) {
24
26
  'use strict';
25
27
 
@@ -402,6 +404,12 @@
402
404
  ) {
403
405
  next.title = session.title;
404
406
  next.titleSource = 'manual';
407
+ } else if (session.titleSource === 'auto' && patch.titleSource !== 'manual'
408
+ && typeof patch.title === 'string' && patch.title.trim() && session.runs?.length
409
+ && SessionTitle.isGeneric(sanitizeAutoTitle(patch.title))
410
+ && !SessionTitle.isGeneric(sanitizeAutoTitle(session.title))) {
411
+ next.title = session.title;
412
+ next.titleSource = 'auto';
405
413
  }
406
414
  return normalizeSession({
407
415
  ...next
@@ -472,12 +480,7 @@
472
480
  }
473
481
 
474
482
  function deriveSessionTitle(runs, task) {
475
- const taskTitle = sanitizeAutoTitle(task);
476
- if (taskTitle) {
477
- return taskTitle;
478
- }
479
- const firstRunTask = Array.isArray(runs) && runs.length ? runs[0]?.task : '';
480
- return sanitizeAutoTitle(firstRunTask);
483
+ return SessionTitle.derive(runs, task, { sanitize: sanitizeAssistantVisibleText, maxChars: SESSION_AUTO_TITLE_CHARS });
481
484
  }
482
485
 
483
486
  // Rename a session with the placeholder/derived-title ghost guard (mirrors
@@ -519,21 +522,14 @@
519
522
  if (titleSource === 'manual') {
520
523
  return normalizeTextField(rawTitle === LEGACY_DEFAULT_SESSION_TITLE ? '' : rawTitle, STORAGE_DEFAULT_LIMITS.sessionTitleChars);
521
524
  }
522
- return sanitizeAutoTitle(rawTitle === LEGACY_DEFAULT_SESSION_TITLE ? '' : rawTitle) || derivedTitle || '';
525
+ const current = sanitizeAutoTitle(rawTitle === LEGACY_DEFAULT_SESSION_TITLE ? '' : rawTitle);
526
+ if (!current || SessionTitle.isGeneric(current)) return derivedTitle || current || '';
527
+ if (current.endsWith('…') && derivedTitle.startsWith(current.slice(0, -1))) return derivedTitle;
528
+ return current;
523
529
  }
524
530
 
525
531
  function sanitizeAutoTitle(value) {
526
- const title = sanitizeAssistantVisibleText(value)
527
- .replace(/@file:[^\s]+/g, ' ')
528
- .replace(/@(context|compile-log)\b/g, ' ')
529
- .replace(/\s+/g, ' ')
530
- .trim();
531
- if (!title) {
532
- return '';
533
- }
534
- return title.length > SESSION_AUTO_TITLE_CHARS
535
- ? `${title.slice(0, SESSION_AUTO_TITLE_CHARS - 1)}…`
536
- : title;
532
+ return SessionTitle.fromPrompt(value, { sanitize: sanitizeAssistantVisibleText, maxChars: SESSION_AUTO_TITLE_CHARS });
537
533
  }
538
534
 
539
535
  function isDisplayableSession(session) {