codex-overleaf-link 2.0.0 → 2.1.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 (119) hide show
  1. package/README.md +24 -23
  2. package/extension/bootstrap/manifest.template.json +2 -1
  3. package/extension/runtime-manifest.json +12 -6
  4. package/extension/src/background.js +7 -0
  5. package/extension/src/content/composerPanel.js +13 -3
  6. package/extension/src/content/contentRuntime.js +129 -64
  7. package/extension/src/content/contextTray.js +173 -9
  8. package/extension/src/content/markdownText.js +16 -1
  9. package/extension/src/content/mathText.js +233 -0
  10. package/extension/src/content/modelPicker.js +117 -106
  11. package/extension/src/content/modelPickerSupport.js +115 -0
  12. package/extension/src/content/nativeChannel.js +20 -1
  13. package/extension/src/content/panelMaintenance.js +2 -0
  14. package/extension/src/content/projectSettingsCoordinator.js +2 -0
  15. package/extension/src/content/providerSettingsCoordinator.js +413 -0
  16. package/extension/src/content/providerSettingsDialog.js +981 -0
  17. package/extension/src/content/runTimelineView.js +24 -5
  18. package/extension/src/content/settingsPanel.js +33 -2
  19. package/extension/src/content/writebackOrchestrator.js +3 -1
  20. package/extension/src/page/overleafProjectSnapshot.js +29 -33
  21. package/extension/src/page/snapshotRouter.js +75 -8
  22. package/extension/src/page/writebackRouter.js +20 -33
  23. package/extension/src/pageBridge.js +1 -1
  24. package/extension/src/shared/agentTranscript.js +130 -0
  25. package/extension/src/shared/compatibility.js +3 -2
  26. package/extension/src/shared/failureReasons.js +75 -0
  27. package/extension/src/shared/i18n.js +22 -0
  28. package/extension/src/shared/models.js +1 -0
  29. package/extension/src/shared/providerProfiles.js +283 -0
  30. package/extension/src/shared/sessionState.js +12 -2
  31. package/extension/src/shared/storageDb.js +1 -1
  32. package/extension/styles/panel.css +587 -1
  33. package/extension/vendor/katex/LICENSE +21 -0
  34. package/extension/vendor/katex/VERSION +2 -0
  35. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  36. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  37. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  38. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  39. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  40. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  41. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  42. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  43. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  44. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  45. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  46. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  47. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  48. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  49. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  50. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  51. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  52. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  53. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  54. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  55. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  56. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  57. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  58. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  59. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  60. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  61. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  62. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  63. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  64. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  65. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  66. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  67. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  68. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  69. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  70. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  71. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  72. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  73. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  74. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  75. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  76. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  77. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  78. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  79. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  80. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  81. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  82. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  83. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  84. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  85. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  86. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  87. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  88. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  89. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  90. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  91. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  92. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  93. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  94. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  95. package/extension/vendor/katex/katex.min.css +1 -0
  96. package/extension/vendor/katex/katex.min.js +1 -0
  97. package/native-host/src/anthropicBridgeRequest.js +435 -0
  98. package/native-host/src/anthropicBridgeResponse.js +431 -0
  99. package/native-host/src/anthropicMessagesBridge.js +281 -0
  100. package/native-host/src/chatBridgeRequest.js +274 -0
  101. package/native-host/src/chatBridgeResponse.js +594 -0
  102. package/native-host/src/chatCompletionsBridge.js +485 -0
  103. package/native-host/src/codexCommand.js +25 -0
  104. package/native-host/src/codexPromptAssembly.js +9 -0
  105. package/native-host/src/codexProviderLaunch.js +213 -0
  106. package/native-host/src/codexProviderTest.js +196 -0
  107. package/native-host/src/codexSessionRunner.js +29 -25
  108. package/native-host/src/managedInstall.js +1 -0
  109. package/native-host/src/providerBridgeRoutes.js +11 -0
  110. package/native-host/src/providerModelCatalog.js +107 -0
  111. package/native-host/src/providerProfile.js +454 -0
  112. package/native-host/src/providerReasoning.js +235 -0
  113. package/native-host/src/providerRedaction.js +18 -0
  114. package/native-host/src/providerRuntime.js +365 -0
  115. package/native-host/src/providerStore.js +691 -0
  116. package/native-host/src/readProgressGuard.js +360 -0
  117. package/native-host/src/taskRunnerRuntime.js +39 -3
  118. package/native-host/src/updateArchive.js +1 -0
  119. package/package.json +2 -1
@@ -480,7 +480,11 @@
480
480
  existing.dataset.streamRole = event.streamRole || '';
481
481
  const text = existing.querySelector('[data-stream-text]');
482
482
  if (text) {
483
- renderMarkdownInlineText(text, event.title || '');
483
+ if (event.streamRole === 'assistant') {
484
+ renderMarkdownBlockText(text, event.title || '');
485
+ } else {
486
+ renderMarkdownInlineText(text, event.title || '');
487
+ }
484
488
  }
485
489
  return;
486
490
  }
@@ -498,7 +502,11 @@
498
502
  const text = document.createElement('div');
499
503
  text.className = 'run-stream-text';
500
504
  text.dataset.streamText = '';
501
- renderMarkdownInlineText(text, event.title || '');
505
+ if (event.streamRole === 'assistant') {
506
+ renderMarkdownBlockText(text, event.title || '');
507
+ } else {
508
+ renderMarkdownInlineText(text, event.title || '');
509
+ }
502
510
 
503
511
  row.append(text);
504
512
  return row;
@@ -727,9 +735,7 @@
727
735
  main.className = 'run-final-answer';
728
736
  const mainSections = [];
729
737
  if (structured.conclusion) {
730
- mainSections.push(getLocale() === 'zh'
731
- ? `结论:${structured.conclusion}`
732
- : `Conclusion: ${structured.conclusion}`);
738
+ mainSections.push(formatConclusionMarkdown(structured.conclusion));
733
739
  }
734
740
  if (structured.body) {
735
741
  mainSections.push(structured.body);
@@ -764,6 +770,19 @@
764
770
  return report;
765
771
  }
766
772
 
773
+ function formatConclusionMarkdown(value) {
774
+ const conclusion = String(value || '');
775
+ const label = getLocale() === 'zh' ? '结论:' : 'Conclusion:';
776
+ return hasLeadingMarkdownBlock(conclusion)
777
+ ? `${label}\n${conclusion}`
778
+ : `${label} ${conclusion}`;
779
+ }
780
+
781
+ function hasLeadingMarkdownBlock(value) {
782
+ const firstLine = String(value || '').trimStart().split(/\r?\n/, 1)[0];
783
+ return /^(?:#{1,6}\s+\S|```|~~~|(?:[-+*]|\d+[.)])\s+\S|\*\*[^*]+\*\*:?\s*$)/.test(firstLine);
784
+ }
785
+
767
786
  // For failure codes that have an actionable recovery path the user can
768
787
  // invoke directly from the run card, append a button inside the completion
769
788
  // report. Today's only consumer is codex_project_locked → "Force-release
@@ -147,12 +147,29 @@
147
147
  <p class="codex-set-row-help" data-i18n="languageHelp">Panel display language.</p>
148
148
  </div>
149
149
  </details>
150
+ <details class="codex-set-group" data-set-group="providers" open>
151
+ <summary class="codex-set-group-head">
152
+ <span class="codex-set-group-title">${codexSetIcon('bolt')}<span data-i18n="providerSettingsTitle">Model providers</span><span class="codex-provider-experimental-badge" data-i18n="providerSettingsExperimental">Third-party experimental</span></span>
153
+ </summary>
154
+ <div class="codex-set-card codex-provider-settings-entry">
155
+ <div class="codex-provider-settings-entry-copy">
156
+ <p class="codex-set-row-help" data-i18n="providerSettingsHelp">Configure experimental third-party model APIs. Compatibility varies by provider and gateway.</p>
157
+ <p class="codex-provider-settings-summary" data-provider-settings-summary data-tone="ok" data-i18n="providerSettingsBuiltin">Built-in Codex is active</p>
158
+ </div>
159
+ <button type="button" class="codex-set-btn" data-provider-settings-open data-i18n="providerSettingsConfigure">Configure</button>
160
+ </div>
161
+ </details>
150
162
  <details class="codex-set-group" data-set-group="updates" open>
151
163
  <summary class="codex-set-group-head">
152
164
  <span class="codex-set-group-title">${codexSetIcon('software')}<span data-i18n="softwareUpdatesTitle">Software updates</span></span>
153
165
  </summary>
154
166
  <div class="codex-set-card" data-update-settings>
155
167
  <div class="codex-set-row">
168
+ <label class="codex-project-settings-row codex-project-settings-row--switch">
169
+ <span class="codex-project-settings-row-label" data-i18n="preloadProjectContextTitle">Preload project context</span>
170
+ <input type="checkbox" class="codex-switch" data-preload-project-context>
171
+ </label>
172
+ <p class="codex-set-row-help" data-i18n="preloadProjectContextDescription">Fetch the exact project file list in the background after an Overleaf project becomes idle.</p>
156
173
  <p class="codex-set-row-help" data-update-settings-summary data-i18n="softwareUpdatesHelp">Automatically checks signed stable releases for the Extension and Native Host.</p>
157
174
  <p class="codex-set-note" data-update-settings-state aria-live="polite" hidden></p>
158
175
  <button type="button" class="codex-set-btn" data-check-updates data-i18n="checkForUpdates">Check for updates</button>
@@ -211,6 +228,7 @@
211
228
  container.querySelector('[data-settings-back]')?.addEventListener('click', () => instance.callbacks.onBack?.());
212
229
  container.querySelector('[data-skills-back]')?.addEventListener('click', () => instance.callbacks.onSkillsBack?.());
213
230
  container.querySelector('[data-skills-entry]')?.addEventListener('click', () => instance.callbacks.onSkillsOpen?.());
231
+ container.querySelector('[data-provider-settings-open]')?.addEventListener('click', () => instance.callbacks.onProvidersOpen?.());
214
232
  container.querySelector('[data-clear-all-history]')?.addEventListener('click', () => instance.callbacks.onClearAllHistory?.());
215
233
  container.querySelector('[data-history-card]')?.addEventListener('toggle', event => {
216
234
  if (event.target.open) {
@@ -230,7 +248,7 @@
230
248
  flashSaved(instance, event);
231
249
  });
232
250
  // Governance and skill fields: auto-save on change and input for immediate response.
233
- for (const selector of ['[data-governance-readonly-patterns]', '[data-governance-writable-patterns]', '[data-sensitive-check-enabled]', '[data-sensitive-confirm-allowed]', '[data-load-codex-local-skills]', '[data-load-codex-overleaf-skills]', '[data-theme-select]', '[data-language-select]']) {
251
+ for (const selector of ['[data-governance-readonly-patterns]', '[data-governance-writable-patterns]', '[data-sensitive-check-enabled]', '[data-sensitive-confirm-allowed]', '[data-preload-project-context]', '[data-load-codex-local-skills]', '[data-load-codex-overleaf-skills]', '[data-theme-select]', '[data-language-select]']) {
234
252
  const element = container.querySelector(selector);
235
253
  element?.addEventListener?.('change', event => {
236
254
  instance.callbacks.onInputChange?.(event);
@@ -254,6 +272,7 @@
254
272
  setStatus: (text, status) => setStatus(instance, text, status),
255
273
  clearStatus: () => clearStatus(instance),
256
274
  setSkillsSummary: text => setSkillsSummary(instance, text),
275
+ setProviderSummary: summary => setProviderSummary(instance, summary),
257
276
  refreshNotes: () => updateGovernanceNotes(instance),
258
277
  destroy: () => destroy(instance),
259
278
  _instance: instance
@@ -328,6 +347,7 @@
328
347
  // shared container, so query that instead of a single screen root.
329
348
  const scope = instance?.container || root;
330
349
  const toggles = state.skillToggles || {};
350
+ setChecked(scope, '[data-preload-project-context]', toggles.preloadProjectContext !== false);
331
351
  setChecked(scope, '[data-load-codex-local-skills]', toggles.loadCodexLocalSkills !== false);
332
352
  setChecked(scope, '[data-load-codex-overleaf-skills]', toggles.loadCodexOverleafSkills !== false);
333
353
  setValue(scope, '[data-theme-select]', state.theme || 'dark');
@@ -349,6 +369,7 @@
349
369
  sensitiveConfirmAllowed: root?.querySelector('[data-sensitive-confirm-allowed]')?.checked === true
350
370
  },
351
371
  skillToggles: {
372
+ preloadProjectContext: scope?.querySelector('[data-preload-project-context]')?.checked !== false,
352
373
  loadCodexLocalSkills: scope?.querySelector('[data-load-codex-local-skills]')?.checked !== false,
353
374
  loadCodexOverleafSkills: scope?.querySelector('[data-load-codex-overleaf-skills]')?.checked !== false
354
375
  },
@@ -467,6 +488,15 @@
467
488
  }
468
489
  }
469
490
 
491
+ function setProviderSummary(instance, value = {}) {
492
+ const element = instance?.container?.querySelector('[data-provider-settings-summary]');
493
+ if (!element) {
494
+ return;
495
+ }
496
+ element.textContent = typeof value === 'string' ? value : value.summary || '';
497
+ element.dataset.tone = typeof value === 'object' && value.tone ? value.tone : 'ok';
498
+ }
499
+
470
500
  // Collapse memory: card open/closed state persists per browser profile.
471
501
  const GROUP_STORE_KEY = 'codexOverleafSettingsGroups';
472
502
 
@@ -535,6 +565,7 @@
535
565
  isVisible,
536
566
  setStatus,
537
567
  clearStatus,
538
- setSkillsSummary
568
+ setSkillsSummary,
569
+ setProviderSummary
539
570
  };
540
571
  })();
@@ -544,7 +544,9 @@
544
544
  status: 'running'
545
545
  });
546
546
 
547
- await callPageBridge('invalidateProjectSnapshot', {});
547
+ await callPageBridge('invalidateProjectSnapshot', {
548
+ invalidateFileList: true
549
+ });
548
550
  const freshProject = sanitizeRunProjectSnapshot(await callPageBridge('getProjectSnapshot', {
549
551
  force: true,
550
552
  maxAgeMs: 0,
@@ -14,7 +14,7 @@
14
14
  function create(deps = {}) {
15
15
  const pageWindow = deps.window || window;
16
16
  const snapshotCache = createKeyedCache();
17
- const fileListCache = createCache();
17
+ const fileListCache = createKeyedCache();
18
18
 
19
19
  async function getProjectSnapshot(params = {}) {
20
20
  const cacheKey = getSnapshotCacheKey(params);
@@ -62,41 +62,53 @@
62
62
  const cacheKey = getProjectFileListCacheKey(params);
63
63
  const maxAgeMs = normalizeFileListMaxAge(params.maxAgeMs);
64
64
  const now = Date.now();
65
+ const cacheEntry = fileListCache.entries.get(cacheKey);
65
66
 
66
- if (fileListCache.key === cacheKey && fileListCache.pending) {
67
- return fileListCache.pending;
67
+ if (cacheEntry?.pending) {
68
+ return cacheEntry.pending;
68
69
  }
69
70
 
70
- if (!params.force && fileListCache.key === cacheKey && fileListCache.value && now - fileListCache.capturedAt <= maxAgeMs) {
71
- return withFileListCacheMetadata(fileListCache.value, 'memory', fileListCache.capturedAt);
71
+ if (!params.force && cacheEntry?.value && now - cacheEntry.capturedAt <= maxAgeMs) {
72
+ return withFileListCacheMetadata(cacheEntry.value, 'memory', cacheEntry.capturedAt);
72
73
  }
73
74
 
74
75
  const pending = deps.buildProjectFileList(params)
75
76
  .then(fileList => {
76
77
  const capturedAt = Date.now();
77
- if (shouldCacheProjectFileList(fileList, params)) {
78
- fileListCache.key = cacheKey;
79
- fileListCache.value = fileList;
80
- fileListCache.capturedAt = capturedAt;
81
- } else if (fileListCache.key === cacheKey) {
82
- fileListCache.value = null;
83
- fileListCache.capturedAt = 0;
78
+ const currentEntry = fileListCache.entries.get(cacheKey);
79
+ if (currentEntry?.pending === pending) {
80
+ if (shouldCacheProjectFileList(fileList, params)) {
81
+ fileListCache.entries.set(cacheKey, {
82
+ value: fileList,
83
+ capturedAt,
84
+ pending: null
85
+ });
86
+ } else {
87
+ fileListCache.entries.delete(cacheKey);
88
+ }
84
89
  }
85
90
  return withFileListCacheMetadata(fileList, 'fresh', capturedAt);
86
91
  })
87
92
  .finally(() => {
88
- if (fileListCache.pending === pending) {
89
- fileListCache.pending = null;
93
+ const currentEntry = fileListCache.entries.get(cacheKey);
94
+ if (currentEntry?.pending === pending) {
95
+ currentEntry.pending = null;
90
96
  }
91
97
  });
92
98
 
93
- fileListCache.key = cacheKey;
94
- fileListCache.pending = pending;
99
+ fileListCache.entries.set(cacheKey, {
100
+ value: cacheEntry?.value || null,
101
+ capturedAt: cacheEntry?.capturedAt || 0,
102
+ pending
103
+ });
95
104
  return pending;
96
105
  }
97
106
 
98
- function invalidateProjectSnapshot() {
107
+ function invalidateProjectSnapshot(options = {}) {
99
108
  resetKeyedCache(snapshotCache);
109
+ if (options.invalidateFileList === true) {
110
+ resetKeyedCache(fileListCache);
111
+ }
100
112
  }
101
113
 
102
114
  function getSnapshotCacheKey(params = {}) {
@@ -143,15 +155,6 @@
143
155
  return fileList?.capabilities?.method === 'overleaf-zip-file-list';
144
156
  }
145
157
 
146
- function createCache() {
147
- return {
148
- key: '',
149
- capturedAt: 0,
150
- value: null,
151
- pending: null
152
- };
153
- }
154
-
155
158
  function createKeyedCache() {
156
159
  return {
157
160
  entries: new Map()
@@ -162,13 +165,6 @@
162
165
  cache.entries.clear();
163
166
  }
164
167
 
165
- function resetCache(cache) {
166
- cache.key = '';
167
- cache.capturedAt = 0;
168
- cache.value = null;
169
- cache.pending = null;
170
- }
171
-
172
168
  function normalizeSnapshotMaxAge(value) {
173
169
  if (value === 0) {
174
170
  return 0;
@@ -21,6 +21,7 @@
21
21
  ? deps.readActiveEditorText
22
22
  : () => '';
23
23
  let invalidatedAt = 0;
24
+ let preferredZipEndpoint = '';
24
25
 
25
26
  function getProjectId() {
26
27
  return treeOperations.getProjectId?.() || null;
@@ -100,6 +101,7 @@
100
101
  skipped: [],
101
102
  diagnostics: {
102
103
  zipEndpoint: zipSnapshot.endpoint,
104
+ zipTiming: zipSnapshot.diagnostics,
103
105
  docRecordCount: internalDocRecords.length,
104
106
  docRecords: internalDocRecords.slice(0, 8)
105
107
  },
@@ -123,6 +125,7 @@
123
125
  reason: zipSnapshot.reason || 'Overleaf source ZIP was unavailable'
124
126
  }],
125
127
  diagnostics: {
128
+ zipTiming: zipSnapshot.diagnostics,
126
129
  docRecordCount: internalDocRecords.length,
127
130
  docRecords: internalDocRecords.slice(0, 8)
128
131
  },
@@ -257,6 +260,7 @@
257
260
  skipped: zipSnapshot.skipped || [],
258
261
  diagnostics: {
259
262
  zipEndpoint: zipSnapshot.endpoint,
263
+ zipTiming: zipSnapshot.diagnostics,
260
264
  docRecordCount: internalDocRecords.length,
261
265
  docRecords: internalDocRecords.slice(0, 8)
262
266
  },
@@ -460,6 +464,7 @@
460
464
  skipped: zipSnapshot.skipped || [],
461
465
  diagnostics: {
462
466
  zipEndpoint: zipSnapshot.endpoint,
467
+ zipTiming: zipSnapshot.diagnostics,
463
468
  docRecordCount: 0,
464
469
  docRecords: []
465
470
  },
@@ -640,24 +645,60 @@
640
645
  `${window.location.origin}/project/${encodeURIComponent(projectId)}/download/zip`,
641
646
  `${window.location.origin}/download/project/${encodeURIComponent(projectId)}`
642
647
  ];
648
+ const orderedEndpoints = preferredZipEndpoint && endpoints.includes(preferredZipEndpoint)
649
+ ? [preferredZipEndpoint, ...endpoints.filter(endpoint => endpoint !== preferredZipEndpoint)]
650
+ : endpoints;
643
651
 
652
+ const startedAt = Date.now();
653
+ const deadline = startedAt + timeoutMs;
644
654
  const errors = [];
645
- for (const endpoint of endpoints) {
655
+ const attempts = [];
656
+ for (const endpoint of orderedEndpoints) {
657
+ const remainingMs = deadline - Date.now();
658
+ if (remainingMs <= 0) {
659
+ errors.push(`Overleaf ZIP download exhausted its ${timeoutMs}ms total timeout budget`);
660
+ break;
661
+ }
662
+ const attemptStartedAt = Date.now();
646
663
  try {
647
- const { response, contentType, buffer } = await fetchZipEndpoint(endpoint, timeoutMs);
664
+ const { response, contentType, buffer, timing } = await fetchZipEndpoint(endpoint, remainingMs);
648
665
  if (!response.ok) {
666
+ attempts.push({
667
+ endpoint,
668
+ status: response.status,
669
+ elapsedMs: Date.now() - attemptStartedAt
670
+ });
649
671
  errors.push(`${endpoint} returned ${response.status}`);
650
672
  continue;
651
673
  }
674
+ const extractStartedAt = Date.now();
652
675
  const extracted = await extractFilesFromZip(buffer, {
653
676
  includeBinaryFiles: Boolean(params.includeBinaryFiles),
654
677
  includeContent: params.includeContent !== false
655
678
  });
679
+ const extractMs = Date.now() - extractStartedAt;
656
680
  const files = extracted.files || [];
657
681
  if (!files.length) {
682
+ attempts.push({
683
+ endpoint,
684
+ status: response.status,
685
+ elapsedMs: Date.now() - attemptStartedAt,
686
+ downloadMs: timing.totalMs,
687
+ extractMs
688
+ });
658
689
  errors.push(`${endpoint} returned no text files (${contentType || 'unknown content type'})`);
659
690
  continue;
660
691
  }
692
+ preferredZipEndpoint = endpoint;
693
+ attempts.push({
694
+ endpoint,
695
+ status: response.status,
696
+ elapsedMs: Date.now() - attemptStartedAt,
697
+ timeToHeadersMs: timing.timeToHeadersMs,
698
+ downloadMs: timing.totalMs,
699
+ extractMs,
700
+ bytes: buffer?.byteLength || 0
701
+ });
661
702
  return {
662
703
  ok: true,
663
704
  endpoint,
@@ -665,22 +706,42 @@
665
706
  files: files.map(file => ({
666
707
  ...file,
667
708
  source: 'overleaf-zip'
668
- }))
709
+ })),
710
+ diagnostics: {
711
+ totalMs: Date.now() - startedAt,
712
+ timeoutBudgetMs: timeoutMs,
713
+ attempts
714
+ }
669
715
  };
670
716
  } catch (error) {
717
+ attempts.push({
718
+ endpoint,
719
+ error: error.message,
720
+ elapsedMs: Date.now() - attemptStartedAt
721
+ });
671
722
  errors.push(`${endpoint} failed: ${error.message}`);
723
+ if (error?.code === 'zip_timeout') {
724
+ errors.push(`Overleaf ZIP download exhausted its ${timeoutMs}ms total timeout budget`);
725
+ break;
726
+ }
672
727
  }
673
728
  }
674
729
 
675
730
  return {
676
731
  ok: false,
677
- reason: errors.join('; ') || 'Overleaf source ZIP was unavailable'
732
+ reason: errors.join('; ') || 'Overleaf source ZIP was unavailable',
733
+ diagnostics: {
734
+ totalMs: Date.now() - startedAt,
735
+ timeoutBudgetMs: timeoutMs,
736
+ attempts
737
+ }
678
738
  };
679
739
  }
680
740
 
681
741
  function fetchZipEndpoint(endpoint, timeoutMs) {
682
742
  const controller = typeof AbortController === 'function' ? new AbortController() : null;
683
743
  const request = (async () => {
744
+ const startedAt = Date.now();
684
745
  const response = await fetch(endpoint, {
685
746
  credentials: 'include',
686
747
  headers: {
@@ -688,12 +749,17 @@
688
749
  },
689
750
  ...(controller ? { signal: controller.signal } : {})
690
751
  });
752
+ const headersAt = Date.now();
691
753
  const contentType = response.headers.get('content-type') || '';
692
754
  const buffer = response.ok ? await response.arrayBuffer() : null;
693
755
  return {
694
756
  response,
695
757
  contentType,
696
- buffer
758
+ buffer,
759
+ timing: {
760
+ timeToHeadersMs: headersAt - startedAt,
761
+ totalMs: Date.now() - startedAt
762
+ }
697
763
  };
698
764
  })();
699
765
 
@@ -706,11 +772,12 @@
706
772
  let timeoutId = null;
707
773
  const timeout = new Promise((_resolve, reject) => {
708
774
  timeoutId = window.setTimeout(() => {
775
+ const timeoutError = new Error(message);
776
+ timeoutError.code = 'zip_timeout';
777
+ reject(timeoutError);
709
778
  try {
710
779
  onTimeout?.();
711
- } finally {
712
- reject(new Error(message));
713
- }
780
+ } catch (_error) {}
714
781
  }, timeoutMs);
715
782
  });
716
783
 
@@ -879,55 +879,42 @@
879
879
  const previousEditorIdentity = getActiveEditorIdentity();
880
880
  const previousSignature = contentSignature(currentText);
881
881
 
882
- // §9.1 target_file_not_found: if the target path is not visible in the
883
- // Overleaf project file tree, refuse the write before attempting to open.
884
- // openFileByPath itself would fail anyway, but emitting the canonical
885
- // path-not-found code (rather than a generic open failure) gives the run
886
- // card / final report a more actionable presentation per §17.3.1.
887
- // Only fires when the tree-operations layer actually exposes a
888
- // projectPathExists hook; older harnesses without the hook fall through
889
- // to the openFileByPath path and surface a generic open failure.
890
- if (operation?.type === 'edit' && hasProjectPathExistsHook() && !projectPathExists(filePath)) {
891
- return {
892
- ok: false,
893
- code: 'path_not_found',
894
- reason: `Cannot edit ${filePath}; the path is not in the Overleaf project file tree.`,
895
- failure: buildPageFailure('target_file_not_found', {
896
- file: filePath,
897
- activeFile: initialActivePath || '',
898
- operationType: operation?.type || 'edit',
899
- changedDocument: false,
900
- userMessage: `Codex could not find ${filePath} in this Overleaf project.`,
901
- evidence: {
902
- originalCode: 'path_not_found',
903
- initialActivePath: initialActivePath || '',
904
- writeStarted: false
905
- }
906
- })
907
- };
908
- }
909
-
910
882
  const opened = await openFileByPath(filePath, { force: initialActivePath === filePath });
911
883
  if (!opened.ok) {
912
- const reasonText = `Cannot edit ${filePath}; active file is ${initialActivePath || 'unknown'}; ${opened.reason}`;
884
+ // A collapsed folder can make a real nested file temporarily invisible
885
+ // to projectPathExists(). openFileByPath() owns the side-effecting
886
+ // expand-and-open attempt, so only classify the path as missing after
887
+ // that attempt has failed and the path still cannot be observed.
888
+ const pathMissing = operation?.type === 'edit'
889
+ && hasProjectPathExistsHook()
890
+ && !projectPathExists(filePath);
891
+ const resultCode = pathMissing ? 'path_not_found' : 'file_open_failed';
892
+ const failureCode = pathMissing ? 'target_file_not_found' : 'target_file_open_failed';
893
+ const reasonText = pathMissing
894
+ ? `Cannot edit ${filePath}; the path was not found after Overleaf tried to open it.`
895
+ : `Cannot edit ${filePath}; active file is ${initialActivePath || 'unknown'}; ${opened.reason}`;
913
896
  return {
914
897
  ok: false,
915
- code: 'file_open_failed',
916
- debug: buildWritebackDebug('open_file_failed', operation, baseFileLookup, currentText, {
898
+ code: resultCode,
899
+ debug: buildWritebackDebug(pathMissing ? 'open_file_path_not_found' : 'open_file_failed', operation, baseFileLookup, currentText, {
917
900
  initialActivePath,
918
901
  opened
919
902
  }),
920
903
  reason: reasonText,
921
- failure: buildPageFailure('target_file_open_failed', {
904
+ failure: buildPageFailure(failureCode, {
922
905
  file: filePath,
923
906
  activeFile: initialActivePath || '',
924
907
  operationType: operation?.type || 'edit',
925
908
  changedDocument: false,
926
- userMessage: `Codex could not open ${filePath} in Overleaf.`,
909
+ userMessage: pathMissing
910
+ ? `Codex could not find ${filePath} in this Overleaf project.`
911
+ : `Codex could not open ${filePath} in Overleaf.`,
927
912
  technicalMessage: opened.reason || '',
928
913
  evidence: {
914
+ originalCode: resultCode,
929
915
  initialActivePath: initialActivePath || '',
930
916
  openMethod: opened.method || '',
917
+ openAttempted: true,
931
918
  writeStarted: false
932
919
  }
933
920
  })
@@ -202,7 +202,7 @@
202
202
  return projectSnapshotBridge.getProjectFileList(params);
203
203
  }
204
204
  if (method === 'invalidateProjectSnapshot') {
205
- projectSnapshotBridge.invalidateProjectSnapshot();
205
+ projectSnapshotBridge.invalidateProjectSnapshot(params);
206
206
  return { ok: true };
207
207
  }
208
208
  if (method === 'createCheckpoint') {