codex-overleaf-link 2.3.6 → 2.4.1

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 (52) hide show
  1. package/README.md +62 -34
  2. package/README.zh-CN.md +546 -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 +122 -118
  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 +1233 -550
  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/panelMaintenance.js +8 -4
  21. package/extension/src/content/recentProjects.js +354 -355
  22. package/extension/src/content/runResultActions.js +16 -1
  23. package/extension/src/content/runTimelineView.js +9 -11
  24. package/extension/src/content/scopedPersistenceCoordinator.js +2 -1
  25. package/extension/src/content/scopedPersistencePanelState.js +45 -2
  26. package/extension/src/content/sessionForkController.js +20 -11
  27. package/extension/src/content/sessionPersistence.js +4 -1
  28. package/extension/src/content/settingsPanel.js +16 -2
  29. package/extension/src/content/trackedChangeCaptureController.js +153 -0
  30. package/extension/src/content/writebackOrchestrator.js +31 -0
  31. package/extension/src/page/binaryAssetUploader.js +101 -29
  32. package/extension/src/page/overleafRealtimeObserver.js +4 -1
  33. package/extension/src/page/saveState.js +44 -1
  34. package/extension/src/page/textFileCreator.js +493 -0
  35. package/extension/src/page/trackedChangeCapture.js +342 -0
  36. package/extension/src/page/trackedChangesLifecycle.js +149 -150
  37. package/extension/src/page/treeOperations.js +40 -29
  38. package/extension/src/page/writebackRouter.js +95 -49
  39. package/extension/src/pageBridge.js +65 -25
  40. package/extension/src/shared/compatibility.js +1 -1
  41. package/extension/src/shared/i18n.js +94 -40
  42. package/extension/src/shared/pageRpcContract.js +2 -1
  43. package/extension/src/shared/sessionState.js +6 -2
  44. package/extension/src/shared/storageDb.js +11 -12
  45. package/extension/src/shared/storageRunActions.js +73 -1
  46. package/extension/src/shared/writebackSettlement.js +19 -5
  47. package/extension/styles/panel.css +254 -405
  48. package/native-host/src/codexSessionRunner.js +41 -38
  49. package/native-host/src/codexSessionTiming.js +68 -0
  50. package/native-host/src/readProgressGuard.js +84 -17
  51. package/native-host/src/taskRunnerRuntime.js +25 -0
  52. package/package.json +5 -1
@@ -2,9 +2,9 @@
2
2
  "schemaVersion": 1,
3
3
  "entry": "extension/entries/content-entry.mjs",
4
4
  "output": "extension/src/content/generated/content.bundle.js",
5
- "sourceDigest": "15c3e103532c84dea57aeac33e02ec665fbf7518543e14daac2459c434b3e5d5",
6
- "outputDigest": "5e1dba4bfcf4c595835312deabefa5ecdd2ee7c6d98f29d1b2bd9711ad5b195c",
7
- "byteLength": 2375784,
5
+ "sourceDigest": "b19a018ab6eb9c7bdca8cfad8ff6efcc4a30e80f81c3c0a2e89a7f78b6361d97",
6
+ "outputDigest": "7640649ee012b3224d696703c2a1ab323d549418a3fcc31e2219bf14399416b5",
7
+ "byteLength": 2422231,
8
8
  "inputs": [
9
9
  "extension/entries/content-entry.mjs",
10
10
  "extension/src/content/activeTurnControl.js",
@@ -62,6 +62,7 @@
62
62
  "extension/src/content/sessionPersistence.js",
63
63
  "extension/src/content/settingsPanel.js",
64
64
  "extension/src/content/themeController.js",
65
+ "extension/src/content/trackedChangeCaptureController.js",
65
66
  "extension/src/content/updateIdle.js",
66
67
  "extension/src/content/updateNotice.js",
67
68
  "extension/src/content/writebackController.js",
@@ -75,6 +75,7 @@
75
75
  'StorageMigration',
76
76
  'Summary',
77
77
  'Theme',
78
+ 'TrackedChangeCaptureController',
78
79
  'UndoOperations',
79
80
  'UpdateIdle',
80
81
  'UpdateNotice',
@@ -134,7 +134,8 @@
134
134
  return { stale: false, error: response?.error || { code: 'provider_models_unavailable' } };
135
135
  }
136
136
  const sourceModels = hasDiscoveredModels ? response.result.models : fallbackModels;
137
- const retainedSelectedModel = retainSelectedModel(sourceModels, currentSelectedModel);
137
+ const preserveBuiltin = (selection?.providerId || 'builtin') === 'builtin' && (getState()?.providerId || 'builtin') === 'builtin';
138
+ const retainedSelectedModel = retainSelectedModel(sourceModels, currentSelectedModel, { preserveUnavailable: preserveBuiltin });
138
139
  const normalized = modelCatalog.normalizeDiscoveredModels({ models: sourceModels, selectedModel: retainedSelectedModel });
139
140
  if (normalized.usedFallback && response.result?.providerId && response.result.providerId !== 'builtin') {
140
141
  applyUnavailableModelOptions({ code: 'provider_model_catalog_invalid', message: 'The active provider returned no usable models.' });
@@ -143,9 +144,12 @@
143
144
  error: { code: 'provider_model_catalog_invalid', message: 'The active provider returned no usable models.' }
144
145
  };
145
146
  }
146
- renderModelOptions(normalized.models, retainedSelectedModel, { allowUnknownSelected: false });
147
+ const renderedModels = preserveBuiltin
148
+ ? Support.preserveSelectedCapabilities(normalized.models, retainedSelectedModel, getState(), !hasDiscoveredModels || response.result?.source === 'fallback')
149
+ : normalized.models;
150
+ renderModelOptions(renderedModels, retainedSelectedModel, { allowUnknownSelected: false });
147
151
  modelDiscovery = {
148
- status: hasDiscoveredModels && !normalized.usedFallback ? 'discovered' : 'fallback',
152
+ status: hasDiscoveredModels && !normalized.usedFallback && response.result?.source !== 'fallback' ? 'discovered' : 'fallback',
149
153
  source: hasDiscoveredModels ? response.result?.source || 'unknown' : 'fallback',
150
154
  fetchedAt: hasDiscoveredModels ? response.result?.fetchedAt || '' : '',
151
155
  errorCode: hasDiscoveredModels ? '' : response?.error?.code || '',
@@ -172,15 +176,16 @@
172
176
  }
173
177
 
174
178
  function applyUnavailableModelOptions(error) {
179
+ const selectedModel = resolveSelectedModel();
175
180
  const modelSelect = getPanel()?.querySelector('[data-model]');
176
181
  if (modelSelect) {
177
182
  modelSelect.textContent = '';
178
183
  const option = document.createElement('option');
179
- option.value = resolveSelectedModel();
184
+ option.value = selectedModel;
180
185
  option.textContent = tx('Models unavailable', '模型不可用');
181
186
  option.disabled = true;
182
187
  modelSelect.append(option);
183
- modelSelect.value = '';
188
+ modelSelect.value = selectedModel;
184
189
  modelSelect.disabled = true;
185
190
  }
186
191
  modelDiscovery = {
@@ -190,8 +195,15 @@
190
195
  errorCode: error?.code || '',
191
196
  errorMessage: error?.message || (error ? String(error) : '')
192
197
  };
193
- renderReasoningOptions([]);
194
- renderSpeedOptions([]);
198
+ const pendingModels = Support.preserveSelectedCapabilities(
199
+ [{ id: selectedModel, unverified: true }], selectedModel, getState(), true
200
+ );
201
+ renderReasoningOptions(pendingModels);
202
+ renderSpeedOptions(pendingModels);
203
+ for (const selector of ['[data-reasoning]', '[data-speed]']) {
204
+ const input = getPanel()?.querySelector(selector);
205
+ if (input) input.disabled = true;
206
+ }
195
207
  renderModelConfigChoices();
196
208
  updateModelDisplay();
197
209
  }
@@ -200,9 +212,9 @@
200
212
  const modelCatalog = getModelCatalog();
201
213
  const fallbackModels = modelCatalog.FALLBACK_MODELS;
202
214
  const sourceModels = fallbackModels;
203
- const retainedSelectedModel = retainSelectedModel(sourceModels, selectedModel);
215
+ const retainedSelectedModel = retainSelectedModel(sourceModels, selectedModel, { preserveUnavailable: (getState()?.providerId || 'builtin') === 'builtin' });
204
216
  const normalized = modelCatalog.normalizeDiscoveredModels({ models: sourceModels, selectedModel: retainedSelectedModel });
205
- renderModelOptions(normalized.models, retainedSelectedModel);
217
+ renderModelOptions(Support.preserveSelectedCapabilities(normalized.models, retainedSelectedModel, getState(), true), retainedSelectedModel);
206
218
  modelDiscovery = {
207
219
  status: 'fallback',
208
220
  source: 'fallback',
@@ -29,14 +29,49 @@
29
29
  return String(label || '').replace(/^gpt[-\s]*/i, '');
30
30
  }
31
31
 
32
- function retainSelectedModel(models, selectedModel) {
32
+ function retainSelectedModel(models, selectedModel, options = {}) {
33
33
  const selectedId = normalizeModelOptionId(selectedModel);
34
34
  if (!selectedId) return '';
35
+ if (options.preserveUnavailable === true) return selectedId;
35
36
  return (Array.isArray(models) ? models : []).some(model => (
36
37
  normalizeModelOptionId(typeof model === 'string' ? model : model?.id) === selectedId
37
38
  )) ? selectedId : '';
38
39
  }
39
40
 
41
+ // A fallback or missing catalog entry cannot revoke an explicit selection.
42
+ // Keep its saved controls provisional until real capability data arrives.
43
+ function preserveSelectedCapabilities(models, selectedModel, preferences = {}, provisional = false) {
44
+ const selectedId = normalizeModelOptionId(selectedModel);
45
+ const saved = preferences || {};
46
+ return (Array.isArray(models) ? models : []).map(model => {
47
+ if (model.id !== selectedId || (!provisional && !model.unverified)) return model;
48
+ const reasoningEfforts = normalizeReasoningEffortsForSelect([
49
+ ...(model.reasoningEfforts || []), saved.reasoningEffort
50
+ ]);
51
+ const speedTiers = normalizeSpeedTiersForSelect(model.speedTiers);
52
+ const speed = ['standard', 'fast'].includes(saved.speedTier) ? saved.speedTier : '';
53
+ if (speed && !speedTiers.includes(speed)) speedTiers.push(speed);
54
+ return {
55
+ ...model, unverified: true, reasoningEfforts, speedTiers,
56
+ defaultReasoningEffort: reasoningEfforts.includes(saved.reasoningEffort)
57
+ ? saved.reasoningEffort : model.defaultReasoningEffort,
58
+ defaultSpeedTier: speed || model.defaultSpeedTier || 'standard'
59
+ };
60
+ });
61
+ }
62
+
63
+ function getRenderedModelEntries(panel) {
64
+ return Array.from(panel?.querySelector('[data-model]')?.options || []).map(option => ({
65
+ id: option.value,
66
+ label: option.textContent,
67
+ reasoningEfforts: (option.dataset.reasoningEfforts || '').split(',').filter(Boolean),
68
+ defaultReasoningEffort: option.dataset.defaultReasoningEffort || '',
69
+ reasoningPresentation: option.dataset.reasoningPresentation || '',
70
+ speedTiers: (option.dataset.speedTiers || 'standard').split(',').filter(Boolean),
71
+ defaultSpeedTier: option.dataset.defaultSpeedTier || 'standard'
72
+ }));
73
+ }
74
+
40
75
  function shouldUseBuiltInFallback(response) {
41
76
  const providerId = normalizeModelOptionId(response?.result?.providerId);
42
77
  return response?.ok === true && (!providerId || providerId === 'builtin');
@@ -114,6 +149,8 @@
114
149
  normalizeReasoningEffortsForSelect,
115
150
  normalizeSpeedTiersForSelect,
116
151
  retainSelectedModel,
152
+ preserveSelectedCapabilities,
153
+ getRenderedModelEntries,
117
154
  shouldUseBuiltInFallback
118
155
  };
119
156
  return api;
@@ -23,7 +23,10 @@
23
23
  async function dispatch(payload, channelMethod) {
24
24
  const compatibilityGate = await ensureForMethod(payload?.method);
25
25
  if (!compatibilityGate.ok) return compatibilityGate.response;
26
- if (gatedMethods.has(payload?.method)) options.throwIfCancellationRequested?.();
26
+ // Post-Undo cache bookkeeping must survive the ended run's cancellation.
27
+ if (gatedMethods.has(payload?.method) && payload.method !== 'mirror.invalidate') {
28
+ options.throwIfCancellationRequested?.();
29
+ }
27
30
  return nativeChannel[channelMethod](
28
31
  attachEvidence(payload, compatibilityGate.compatibility)
29
32
  );
@@ -1,6 +1,5 @@
1
1
  (function initCodexOverleafOtWarmMirror() {
2
2
  'use strict';
3
-
4
3
  // Experimental OT warm-mirror glue carved out of contentRuntime.js (v1.4.9
5
4
  // structural-debt phase 5): the per-project enable toggle flow, the
6
5
  // poll/flush timers and patch queue, mirror prefetch, warm-start
@@ -52,6 +51,7 @@
52
51
  let currentOtStatus = 'off';
53
52
  let otTogglePending = false;
54
53
  let otSyncRequestId = 0;
54
+ let otStartRetryCount = 0;
55
55
  let otWarmMirrorProjectId = '';
56
56
  let lastExperimentalOtProjectId = '';
57
57
  let otWarmMirrorState = {
@@ -226,6 +226,7 @@
226
226
  const projectId = getCurrentProjectId();
227
227
  otWarmMirrorState.failureActive = false;
228
228
  lastExperimentalOtProjectId = projectId;
229
+ otStartRetryCount = 0;
229
230
  setExperimentalOtEnabledForProject(projectId, checkbox.checked);
230
231
  updateExperimentalOtToggleControl(checkbox.checked);
231
232
  // Per-card saved feedback, same contract as every other settings card.
@@ -306,8 +307,10 @@
306
307
  await pauseOtWarmMirror(pauseAfterStart.reason || 'paused');
307
308
  return response;
308
309
  }
309
- otWarmMirrorState.failureActive = false;
310
+ if (status === 'starting') recordOtFailure('missing_active_path', 'start');
311
+ else otWarmMirrorState.failureActive = false;
310
312
  updateOtStatusDisplay(status);
313
+ otStartRetryCount = 0;
311
314
  scheduleOtEventPolling(projectId, { immediate: true });
312
315
  return response;
313
316
  }
@@ -338,21 +341,36 @@
338
341
  clearOtEventPolling({ clearPatchQueue: true });
339
342
  recordOtFailure(readOtBridgeErrorCode(response, 'ot_start_failed'), 'start');
340
343
  otWarmMirrorState.suspended = true;
341
- setExperimentalOtEnabledForProject(projectId, false);
342
- const experimentalOtCheckbox = getPanel()?.querySelector('[data-experimental-ot]');
343
- if (experimentalOtCheckbox) {
344
- experimentalOtCheckbox.checked = false;
345
- }
344
+ // Observer availability is transient; only an explicit user action may
345
+ // change the persisted opt-in. A cold editor/bridge must not turn OT off
346
+ // permanently. failureActive/suspended already prohibit cache reuse.
347
+ updateExperimentalOtToggleControl(isExperimentalOtEnabledForProject(projectId));
346
348
  updateOtStatusDisplay('unavailable');
347
- saveStateSoon();
348
349
  try { await callPageBridge('stopOtObserver', { projectId }); } catch (_error) { /* preserve the original failure */ }
350
+ scheduleOtStartRetry(projectId, requestId);
351
+ }
352
+
353
+ function scheduleOtStartRetry(projectId, requestId) {
354
+ const retryDelays = [1000, 3000, 6000];
355
+ const canRetry = () => isCurrentOtSync(requestId, projectId) && getCurrentProjectId() === projectId
356
+ && isExperimentalOtEnabledForProject(projectId) && !getCurrentRunView();
357
+ if (!canRetry() || otStartRetryCount >= retryDelays.length) return;
358
+ // Use the existing owned timer so disabling OT, changing projects and
359
+ // starting a new observation all cancel pending retries normally.
360
+ const delayMs = retryDelays[otStartRetryCount++];
361
+ otWarmMirrorState.pollTimer = window.setTimeout(() => {
362
+ otWarmMirrorState.pollTimer = null;
363
+ if (!canRetry()) return;
364
+ syncOtWarmMirrorController().catch(() => {
365
+ if (getCurrentProjectId() === projectId && isExperimentalOtEnabledForProject(projectId)) {
366
+ updateOtStatusDisplay('unavailable');
367
+ }
368
+ });
369
+ }, delayMs);
349
370
  }
350
371
 
351
372
  function readOtBridgeStatus(response) {
352
- if (!response || response.ok === false) {
353
- return 'unavailable';
354
- }
355
- return response.state || response.status || response.result?.state || response.result?.status || 'unavailable';
373
+ return otWarmMirrorController.readOtBridgeStatus(response);
356
374
  }
357
375
 
358
376
  function ensureOtWarmMirrorStateProject(projectId) {
@@ -361,6 +379,7 @@
361
379
  }
362
380
  clearOtEventPolling({ clearPatchQueue: true });
363
381
  otWarmMirrorState.projectId = projectId;
382
+ otStartRetryCount = 0;
364
383
  otWarmMirrorState.lastPatchAt = 0;
365
384
  otWarmMirrorState.lastErrorCode = '';
366
385
  otWarmMirrorState.failureActive = false;
@@ -438,6 +457,7 @@
438
457
  return statusResponse;
439
458
  }
440
459
  const status = readOtBridgeStatus(statusResponse);
460
+ if (status === 'observing' && otWarmMirrorState.lastFailure?.phase === 'start') Object.assign(otWarmMirrorState, { failureActive: false, lastErrorCode: '', lastFailure: null });
441
461
  if (['unavailable', 'inconsistent'].includes(status)) recordOtFailure(readOtBridgeErrorCode(statusResponse, 'ot_observer_unavailable'), 'observe');
442
462
  if (status) {
443
463
  updateOtStatusDisplay(status);
@@ -470,27 +490,11 @@
470
490
  }
471
491
 
472
492
  function readOtBridgeEvents(response) {
473
- if (Array.isArray(response)) {
474
- return response;
475
- }
476
- if (Array.isArray(response?.events)) {
477
- return response.events;
478
- }
479
- if (Array.isArray(response?.result?.events)) {
480
- return response.result.events;
481
- }
482
- return [];
493
+ return otWarmMirrorController.readOtBridgeEvents(response);
483
494
  }
484
-
485
495
  function readOtBridgeErrorCode(response, fallback) {
486
- const value = response?.lastErrorCode
487
- || response?.reason
488
- || response?.error?.code
489
- || response?.error
490
- || fallback;
491
- return typeof value === 'string' ? value : fallback;
496
+ return otWarmMirrorController.readOtBridgeErrorCode(response, fallback);
492
497
  }
493
-
494
498
  function recordOtFailure(code, phase) {
495
499
  code = otWarmMirrorController.normalizeFailureCode(code);
496
500
  otWarmMirrorState.lastErrorCode = code;
@@ -174,6 +174,23 @@
174
174
  return { ok: true };
175
175
  }
176
176
 
177
+ function readOtBridgeStatus(response) {
178
+ if (!response || response.ok === false) return 'unavailable';
179
+ return response.state || response.status || response.result?.state || response.result?.status || 'unavailable';
180
+ }
181
+
182
+ function readOtBridgeEvents(response) {
183
+ if (Array.isArray(response)) return response;
184
+ if (Array.isArray(response?.events)) return response.events;
185
+ if (Array.isArray(response?.result?.events)) return response.result.events;
186
+ return [];
187
+ }
188
+
189
+ function readOtBridgeErrorCode(response, fallback) {
190
+ const value = response?.lastErrorCode || response?.reason || response?.error?.code || response?.error || fallback;
191
+ return typeof value === 'string' ? value : fallback;
192
+ }
193
+
177
194
  function normalizeFailureCode(value, fallback = 'ot_error') {
178
195
  const code = typeof value === 'string' ? value : value?.code;
179
196
  if (/^[a-z][a-z0-9_]{0,79}$/.test(code || '')) return code;
@@ -265,6 +282,9 @@
265
282
  takePatchBatch,
266
283
  validatePatchReceipt,
267
284
  normalizeFailureCode,
285
+ readOtBridgeStatus,
286
+ readOtBridgeEvents,
287
+ readOtBridgeErrorCode,
268
288
  getFailureMessageKey,
269
289
  buildPatchFilesRequest,
270
290
  canUseOtWarmStart,
@@ -21,9 +21,11 @@
21
21
  ['src/page/overleafProjectSnapshot.js', 'codex-overleaf-project-snapshot-script'],
22
22
  ['src/page/pageBridgeCapability.js', 'codex-overleaf-page-bridge-capability-script'],
23
23
  ['src/page/treeOperations.js', 'codex-overleaf-tree-operations-script', true],
24
+ ['src/page/textFileCreator.js', 'codex-overleaf-text-file-creator-script', true],
24
25
  ['src/page/binaryAssetUploader.js', 'codex-overleaf-binary-asset-uploader-script', true],
25
26
  ['src/page/snapshotRouter.js', 'codex-overleaf-snapshot-router-script'],
26
27
  ['src/page/writeGuard.js', 'codex-overleaf-write-guard-script', true],
28
+ ['src/page/trackedChangeCapture.js', 'codex-overleaf-tracked-change-capture-script', true],
27
29
  ['src/page/trackedChangesLifecycle.js', 'codex-overleaf-tracked-changes-lifecycle-script', true],
28
30
  ['src/page/writebackRouter.js', 'codex-overleaf-writeback-router-script', true],
29
31
  ['src/pageBridge.js', 'codex-overleaf-page-bridge-script', true]
@@ -137,15 +137,19 @@
137
137
  }
138
138
  const requestId = ++storageUsageRequestId;
139
139
  const projectId = getCurrentProjectId?.();
140
+ const settingsScope = getPanel?.()?.dataset?.settingsScope;
140
141
  const isCurrent = () => requestId === storageUsageRequestId && projectId === getCurrentProjectId?.()
142
+ && settingsScope === getPanel?.()?.dataset?.settingsScope
141
143
  && getSettingsPanelInstance()?.container?.querySelector('[data-storage-usage]') === node;
142
144
  const current = getState();
143
145
  const sessionCount = Array.isArray(current?.sessions) ? current.sessions.length : 0;
144
146
  const runCount = Array.isArray(current?.runs) ? current.runs.length : 0;
145
- const counts = tx(
146
- `${sessionCount} loaded session(s) in this project · ${runCount} run(s) in the active session`,
147
- `当前项目已加载 ${sessionCount} 个会话 · 当前会话 ${runCount} 轮运行`
148
- );
147
+ const counts = settingsScope === 'account'
148
+ ? tx('All projects in this browser', '当前浏览器中的所有项目')
149
+ : tx(
150
+ `${sessionCount} loaded session(s) in this project · ${runCount} run(s) in the active session`,
151
+ `当前项目已加载 ${sessionCount} 个会话 · 当前会话 ${runCount} 轮运行`
152
+ );
149
153
  // Dynamic text belongs to this renderer, not the loading-label translator.
150
154
  node.removeAttribute?.('data-i18n');
151
155
  node.textContent = counts;