codex-overleaf-link 1.9.13 → 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 (122) hide show
  1. package/README.md +24 -23
  2. package/extension/bootstrap/manifest.template.json +2 -1
  3. package/extension/runtime-manifest.json +13 -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 +130 -65
  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/trackedChangesLifecycle.js +154 -26
  23. package/extension/src/page/writebackRouter.js +31 -60
  24. package/extension/src/pageBridge.js +1 -1
  25. package/extension/src/shared/agentTranscript.js +130 -0
  26. package/extension/src/shared/compatibility.js +10 -7
  27. package/extension/src/shared/failureReasons.js +75 -0
  28. package/extension/src/shared/i18n.js +22 -0
  29. package/extension/src/shared/models.js +1 -0
  30. package/extension/src/shared/providerProfiles.js +283 -0
  31. package/extension/src/shared/sessionState.js +12 -2
  32. package/extension/src/shared/storageDb.js +27 -19
  33. package/extension/src/shared/storageRunActions.js +99 -0
  34. package/extension/styles/panel.css +587 -1
  35. package/extension/vendor/katex/LICENSE +21 -0
  36. package/extension/vendor/katex/VERSION +2 -0
  37. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  38. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  39. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  40. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  41. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  42. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  43. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  44. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  45. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  46. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  47. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  48. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  49. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  50. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  51. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  52. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  53. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  54. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  55. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  56. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  57. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  58. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  59. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  60. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  61. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  62. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  63. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  64. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  65. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  66. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  67. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  68. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  69. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  70. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  71. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  72. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  73. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  74. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  75. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  76. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  77. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  78. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  79. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  80. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  81. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  82. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  83. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  84. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  85. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  86. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  87. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  88. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  89. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  90. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  91. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  92. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  93. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  94. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  95. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  96. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  97. package/extension/vendor/katex/katex.min.css +1 -0
  98. package/extension/vendor/katex/katex.min.js +1 -0
  99. package/native-host/src/anthropicBridgeRequest.js +435 -0
  100. package/native-host/src/anthropicBridgeResponse.js +431 -0
  101. package/native-host/src/anthropicMessagesBridge.js +281 -0
  102. package/native-host/src/chatBridgeRequest.js +274 -0
  103. package/native-host/src/chatBridgeResponse.js +594 -0
  104. package/native-host/src/chatCompletionsBridge.js +485 -0
  105. package/native-host/src/codexCommand.js +25 -0
  106. package/native-host/src/codexPromptAssembly.js +9 -0
  107. package/native-host/src/codexProviderLaunch.js +213 -0
  108. package/native-host/src/codexProviderTest.js +196 -0
  109. package/native-host/src/codexSessionRunner.js +29 -25
  110. package/native-host/src/managedInstall.js +1 -0
  111. package/native-host/src/providerBridgeRoutes.js +11 -0
  112. package/native-host/src/providerModelCatalog.js +107 -0
  113. package/native-host/src/providerProfile.js +454 -0
  114. package/native-host/src/providerReasoning.js +235 -0
  115. package/native-host/src/providerRedaction.js +18 -0
  116. package/native-host/src/providerRuntime.js +365 -0
  117. package/native-host/src/providerStore.js +691 -0
  118. package/native-host/src/readProgressGuard.js +360 -0
  119. package/native-host/src/taskRunnerRuntime.js +40 -4
  120. package/native-host/src/updateArchive.js +1 -0
  121. package/native-host/src/updateTrust.js +2 -1
  122. package/package.json +2 -1
@@ -88,7 +88,9 @@
88
88
  skipped
89
89
  };
90
90
  }
91
- const snapshotUndo = await restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles);
91
+ const snapshotUndo = await restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles, {
92
+ runProjectId: params.runProjectId
93
+ });
92
94
  if (snapshotUndo.attempted) {
93
95
  applied.push(...snapshotUndo.applied);
94
96
  skipped.push(...snapshotUndo.skipped);
@@ -291,6 +293,7 @@
291
293
  .filter(file => file?.path && typeof file.content === 'string')
292
294
  .map(file => [normalizeSafeProjectPath(file.path), file.content])
293
295
  .filter(([path]) => path));
296
+ let snapshotContextRebased = false;
294
297
  const paths = Array.from(expectedByPath.keys()).filter(path => postByPath.has(path));
295
298
  if (!paths.length) {
296
299
  return {
@@ -313,30 +316,63 @@
313
316
  // the reject path relies on (its primary path).
314
317
  const appliedBeforeUndo = applied.length;
315
318
  const editorUndo = await rejectTrackedChangesViaEditorUndo(expectedFiles, postFiles, applied);
319
+ let undoReady = editorUndo.ok === true;
320
+ let snapshotUndo = null;
321
+ // A page refresh preserves the run checkpoint but clears CodeMirror's
322
+ // in-memory undo history. When the native editor undo made no progress,
323
+ // reuse Undo's guarded snapshot path: it first verifies that every file is
324
+ // still exactly at this run's post-write content, then restores the
325
+ // persisted pre-write content with Track Changes disabled. The normal
326
+ // Accept replay below can then apply the post-write patch untracked.
327
+ if (!undoReady && applied.length === appliedBeforeUndo) {
328
+ snapshotUndo = await restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles, {
329
+ runProjectId: params.runProjectId
330
+ });
331
+ if (snapshotUndo.ok) {
332
+ applied.push(...snapshotUndo.applied);
333
+ for (const file of snapshotUndo.rebasedExpectedFiles || []) {
334
+ expectedByPath.set(file.path, file.content);
335
+ }
336
+ for (const file of snapshotUndo.rebasedPostFiles || []) {
337
+ if (postByPath.get(file.path) !== file.content) {
338
+ snapshotContextRebased = true;
339
+ }
340
+ postByPath.set(file.path, file.content);
341
+ }
342
+ undoReady = true;
343
+ }
344
+ }
316
345
  pushDiagnostic('editorUndo', {
317
- ok: editorUndo.ok === true,
346
+ ok: undoReady,
318
347
  attempted: editorUndo.attempted === true,
319
348
  code: editorUndo.code || '',
320
349
  reason: editorUndo.reason || '',
321
- pathsProcessed: applied.length - appliedBeforeUndo
350
+ pathsProcessed: applied.length - appliedBeforeUndo,
351
+ snapshotFallbackAttempted: snapshotUndo?.attempted === true,
352
+ snapshotFallbackOk: snapshotUndo?.ok === true,
353
+ snapshotFallbackApplied: snapshotUndo?.applied?.length || 0,
354
+ snapshotFallbackSkipped: snapshotUndo?.skipped?.length || 0
322
355
  });
323
- if (!editorUndo.ok) {
324
- // Drift bail: the editor-undo could not reach the pre-write state. Do NOT
325
- // re-write — that would only make the document worse. Return not-ok so the
326
- // run stays pending and the user can retry, same as Undo.
356
+ if (!undoReady) {
357
+ // Drift or a partial editor undo still bails. The snapshot fallback is
358
+ // allowed only when editor undo made no progress, and its own post-state
359
+ // verification prevents overwriting edits made after this run.
360
+ const snapshotFailure = snapshotUndo?.skipped?.[0]?.result;
327
361
  return {
328
362
  ok: false,
329
- applied: [],
330
- skipped: [{
331
- trackedChange: null,
332
- result: editorUndo.attempted || editorUndo.code
333
- ? editorUndo
334
- : {
335
- ok: false,
336
- code: 'accept_editor_undo_unavailable',
337
- reason: '没有可执行的 Overleaf 原生撤销来清掉本轮留痕;Codex 没有接受这轮改动。'
338
- }
339
- }],
363
+ applied: snapshotUndo?.applied || [],
364
+ skipped: snapshotUndo?.skipped?.length
365
+ ? snapshotUndo.skipped
366
+ : [{
367
+ trackedChange: null,
368
+ result: snapshotFailure || (editorUndo.attempted || editorUndo.code
369
+ ? editorUndo
370
+ : {
371
+ ok: false,
372
+ code: 'accept_editor_undo_unavailable',
373
+ reason: '没有可执行的 Overleaf 原生撤销或安全快照回滚来清掉本轮留痕;Codex 没有接受这轮改动。'
374
+ })
375
+ }],
340
376
  diagnostics
341
377
  };
342
378
  }
@@ -417,7 +453,12 @@
417
453
  // applyOperationsCore — never a whole-file replaceAll, which would clobber
418
454
  // any unrelated content and produce one giant tracked change if anything
419
455
  // about the mode switch were imperfect.
420
- const operationsResult = buildAcceptReplayOperations(paths, expectedByPath, postByPath, params.appliedOperations);
456
+ const operationsResult = buildAcceptReplayOperations(
457
+ paths,
458
+ expectedByPath,
459
+ postByPath,
460
+ snapshotContextRebased ? [] : params.appliedOperations
461
+ );
421
462
  if (!operationsResult.ok) {
422
463
  if (weToggledOff) {
423
464
  await setReviewingEnabled(true, { waitMs: 1800 });
@@ -975,7 +1016,7 @@
975
1016
  return { ok: true, attempted: true };
976
1017
  }
977
1018
 
978
- async function restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles) {
1019
+ async function restoreExpectedFilesWithNoTraceUndo(expectedFiles, postFiles, options = {}) {
979
1020
  const expectedByPath = new Map((expectedFiles || [])
980
1021
  .filter(file => file?.path && typeof file.content === 'string')
981
1022
  .map(file => [normalizeSafeProjectPath(file.path), file.content])
@@ -985,12 +1026,16 @@
985
1026
  .map(file => [normalizeSafeProjectPath(file.path), file.content])
986
1027
  .filter(([path]) => path));
987
1028
  const paths = Array.from(expectedByPath.keys()).filter(path => postByPath.has(path));
1029
+ const rebasedExpectedByPath = new Map();
1030
+ const rebasedPostByPath = new Map();
988
1031
  if (!paths.length) {
989
1032
  return {
990
1033
  ok: false,
991
1034
  attempted: false,
992
1035
  applied: [],
993
- skipped: []
1036
+ skipped: [],
1037
+ rebasedExpectedFiles: [],
1038
+ rebasedPostFiles: []
994
1039
  };
995
1040
  }
996
1041
 
@@ -1008,30 +1053,40 @@
1008
1053
  };
1009
1054
  }
1010
1055
  }
1011
- const ready = await waitForActiveEditorExpectedText(path, postByPath.get(path), 1500);
1056
+ const ready = await waitForActiveEditorCheckpoint(
1057
+ path,
1058
+ expectedByPath.get(path),
1059
+ postByPath.get(path),
1060
+ 1500
1061
+ );
1012
1062
  if (!ready.ok) {
1013
1063
  return {
1014
1064
  ok: false,
1015
1065
  attempted: false,
1016
1066
  applied: [],
1017
1067
  skipped: [],
1068
+ rebasedExpectedFiles: [],
1069
+ rebasedPostFiles: [],
1018
1070
  code: 'snapshot_undo_current_mismatch',
1019
1071
  reason: `${path} 当前内容已经不是本轮写入后的内容;为避免覆盖你的后续修改,Codex 不执行快照撤销。`
1020
1072
  };
1021
1073
  }
1074
+ rebasedExpectedByPath.set(path, ready.expectedContent);
1075
+ rebasedPostByPath.set(path, ready.postContent);
1022
1076
  }
1023
1077
 
1024
1078
  const operations = paths.map(path => ({
1025
1079
  type: 'edit',
1026
1080
  path,
1027
- replaceAll: expectedByPath.get(path),
1081
+ replaceAll: rebasedExpectedByPath.get(path),
1028
1082
  reason: 'Undo tracked edit'
1029
1083
  }));
1030
1084
  const result = await applyOperationsWithNoTraceUndo(operations, {
1031
1085
  baseFiles: paths.map(path => ({
1032
1086
  path,
1033
- content: postByPath.get(path)
1034
- }))
1087
+ content: rebasedPostByPath.get(path)
1088
+ })),
1089
+ runProjectId: typeof options.runProjectId === 'string' ? options.runProjectId : ''
1035
1090
  });
1036
1091
  const toTrackedResult = item => ({
1037
1092
  trackedChange: {
@@ -1046,7 +1101,66 @@
1046
1101
  ok: !(result.skipped || []).length,
1047
1102
  attempted: true,
1048
1103
  applied: (result.applied || []).map(toTrackedResult),
1049
- skipped: (result.skipped || []).map(toTrackedResult)
1104
+ skipped: (result.skipped || []).map(toTrackedResult),
1105
+ rebasedExpectedFiles: paths.map(path => ({
1106
+ path,
1107
+ content: rebasedExpectedByPath.get(path)
1108
+ })),
1109
+ rebasedPostFiles: paths.map(path => ({
1110
+ path,
1111
+ content: rebasedPostByPath.get(path)
1112
+ }))
1113
+ };
1114
+ }
1115
+
1116
+ async function waitForActiveEditorCheckpoint(filePath, expectedContent, postContent, timeoutMs) {
1117
+ const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
1118
+ let actual = readActiveEditorText();
1119
+ while (Date.now() < deadline) {
1120
+ const activeFileMatches = !filePath || getActiveFilePath() === filePath;
1121
+ actual = readActiveEditorText();
1122
+ if (activeFileMatches) {
1123
+ const rebased = rebaseCheckpointPair(actual, expectedContent, postContent);
1124
+ if (rebased.ok) {
1125
+ return rebased;
1126
+ }
1127
+ }
1128
+ await delay(60);
1129
+ }
1130
+ return {
1131
+ ok: false,
1132
+ text: actual
1133
+ };
1134
+ }
1135
+
1136
+ function rebaseCheckpointPair(actualContent, expectedContent, postContent) {
1137
+ const actual = String(actualContent ?? '');
1138
+ const expected = String(expectedContent ?? '');
1139
+ const post = String(postContent ?? '');
1140
+ if (actual === post) {
1141
+ return {
1142
+ ok: true,
1143
+ expectedContent: expected,
1144
+ postContent: post,
1145
+ prefixLength: 0,
1146
+ suffixLength: 0
1147
+ };
1148
+ }
1149
+ if (!post) {
1150
+ return { ok: false };
1151
+ }
1152
+ const matchIndex = actual.indexOf(post);
1153
+ if (matchIndex < 0 || actual.indexOf(post, matchIndex + 1) >= 0) {
1154
+ return { ok: false };
1155
+ }
1156
+ const prefix = actual.slice(0, matchIndex);
1157
+ const suffix = actual.slice(matchIndex + post.length);
1158
+ return {
1159
+ ok: true,
1160
+ expectedContent: prefix + expected + suffix,
1161
+ postContent: actual,
1162
+ prefixLength: prefix.length,
1163
+ suffixLength: suffix.length
1050
1164
  };
1051
1165
  }
1052
1166
 
@@ -1319,6 +1433,20 @@
1319
1433
 
1320
1434
 
1321
1435
 
1436
+ function diffTrackedChangeRefs(before = [], after = []) {
1437
+ const beforeKeys = new Set((before || []).map(ref => ref.key).filter(Boolean));
1438
+ const seen = new Set();
1439
+ const added = [];
1440
+ for (const ref of after || []) {
1441
+ if (!ref?.key || beforeKeys.has(ref.key) || seen.has(ref.key)) {
1442
+ continue;
1443
+ }
1444
+ seen.add(ref.key);
1445
+ added.push(ref);
1446
+ }
1447
+ return added;
1448
+ }
1449
+
1322
1450
  async function waitForTrackedChangeDiff(trackedBefore, paths, options = {}) {
1323
1451
  const waitMs = Number.isFinite(Number(options.waitMs)) ? Number(options.waitMs) : 3000;
1324
1452
  const intervalMs = Number.isFinite(Number(options.intervalMs)) ? Number(options.intervalMs) : 180;
@@ -791,19 +791,17 @@
791
791
  }
792
792
  const trackedChanges = [];
793
793
  if (observeTrackedChanges) {
794
- const trackedDiff = forbidTrackedChanges
795
- ? await waitForTrackedChangeDiff(trackedBefore, operationPaths, {
796
- waitMs: 3600,
797
- intervalMs: 180
798
- })
799
- : (() => null)();
800
- if (trackedDiff) {
801
- trackedChanges.push(...trackedDiff.trackedChanges);
802
- } else {
803
- await delay(120);
804
- const trackedAfter = collectTrackedChangeRefsForPaths(operationPaths);
805
- trackedChanges.push(...diffTrackedChangeRefs(trackedBefore, trackedAfter));
806
- }
794
+ // Overleaf renders review markers asynchronously after the editor text
795
+ // has already changed. A single 120 ms snapshot intermittently missed
796
+ // those markers, leaving a Reviewing write with Undo but no Accept.
797
+ // Poll both paths: normal Reviewing writes get a shorter capture window,
798
+ // while Accept replay keeps the longer safety window used to prove that
799
+ // it did not create fresh tracked changes.
800
+ const trackedDiff = await waitForTrackedChangeDiff(trackedBefore, operationPaths, {
801
+ waitMs: forbidTrackedChanges ? 3600 : 1800,
802
+ intervalMs: 180
803
+ });
804
+ trackedChanges.push(...trackedDiff.trackedChanges);
807
805
  if (forbidTrackedChanges && trackedChanges.length > 0) {
808
806
  return {
809
807
  ok: false,
@@ -881,55 +879,42 @@
881
879
  const previousEditorIdentity = getActiveEditorIdentity();
882
880
  const previousSignature = contentSignature(currentText);
883
881
 
884
- // §9.1 target_file_not_found: if the target path is not visible in the
885
- // Overleaf project file tree, refuse the write before attempting to open.
886
- // openFileByPath itself would fail anyway, but emitting the canonical
887
- // path-not-found code (rather than a generic open failure) gives the run
888
- // card / final report a more actionable presentation per §17.3.1.
889
- // Only fires when the tree-operations layer actually exposes a
890
- // projectPathExists hook; older harnesses without the hook fall through
891
- // to the openFileByPath path and surface a generic open failure.
892
- if (operation?.type === 'edit' && hasProjectPathExistsHook() && !projectPathExists(filePath)) {
893
- return {
894
- ok: false,
895
- code: 'path_not_found',
896
- reason: `Cannot edit ${filePath}; the path is not in the Overleaf project file tree.`,
897
- failure: buildPageFailure('target_file_not_found', {
898
- file: filePath,
899
- activeFile: initialActivePath || '',
900
- operationType: operation?.type || 'edit',
901
- changedDocument: false,
902
- userMessage: `Codex could not find ${filePath} in this Overleaf project.`,
903
- evidence: {
904
- originalCode: 'path_not_found',
905
- initialActivePath: initialActivePath || '',
906
- writeStarted: false
907
- }
908
- })
909
- };
910
- }
911
-
912
882
  const opened = await openFileByPath(filePath, { force: initialActivePath === filePath });
913
883
  if (!opened.ok) {
914
- 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}`;
915
896
  return {
916
897
  ok: false,
917
- code: 'file_open_failed',
918
- 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, {
919
900
  initialActivePath,
920
901
  opened
921
902
  }),
922
903
  reason: reasonText,
923
- failure: buildPageFailure('target_file_open_failed', {
904
+ failure: buildPageFailure(failureCode, {
924
905
  file: filePath,
925
906
  activeFile: initialActivePath || '',
926
907
  operationType: operation?.type || 'edit',
927
908
  changedDocument: false,
928
- 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.`,
929
912
  technicalMessage: opened.reason || '',
930
913
  evidence: {
914
+ originalCode: resultCode,
931
915
  initialActivePath: initialActivePath || '',
932
916
  openMethod: opened.method || '',
917
+ openAttempted: true,
933
918
  writeStarted: false
934
919
  }
935
920
  })
@@ -1707,20 +1692,6 @@
1707
1692
  }
1708
1693
 
1709
1694
 
1710
- function diffTrackedChangeRefs(before = [], after = []) {
1711
- const beforeKeys = new Set((before || []).map(ref => ref.key).filter(Boolean));
1712
- const seen = new Set();
1713
- const added = [];
1714
- for (const ref of after || []) {
1715
- if (!ref?.key || beforeKeys.has(ref.key) || seen.has(ref.key)) {
1716
- continue;
1717
- }
1718
- seen.add(ref.key);
1719
- added.push(ref);
1720
- }
1721
- return added;
1722
- }
1723
-
1724
1695
  function mergeTrackedChangeRefs(refs = []) {
1725
1696
  return normalizeTrackedChangeRefs(refs);
1726
1697
  }
@@ -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') {
@@ -584,6 +584,132 @@
584
584
  };
585
585
  }
586
586
 
587
+ function translateProviderError(text, locale) {
588
+ const rules = [
589
+ {
590
+ pattern: /provider_revision_conflict|active provider changed|provider changed in another tab/i,
591
+ conclusionZh: '这轮没有启动:模型服务配置已在另一个标签页发生变化。',
592
+ conclusionEn: 'This run did not start: the model provider configuration changed in another tab.',
593
+ nextZh: '请刷新模型服务配置,然后重新运行任务。',
594
+ nextEn: 'Refresh the provider configuration, then run the task again.',
595
+ code: 'provider_revision_conflict'
596
+ },
597
+ {
598
+ pattern: /provider_auth_rejected|rejected the API key|denied access for this credential/i,
599
+ conclusionZh: '这轮没有继续:第三方模型服务拒绝了当前 API 密钥。',
600
+ conclusionEn: 'This run did not continue: the third-party provider rejected the current API key.',
601
+ nextZh: '请在 Settings 的模型服务配置中替换 API 密钥并测试连接。',
602
+ nextEn: 'Replace the API key in Model Providers and run Test connection.',
603
+ code: 'provider_auth_rejected'
604
+ },
605
+ {
606
+ pattern: /provider_model_not_found|configured model was not found/i,
607
+ conclusionZh: '这轮没有继续:第三方模型服务中找不到所选模型。',
608
+ conclusionEn: 'This run did not continue: the selected model was not found by the third-party provider.',
609
+ nextZh: '请检查模型 ID,测试连接后重试。',
610
+ nextEn: 'Check the model ID, test the connection, and retry.',
611
+ code: 'provider_model_not_found'
612
+ },
613
+ {
614
+ pattern: /provider_model_not_configured|selected model is not configured|at least one model ID is required/i,
615
+ conclusionZh: '这轮没有启动:当前模型没有配置在启用的模型服务中。',
616
+ conclusionEn: 'This run did not start: the current model is not configured for the active provider.',
617
+ nextZh: '请打开模型服务配置,添加或选择一个可用模型。',
618
+ nextEn: 'Open Model Providers and add or select an available model.',
619
+ code: 'provider_model_not_configured'
620
+ },
621
+ {
622
+ pattern: /provider_protocol_unverified|test connection before (activating|using)/i,
623
+ conclusionZh: '这轮没有启动:自动 API 协议尚未完成验证。',
624
+ conclusionEn: 'This run did not start: the automatic API protocol has not been verified.',
625
+ nextZh: '请在模型服务配置中运行“测试连接”,保存后重试。',
626
+ nextEn: 'Run Test connection in Model Providers, save, and retry.',
627
+ code: 'provider_protocol_unverified'
628
+ },
629
+ {
630
+ pattern: /provider_protocol_incompatible|incompatible with this API protocol/i,
631
+ conclusionZh: '这轮没有继续:端点与所选 API 协议不兼容。',
632
+ conclusionEn: 'This run did not continue: the endpoint is incompatible with the selected API protocol.',
633
+ nextZh: '请切换 Responses 或 Chat Completions,或使用自动检测重新测试。',
634
+ nextEn: 'Switch between Responses and Chat Completions, or test again with Auto detection.',
635
+ code: 'provider_protocol_incompatible'
636
+ },
637
+ {
638
+ pattern: /provider_rate_limited|rate limit was reached/i,
639
+ conclusionZh: '这轮没有完成:第三方模型服务触发了限流。',
640
+ conclusionEn: 'This run did not finish: the third-party provider rate limit was reached.',
641
+ nextZh: '请等待限流窗口恢复后重试。',
642
+ nextEn: 'Wait for the provider rate-limit window to recover, then retry.',
643
+ code: 'provider_rate_limited'
644
+ },
645
+ {
646
+ pattern: /provider_connection_timeout|provider connection.*timed out/i,
647
+ conclusionZh: '这轮没有完成:连接第三方模型服务超时。',
648
+ conclusionEn: 'This run did not finish: the third-party provider connection timed out.',
649
+ nextZh: '请检查端点和网络,或提高模型服务的请求超时后重试。',
650
+ nextEn: 'Check the endpoint and network, or increase the provider timeout before retrying.',
651
+ code: 'provider_connection_timeout'
652
+ },
653
+ {
654
+ pattern: /provider_dns_failed|hostname could not be resolved/i,
655
+ conclusionZh: '这轮没有启动:无法解析第三方模型服务的主机名。',
656
+ conclusionEn: 'This run did not start: the third-party provider hostname could not be resolved.',
657
+ nextZh: '请检查 Base URL 和本机 DNS 后重试。',
658
+ nextEn: 'Check the Base URL and local DNS, then retry.',
659
+ code: 'provider_dns_failed'
660
+ },
661
+ {
662
+ pattern: /provider_tls_failed|TLS verification failed/i,
663
+ conclusionZh: '这轮没有启动:第三方模型服务的 TLS 验证失败。',
664
+ conclusionEn: 'This run did not start: TLS verification failed for the third-party provider.',
665
+ nextZh: '请检查端点证书和 Base URL,修复后重试。',
666
+ nextEn: 'Check the endpoint certificate and Base URL, then retry.',
667
+ code: 'provider_tls_failed'
668
+ },
669
+ {
670
+ pattern: /provider_not_found|active provider no longer exists/i,
671
+ conclusionZh: '这轮没有启动:启用的模型服务已经不存在。',
672
+ conclusionEn: 'This run did not start: the active model provider no longer exists.',
673
+ nextZh: '请在 Settings 中选择内置 Codex 或另一个模型服务。',
674
+ nextEn: 'Select Built-in Codex or another provider in Settings.',
675
+ code: 'provider_not_found'
676
+ },
677
+ {
678
+ pattern: /provider_store_corrupt|provider stores are out of sync|provider store.*invalid/i,
679
+ conclusionZh: '这轮没有启动:本地模型服务配置存储已损坏或不同步。',
680
+ conclusionEn: 'This run did not start: the local provider configuration store is corrupt or out of sync.',
681
+ nextZh: '请打开技术详情,修复或重建本地模型服务配置。',
682
+ nextEn: 'Open Technical Details and repair or rebuild the local provider configuration.',
683
+ code: 'provider_store_corrupt'
684
+ },
685
+ {
686
+ pattern: /provider_store_unavailable|provider settings could not be saved/i,
687
+ conclusionZh: '模型服务操作失败:Native Host 无法访问本地配置存储。',
688
+ conclusionEn: 'The provider operation failed: the Native Host could not access local provider storage.',
689
+ nextZh: '请检查本地目录权限和 Native Host 状态后重试。',
690
+ nextEn: 'Check local directory permissions and Native Host status, then retry.',
691
+ code: 'provider_store_unavailable'
692
+ },
693
+ {
694
+ pattern: /provider_response_invalid|provider connection test failed/i,
695
+ conclusionZh: '这轮没有继续:第三方模型服务返回了不兼容或无效的响应。',
696
+ conclusionEn: 'This run did not continue: the third-party provider returned an incompatible or invalid response.',
697
+ nextZh: '请测试连接并确认服务兼容所选 API 协议。',
698
+ nextEn: 'Test the connection and confirm that the endpoint supports the selected API protocol.',
699
+ code: 'provider_response_invalid'
700
+ }
701
+ ];
702
+ const rule = rules.find(candidate => candidate.pattern.test(text));
703
+ if (!rule) {
704
+ return null;
705
+ }
706
+ return {
707
+ conclusion: textFor(locale, rule.conclusionZh, rule.conclusionEn),
708
+ nextStep: textFor(locale, rule.nextZh, rule.nextEn),
709
+ failureCode: rule.code
710
+ };
711
+ }
712
+
587
713
  function translateRawError(message, context = {}) {
588
714
  const locale = normalizeLocale(context);
589
715
  const text = String(message || '');
@@ -594,6 +720,10 @@
594
720
  // failure data alongside the human-readable text. The string text is kept
595
721
  // verbatim because it has mode-aware nuance (e.g. "No files were written"
596
722
  // vs "This run did not start") the catalog's fallback strings don't capture.
723
+ const providerError = translateProviderError(text, locale);
724
+ if (providerError) {
725
+ return providerError;
726
+ }
597
727
  if (/Mode must be "confirm" or "auto"/i.test(text)) {
598
728
  return {
599
729
  conclusion: textFor(locale, '这轮没有写入:当前是“只问不改”,但这个任务需要写入权限。', 'No files were written: this task needs write access, but the current mode is Ask.'),
@@ -7,12 +7,12 @@
7
7
  })(typeof globalThis !== 'undefined' ? globalThis : window, function compatibilityFactory() {
8
8
  'use strict';
9
9
 
10
- const EXTENSION_PROTOCOL_VERSION = 1;
11
- const SUPPORTED_NATIVE_PROTOCOL = Object.freeze({ min: 1, max: 1 });
10
+ const EXTENSION_PROTOCOL_VERSION = 2;
11
+ const SUPPORTED_NATIVE_PROTOCOL = Object.freeze({ min: 1, max: 2 });
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 = '1.9.13';
15
+ const BUILD_TARGET_VERSION = '2.1.0';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -24,7 +24,8 @@
24
24
  'codexModels',
25
25
  'historyClearPlugin',
26
26
  'localSkills',
27
- 'mirrorSensitiveScan'
27
+ 'mirrorSensitiveScan',
28
+ 'providerProfiles'
28
29
  ]);
29
30
  const UPDATE_AVAILABLE_CAPABILITIES = Object.freeze([
30
31
  'bridgePing',
@@ -153,7 +154,7 @@
153
154
  return { status: 'native_too_old', classification: 'incompatible' };
154
155
  }
155
156
 
156
- if (!reportsProtocolOne(native)) {
157
+ if (!reportsSupportedProtocol(native)) {
157
158
  return { status: 'protocol_unsupported', classification: 'incompatible' };
158
159
  }
159
160
 
@@ -233,8 +234,10 @@
233
234
  return requiredCapabilities.filter(capability => capabilities[capability] !== true);
234
235
  }
235
236
 
236
- function reportsProtocolOne(native) {
237
- if (native.protocolVersion !== EXTENSION_PROTOCOL_VERSION) {
237
+ function reportsSupportedProtocol(native) {
238
+ if (!Number.isInteger(native.protocolVersion) ||
239
+ native.protocolVersion < SUPPORTED_NATIVE_PROTOCOL.min ||
240
+ native.protocolVersion > SUPPORTED_NATIVE_PROTOCOL.max) {
238
241
  return false;
239
242
  }
240
243
  if (native.supportedProtocol === undefined || native.supportedProtocol === null) {