codex-overleaf-link 2.3.0 → 2.3.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 (33) hide show
  1. package/README.md +23 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/background.js +20 -0
  4. package/extension/src/content/composerPanel.js +0 -2
  5. package/extension/src/content/contentRuntime.js +108 -110
  6. package/extension/src/content/generated/content.bundle.js +566 -331
  7. package/extension/src/content/generated/content.bundle.meta.json +4 -3
  8. package/extension/src/content/markdownText.js +12 -10
  9. package/extension/src/content/mathText.js +155 -9
  10. package/extension/src/content/nativeCompatibilityController.js +2 -2
  11. package/extension/src/content/otWarmMirror.js +1 -1
  12. package/extension/src/content/runTimelineView.js +6 -0
  13. package/extension/src/content/writebackOrchestrator.js +2 -49
  14. package/extension/src/page/trackedChangesLifecycle.js +10 -4
  15. package/extension/src/page/writebackRouter.js +6 -2
  16. package/extension/src/shared/agentTranscript.js +10 -3
  17. package/extension/src/shared/compatibility.js +1 -1
  18. package/extension/src/shared/i18n.js +2 -4
  19. package/extension/src/shared/pageRpcContract.js +2 -2
  20. package/extension/src/shared/runExecutionSnapshot.js +14 -4
  21. package/extension/src/shared/runExecutionSnapshotCodec.js +5 -2
  22. package/extension/src/shared/scopedPersistenceQueuePolicy.js +20 -1
  23. package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
  24. package/extension/src/shared/scopedPersistenceTransaction.js +50 -47
  25. package/extension/src/shared/sessionState.js +46 -11
  26. package/extension/src/shared/storageDb.js +6 -5
  27. package/extension/src/shared/writebackOperationEvidence.js +16 -6
  28. package/extension/src/shared/writebackSettlement.js +28 -6
  29. package/extension/styles/panel.css +18 -8
  30. package/native-host/src/codexPromptAssembly.js +1 -1
  31. package/native-host/src/taskRunnerRuntime.js +11 -81
  32. package/native-host/src/updateManager.js +36 -1
  33. package/package.json +1 -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": "83c5f2bc9eb28735ba091c432476e505b311cfba37d53aa205049ea70ef2d2fc",
6
- "outputDigest": "582a7804b46491dd34cd9f6f24304597f4e946e79870e323b9868a825b4666dd",
7
- "byteLength": 2277820,
5
+ "sourceDigest": "ddbc07ac8d34d864aa3648c53211c5858e0312a73df815a0392bc52ec5eac1cc",
6
+ "outputDigest": "42051c8624bdfca3094c8bef2430094e55b7e1ec49d475ac27f6df14629bf313",
7
+ "byteLength": 2285557,
8
8
  "inputs": [
9
9
  "extension/entries/content-entry.mjs",
10
10
  "extension/src/content/activeTurnControl.js",
@@ -81,6 +81,7 @@
81
81
  "extension/src/shared/runInputQueue.js",
82
82
  "extension/src/shared/scopedPersistenceBrowserAdapter.js",
83
83
  "extension/src/shared/scopedPersistenceQueuePolicy.js",
84
+ "extension/src/shared/scopedPersistenceReceipt.js",
84
85
  "extension/src/shared/scopedPersistenceTransaction.js",
85
86
  "extension/src/shared/sensitiveScan.js",
86
87
  "extension/src/shared/sessionState.js",
@@ -13,12 +13,14 @@
13
13
  tx,
14
14
  callPageBridge, getCurrentProjectId,
15
15
  getCurrentProjectReferenceFiles,
16
+ getCurrentProjectMathSources,
16
17
  showPluginToast
17
18
  } = deps;
18
19
 
19
20
  function renderMarkdownInlineText(target, value) {
20
21
  target.replaceChildren(...buildMarkdownInlineNodes(value));
21
22
  }
23
+ function getMathMacros() { return MathText?.buildMathMacros?.(getCurrentProjectMathSources?.() || []) || {}; }
22
24
 
23
25
  function sanitizeAssistantVisibleText(value) {
24
26
  if (typeof value !== 'string' || !value) {
@@ -67,11 +69,8 @@
67
69
  if (!mathText?.buildMathNodes) {
68
70
  return buildMarkdownInlineNodesWithoutMath(source);
69
71
  }
70
- return mathText.buildMathNodes(source, {
71
- document,
72
- katex: window.katex,
73
- renderText: buildMarkdownInlineNodesWithoutMath
74
- });
72
+ return mathText.buildMathNodes(source, { document, katex: window.katex,
73
+ macros: getMathMacros(), renderText: buildMarkdownInlineNodesWithoutMath });
75
74
  }
76
75
 
77
76
  function buildMarkdownInlineNodesWithoutMath(value) {
@@ -109,6 +108,10 @@
109
108
  function buildInlineCodeNodes(value) {
110
109
  const source = String(value || '');
111
110
  const trimmed = source.trim();
111
+ const standaloneMath = MathText?.parseStandaloneMath?.(trimmed);
112
+ if (standaloneMath && MathText?.createMathNode) return [MathText.createMathNode(
113
+ standaloneMath, { document, katex: window.katex, macros: getMathMacros() }
114
+ )];
112
115
  const refs = trimmed ? parseRuntimeLineReferences(trimmed, 'plain-text-token') : [];
113
116
  const ref = refs.length === 1 && refs[0]?.rawText === trimmed ? refs[0] : null;
114
117
  if (ref && !isRuntimeLocalPathLike(ref.rawPath)) {
@@ -492,12 +495,11 @@
492
495
  `[${safeLabel.replace(/]/g, '\\]')}](${target.replace(/\)/g, '\\)')})`
493
496
  );
494
497
  },
495
- renderCodeNodes: text => buildMarkdownInlineNodes(`\`${String(text || '').replace(/`/g, '\\`')}\``),
498
+ renderCodeNodes: text => buildInlineCodeNodes(text),
496
499
  sanitizeText: sanitizeAssistantVisibleText,
497
- createMathNode: segment => runtimeRoot.CodexOverleafMathText.createMathNode(segment, {
498
- document, katex: runtimeRoot.katex,
499
- renderText: text => buildMarkdownInlineNodes(text)
500
- }),
500
+ createMathNode: segment => runtimeRoot.CodexOverleafMathText.createMathNode(segment,
501
+ { document, katex: runtimeRoot.katex, macros: getMathMacros(),
502
+ renderText: text => buildMarkdownInlineNodes(text) }),
501
503
  recordDiagnostic: recordRenderingDiagnostic
502
504
  });
503
505
  return;
@@ -9,6 +9,19 @@
9
9
  ]);
10
10
  const LONG_INLINE_MATH_CHARS = 120;
11
11
  const COMPLEX_INLINE_MATH_CHARS = 80;
12
+ const MAX_PROJECT_MACRO_SOURCES = 12;
13
+ const MAX_PROJECT_MACRO_SOURCE_CHARS = 256 * 1024;
14
+ const MAX_PROJECT_MACROS = 64;
15
+ const MAX_MACRO_EXPANSION_CHARS = 512;
16
+ const DEFAULT_MATH_MACROS = Object.freeze({
17
+ '\\E': '\\mathbb{E}',
18
+ '\\Var': '\\operatorname{Var}',
19
+ '\\Cov': '\\operatorname{Cov}',
20
+ '\\R': '\\mathbb{R}',
21
+ '\\N': '\\mathbb{N}',
22
+ '\\Z': '\\mathbb{Z}'
23
+ });
24
+ const UNSAFE_MACRO_PATTERN = /\\(?:href|url|includegraphics|input|include|write|openout|read|html\w*|class|style|data|def|gdef|edef|xdef|newcommand|renewcommand|providecommand|DeclareMathOperator)\b/i;
12
25
 
13
26
  function parseMathSegments(value) {
14
27
  const source = String(value || '');
@@ -74,9 +87,10 @@
74
87
  searchFrom = start + delimiter.open.length;
75
88
  continue;
76
89
  }
77
- const value = source.slice(start + delimiter.open.length, closeStart);
90
+ const rawValue = source.slice(start + delimiter.open.length, closeStart);
91
+ const value = rawValue.trim();
78
92
  const end = closeStart + delimiter.close.length;
79
- if (!isValidMathValue(value, delimiter) || overlapsCodeRange(start, end, codeRanges)) {
93
+ if (!isValidMathValue(rawValue, delimiter) || overlapsCodeRange(start, end, codeRanges)) {
80
94
  searchFrom = start + delimiter.open.length;
81
95
  continue;
82
96
  }
@@ -94,7 +108,7 @@
94
108
  }
95
109
  const previous = source[index - 1] || '';
96
110
  const next = source[index + 1] || '';
97
- return previous !== '$' && next !== '$' && next !== '' && !/\s/.test(next);
111
+ return previous !== '$' && next !== '$' && next !== '';
98
112
  }
99
113
 
100
114
  function findClosingDelimiter(source, fromIndex, delimiter, codeRanges) {
@@ -107,7 +121,7 @@
107
121
  const previous = source[index - 1] || '';
108
122
  const next = source[index + delimiter.close.length] || '';
109
123
  const invalidSingleDollar = delimiter.singleDollar
110
- && (previous === '$' || next === '$' || /\s/.test(previous));
124
+ && (previous === '$' || next === '$');
111
125
  if (!isEscaped(source, index) && !isInsideRange(index, codeRanges) && !invalidSingleDollar) {
112
126
  return index;
113
127
  }
@@ -118,10 +132,24 @@
118
132
 
119
133
  function isValidMathValue(value, delimiter) {
120
134
  const source = String(value || '');
121
- if (!source.trim()) {
135
+ const trimmed = source.trim();
136
+ if (!trimmed) {
122
137
  return false;
123
138
  }
124
- return delimiter.display || !/[\r\n]/.test(source);
139
+ if (!delimiter.display && /[\r\n]/.test(source)) {
140
+ return false;
141
+ }
142
+ if (!delimiter.singleDollar || (!/^\s/.test(source) && !/\s$/.test(source))) {
143
+ return true;
144
+ }
145
+ return looksLikeWhitespaceWrappedMath(trimmed);
146
+ }
147
+
148
+ function looksLikeWhitespaceWrappedMath(value) {
149
+ const source = String(value || '').trim();
150
+ return /\\[A-Za-z]+/.test(source)
151
+ || /[_^=<>+\-*/()[\]{}|]/.test(source)
152
+ || /^[A-Za-z](?:_[A-Za-z0-9{}]+|\^[A-Za-z0-9{}]+)?$/.test(source);
125
153
  }
126
154
 
127
155
  function collectInlineCodeRanges(source) {
@@ -179,7 +207,8 @@
179
207
  }
180
208
  nodes.push(createMathNode(segment, {
181
209
  document: documentRef,
182
- katex: options.katex || root.katex
210
+ katex: options.katex || root.katex,
211
+ macros: options.macros || buildMathMacros(options.projectSources)
183
212
  }));
184
213
  }
185
214
  return nodes;
@@ -206,6 +235,7 @@
206
235
  strict: 'warn',
207
236
  maxExpand: 200,
208
237
  maxSize: 20,
238
+ macros: options.macros || buildMathMacros(),
209
239
  output: 'htmlAndMathml'
210
240
  });
211
241
  node.dataset.mathRendered = 'true';
@@ -213,7 +243,7 @@
213
243
  } catch (_error) {
214
244
  node.classList.add('run-math--fallback');
215
245
  node.dataset.mathRendered = 'false';
216
- node.textContent = segment.raw;
246
+ node.textContent = source;
217
247
  return node;
218
248
  }
219
249
  }
@@ -235,6 +265,120 @@
235
265
  return /(?:=|\\(?:le|ge|approx|sim|to|Rightarrow|Longrightarrow)\b|\\begin\{(?:aligned|gathered|split|array)\}|\\\\)/.test(source);
236
266
  }
237
267
 
268
+ function parseStandaloneMath(value) {
269
+ const source = String(value || '').trim();
270
+ if (!source) {
271
+ return null;
272
+ }
273
+ const segments = parseMathSegments(source);
274
+ if (segments.length !== 1 || segments[0].type !== 'math' || segments[0].raw !== source) {
275
+ return null;
276
+ }
277
+ return segments[0];
278
+ }
279
+
280
+ function buildMathMacros(projectSources = []) {
281
+ const macros = { ...DEFAULT_MATH_MACROS };
282
+ let remainingChars = MAX_PROJECT_MACRO_SOURCE_CHARS;
283
+ let projectMacroCount = 0;
284
+ for (const sourceRecord of Array.isArray(projectSources)
285
+ ? projectSources.slice(0, MAX_PROJECT_MACRO_SOURCES)
286
+ : []) {
287
+ if (projectMacroCount >= MAX_PROJECT_MACROS || remainingChars <= 0) {
288
+ break;
289
+ }
290
+ const path = String(sourceRecord?.path || '');
291
+ if (!/\.(?:tex|sty|cls|ltx)$/i.test(path)) {
292
+ continue;
293
+ }
294
+ const source = stripLatexComments(
295
+ String(sourceRecord?.content || '').slice(0, remainingChars)
296
+ );
297
+ remainingChars -= source.length;
298
+ const pattern = /\\(newcommand|renewcommand|providecommand|DeclareMathOperator)(\*)?/g;
299
+ let match;
300
+ while ((match = pattern.exec(source)) !== null && projectMacroCount < MAX_PROJECT_MACROS) {
301
+ let cursor = skipWhitespace(source, pattern.lastIndex);
302
+ const nameGroup = readBracedGroup(source, cursor);
303
+ if (!nameGroup) {
304
+ continue;
305
+ }
306
+ const macroName = nameGroup.value.trim();
307
+ cursor = skipWhitespace(source, nameGroup.end);
308
+ if (source[cursor] === '[') {
309
+ continue;
310
+ }
311
+ const expansionGroup = readBracedGroup(source, cursor);
312
+ if (!expansionGroup) {
313
+ continue;
314
+ }
315
+ let expansion = expansionGroup.value.trim();
316
+ if (match[1] === 'DeclareMathOperator') {
317
+ expansion = `\\operatorname${match[2] ? '*' : ''}{${expansion}}`;
318
+ }
319
+ if (!isSafeMacroDefinition(macroName, expansion)) {
320
+ continue;
321
+ }
322
+ macros[macroName] = expansion;
323
+ projectMacroCount++;
324
+ pattern.lastIndex = expansionGroup.end;
325
+ }
326
+ }
327
+ return macros;
328
+ }
329
+
330
+ function readBracedGroup(source, start) {
331
+ if (source[start] !== '{') {
332
+ return null;
333
+ }
334
+ let depth = 0;
335
+ for (let index = start; index < source.length; index++) {
336
+ if (source[index] === '{' && !isEscaped(source, index)) {
337
+ depth++;
338
+ } else if (source[index] === '}' && !isEscaped(source, index)) {
339
+ depth--;
340
+ if (depth === 0) {
341
+ return {
342
+ value: source.slice(start + 1, index),
343
+ end: index + 1
344
+ };
345
+ }
346
+ }
347
+ }
348
+ return null;
349
+ }
350
+
351
+ function skipWhitespace(source, start) {
352
+ let index = start;
353
+ while (index < source.length && /\s/.test(source[index])) {
354
+ index++;
355
+ }
356
+ return index;
357
+ }
358
+
359
+ function stripLatexComments(source) {
360
+ return String(source || '').split(/\r?\n/).map(line => {
361
+ for (let index = 0; index < line.length; index++) {
362
+ if (line[index] === '%' && !isEscaped(line, index)) {
363
+ return line.slice(0, index);
364
+ }
365
+ }
366
+ return line;
367
+ }).join('\n');
368
+ }
369
+
370
+ function isSafeMacroDefinition(name, expansion) {
371
+ const macroName = String(name || '');
372
+ const body = String(expansion || '');
373
+ return /^\\[A-Za-z@]+$/.test(macroName)
374
+ && body.length > 0
375
+ && body.length <= MAX_MACRO_EXPANSION_CHARS
376
+ && !body.includes('#')
377
+ && !body.includes(macroName)
378
+ && !UNSAFE_MACRO_PATTERN.test(macroName)
379
+ && !UNSAFE_MACRO_PATTERN.test(body);
380
+ }
381
+
238
382
  function toNodeArray(value) {
239
383
  if (Array.isArray(value)) {
240
384
  return value;
@@ -265,11 +409,13 @@
265
409
  }
266
410
 
267
411
  const api = {
412
+ buildMathMacros,
268
413
  buildMathNodes,
269
414
  createMathNode,
270
415
  matchMathAt,
271
416
  normalizeMathForRendering,
272
- parseMathSegments
417
+ parseMathSegments,
418
+ parseStandaloneMath
273
419
  };
274
420
  root.CodexOverleafMathText = api;
275
421
  if (typeof module !== 'undefined' && module.exports) {
@@ -256,8 +256,8 @@
256
256
  tip.dataset.onboardingTip = 'true';
257
257
  tip.setAttribute('role', 'status');
258
258
  tip.textContent = tx(
259
- 'Tip: Start with Ask mode to explore safely. Switch to Suggest to review edits before applying.',
260
- '提示:先用 Ask 模式安全探索;需要修改时切换到 Suggest,应用前可先审阅。'
259
+ 'Tip: Start with Ask mode to explore safely. Switch to Auto only when you want Codex to edit files.',
260
+ '提示:先用 Ask 模式安全探索;只有需要 Codex 修改文件时才切换到 Auto。'
261
261
  );
262
262
  Object.assign(tip.style, {
263
263
  position: 'absolute',
@@ -868,7 +868,7 @@
868
868
  applyResults: [],
869
869
  nextStep: tr('warmMirrorStaleBlockedNextStep')
870
870
  });
871
- finishRunView(tr('warmMirrorStaleBlockedTitle'), 'failed');
871
+ await finishRunView(tr('warmMirrorStaleBlockedTitle'), 'failed');
872
872
  return { ok: false };
873
873
  }
874
874
  if (focusedPartialSnapshot) {
@@ -274,6 +274,9 @@
274
274
  if (status === 'failed') {
275
275
  return tr('processedFailed', { elapsed });
276
276
  }
277
+ if (status === 'rejected' || status === 'cancelled') {
278
+ return tr('processedCancelled', { elapsed });
279
+ }
277
280
  if (status === 'running') {
278
281
  return tr('processing', { elapsed });
279
282
  }
@@ -411,6 +414,9 @@
411
414
  if (run.startedAt && run.finishedAt) {
412
415
  return formatProcessedSummary(run.status || 'completed', new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime());
413
416
  }
417
+ if (run.status === 'rejected' || run.status === 'cancelled') {
418
+ return tx('Cancelled', '已取消');
419
+ }
414
420
  return run.status === 'failed' ? tx('Failed', '处理失败') : tx('Done', '已处理');
415
421
  }
416
422
 
@@ -25,7 +25,6 @@
25
25
  let appendOperationsPreview;
26
26
  let appendPartialWritebackWarning;
27
27
  let appendApplyResult;
28
- let renderDiffReview;
29
28
  let renderReadOnlyDiffReview;
30
29
  let showPluginConfirm;
31
30
  let callPageBridge;
@@ -96,8 +95,8 @@
96
95
  applyResults: [],
97
96
  mode: runMode,
98
97
  nextStep: tx(
99
- 'Switch to Suggest or Auto only when you want Codex to edit files.',
100
- '只有希望 Codex 修改文件时,才需要切换到建议修改或自动写入。'
98
+ 'Switch to Auto only when you want Codex to edit files.',
99
+ '只有希望 Codex 修改文件时,才需要切换到自动写入。'
101
100
  )
102
101
  });
103
102
  return {
@@ -178,51 +177,6 @@
178
177
  };
179
178
  }
180
179
 
181
- if (runMode === 'confirm') {
182
- appendRunEvent({
183
- title: tx(
184
- `Local Codex produced ${operations.length} change(s). Review the diff before applying.`,
185
- `本地 Codex 产生了 ${operations.length} 项改动,请查看差异后确认。`
186
- ),
187
- status: 'running'
188
- });
189
- const accepted = await renderDiffReview(visibleSyncChanges);
190
- if (!accepted.length) {
191
- appendRunEvent({
192
- title: tx('Sync cancelled: Overleaf was not modified.', '已取消同步:Overleaf 没有被修改。'),
193
- status: 'completed'
194
- });
195
- appendCompletionReport({
196
- conclusion: tx('You cancelled syncing. Local Codex changes were not written back to Overleaf.', '你取消了同步,本地 Codex 改动没有写回 Overleaf。'),
197
- status: 'rejected',
198
- operations: [],
199
- applyResults: [],
200
- mode: runMode,
201
- nextStep: tx('Run the task again, or switch to Auto if you want changes synced directly.', '可以重新运行任务,或切换到自动写入后再同步。')
202
- });
203
- return {
204
- summaryLine: tx('Sync cancelled', '已取消同步'),
205
- hasSkippedOperations: additionalSkippedEntries.length > 0,
206
- settlement: buildSettlement({
207
- operations,
208
- applyResult: { ok: false, applied: [], skipped: additionalSkippedEntries }
209
- }),
210
- audit: buildAuditSummaryFromApply({
211
- operations,
212
- applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
213
- blockedFiles: additionalSkippedEntries.map(item => summarizeOperationForAudit(item.operation, item.result, 'blocked')),
214
- resultStatus: 'rejected'
215
- })
216
- };
217
- }
218
- operations = buildSyncApplyOperations(accepted, project);
219
- const acceptedPathSafety = partitionUnsafeProjectPathOperations(operations);
220
- if (acceptedPathSafety.skipped.length) {
221
- additionalSkippedEntries.push(...acceptedPathSafety.skipped);
222
- operations = acceptedPathSafety.safe;
223
- }
224
- }
225
-
226
180
  const deleteOperations = operations.filter(operation => operation.type === 'delete');
227
181
  if (deleteOperations.length) {
228
182
  const approved = await showPluginConfirm({
@@ -872,7 +826,6 @@
872
826
  appendOperationsPreview,
873
827
  appendPartialWritebackWarning,
874
828
  appendApplyResult,
875
- renderDiffReview,
876
829
  renderReadOnlyDiffReview,
877
830
  showPluginConfirm,
878
831
  callPageBridge,
@@ -971,8 +971,15 @@
971
971
  }
972
972
 
973
973
  for (const path of paths) {
974
- if (path && getActiveFilePath() !== path) {
975
- const opened = await openFileByPath(path);
974
+ const postContent = postByPath.get(path);
975
+ const activePathMatches = getActiveFilePath() === path;
976
+ const activeContentMatches = readActiveEditorText() === postContent;
977
+ // Overleaf can update the selected tree path before CodeMirror swaps its
978
+ // document. In that state getActiveFilePath() already names `path`, while
979
+ // readActiveEditorText() still belongs to the previously viewed file.
980
+ // Force-reopen the target whenever either identity signal disagrees.
981
+ if (path && (!activePathMatches || !activeContentMatches)) {
982
+ const opened = await openFileByPath(path, { force: true });
976
983
  if (!opened.ok) {
977
984
  return {
978
985
  ok: false,
@@ -983,8 +990,7 @@
983
990
  }
984
991
  }
985
992
 
986
- const postContent = postByPath.get(path);
987
- const postReady = await waitForActiveEditorExpectedText(path, postContent, 2500);
993
+ const postReady = await waitForActiveEditorExpectedText(path, postContent, 9000);
988
994
  if (!postReady.ok) {
989
995
  return {
990
996
  ok: false,
@@ -1667,13 +1667,17 @@
1667
1667
  continue;
1668
1668
  }
1669
1669
  const key = typeof ref.key === 'string' ? ref.key : '';
1670
- if (!key || seen.has(key)) {
1670
+ if (!key) {
1671
1671
  continue;
1672
1672
  }
1673
- seen.add(key);
1674
1673
  const hasPath = typeof ref.path === 'string';
1675
1674
  const hasExplicitPath = hasPath && ref.path.trim() !== '';
1676
1675
  const path = hasPath ? normalizeSafeProjectPath(ref.path) : '';
1676
+ const identity = `${path}\u0000${key}`;
1677
+ if (seen.has(identity)) {
1678
+ continue;
1679
+ }
1680
+ seen.add(identity);
1677
1681
  normalized.push({
1678
1682
  key,
1679
1683
  id: typeof ref.id === 'string' ? ref.id : '',
@@ -727,10 +727,17 @@
727
727
  if (providerError) {
728
728
  return providerError;
729
729
  }
730
- if (/Mode must be "confirm" or "auto"/i.test(text)) {
730
+ if (/suggest_mode_removed|Suggest mode has been removed/i.test(text)) {
731
+ return {
732
+ conclusion: textFor(locale, '建议修改模式已经移除。', 'Suggest mode has been removed.'),
733
+ nextStep: textFor(locale, '请刷新扩展,然后选择“只问不改”或“自动写入”。', 'Reload the extension, then choose Ask or Auto.'),
734
+ failureCode: 'suggest_mode_removed'
735
+ };
736
+ }
737
+ if (/Mode must be "(?:confirm|ask)" or "auto"/i.test(text)) {
731
738
  return {
732
739
  conclusion: textFor(locale, '这轮没有写入:当前是“只问不改”,但这个任务需要写入权限。', 'No files were written: this task needs write access, but the current mode is Ask.'),
733
- nextStep: textFor(locale, '请切换到“建议修改”或“自动写入”后重新运行。', 'Switch to Suggest or Auto and run the task again.'),
740
+ nextStep: textFor(locale, '请切换到“自动写入”后重新运行。', 'Switch to Auto and run the task again.'),
734
741
  failureCode: null // no direct catalog match — preflight / governance
735
742
  };
736
743
  }
@@ -797,7 +804,7 @@
797
804
  if (/checkpoint/i.test(text)) {
798
805
  return {
799
806
  conclusion: textFor(locale, '这轮没有自动写入:Codex 没有拿到可恢复版本。', 'This run did not auto-write: Codex did not get a recoverable version.'),
800
- nextStep: textFor(locale, '请切换到“建议修改”,或确认 Overleaf Reviewing 已开启后再用“自动写入”。', 'Switch to Suggest, or confirm Overleaf Reviewing is enabled before using Auto.'),
807
+ nextStep: textFor(locale, '请确认 Overleaf Reviewing 已开启后再用“自动写入”。', 'Confirm that Overleaf Reviewing is enabled before using Auto.'),
801
808
  failureCode: null // no current catalog code; preflight reviewing issue
802
809
  };
803
810
  }
@@ -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.0';
15
+ const BUILD_TARGET_VERSION = '2.3.1';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -266,8 +266,6 @@
266
266
  writeMode: 'Write mode',
267
267
  modeAsk: 'Ask',
268
268
  modeAskTitle: 'Read and analyze only. Do not write to Overleaf.',
269
- modeConfirm: 'Suggest',
270
- modeConfirmTitle: 'Show a change plan first, then write after approval.',
271
269
  modeAuto: 'Auto',
272
270
  modeAutoTitle: 'Write directly after authorization. Deletes still require confirmation.',
273
271
  addContext: 'Add @ context',
@@ -403,6 +401,7 @@
403
401
  refreshProbeDone: 'Refreshed: {status}',
404
402
  refreshProbeFailed: 'Refresh failed. Reload Overleaf and try again.',
405
403
  processedFailed: 'Failed {elapsed}',
404
+ processedCancelled: 'Cancelled {elapsed}',
406
405
  processing: 'Running {elapsed}',
407
406
  processed: 'Done {elapsed}',
408
407
  restoredRunStoppedTitle: 'Stopped tracking this run after a page refresh',
@@ -779,8 +778,6 @@
779
778
  writeMode: '写入模式',
780
779
  modeAsk: '只问不改',
781
780
  modeAskTitle: '只读取和分析,不写入 Overleaf',
782
- modeConfirm: '建议修改',
783
- modeConfirmTitle: '先给出修改方案,确认后写入',
784
781
  modeAuto: '自动写入',
785
782
  modeAutoTitle: '授权后直接写入,删除仍需确认',
786
783
  addContext: '添加 @ 上下文',
@@ -916,6 +913,7 @@
916
913
  refreshProbeDone: '已重新检测:{status}',
917
914
  refreshProbeFailed: '检测失败:请刷新 Overleaf 页面后重试',
918
915
  processedFailed: '处理失败 {elapsed}',
916
+ processedCancelled: '已取消 {elapsed}',
919
917
  processing: '处理中 {elapsed}',
920
918
  processed: '已处理 {elapsed}',
921
919
  restoredRunStoppedTitle: '页面刷新后已停止跟踪这轮任务',
@@ -5,7 +5,7 @@
5
5
  })(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this), function () {
6
6
  'use strict';
7
7
 
8
- var REVISION = '2026-07-26-page-rpc-contract-v13';
8
+ var REVISION = '2026-07-31-page-rpc-contract-v14';
9
9
  var METHODS = freezeCatalog({
10
10
  initializeCapability: entry('bridge.initialize', 'control', 'none', 'default', false, 'none'),
11
11
  probe: entry('bridge.probe', 'read', 'none', 'default', false, 'safe'),
@@ -67,7 +67,7 @@
67
67
  file_list: 35000,
68
68
  compile: 75000,
69
69
  lifecycle: 120000,
70
- writeback: 30000
70
+ writeback: 45000
71
71
  };
72
72
  var candidate = Number(overrides[timeoutClass]);
73
73
  return Number.isFinite(candidate) && candidate > 0 ? candidate : defaults[timeoutClass];
@@ -28,7 +28,10 @@
28
28
  : rawQueueItem;
29
29
  const persisted = rawQueueItem?.executionSnapshot || rawPayload?.executionSnapshot;
30
30
  if (persisted && typeof persisted === 'object') {
31
- return Codec.normalizeSnapshot(persisted, { source: persisted.source || 'submitted' });
31
+ return Codec.normalizeSnapshot(persisted, {
32
+ source: persisted.source || 'submitted',
33
+ migrateLegacyConfirm: true
34
+ });
32
35
  }
33
36
  const raw = rawPayload && typeof rawPayload === 'object' ? rawPayload : {};
34
37
  const fallback = fallbackInput && typeof fallbackInput === 'object' ? fallbackInput : {};
@@ -48,13 +51,17 @@
48
51
  inferred = true;
49
52
  }
50
53
  return Codec.normalizeSnapshot(captured, {
51
- source: inferred ? 'legacy-inferred' : 'legacy-captured'
54
+ source: inferred ? 'legacy-inferred' : 'legacy-captured',
55
+ migrateLegacyConfirm: true
52
56
  });
53
57
  }
54
58
 
55
59
  function resolveForExecution(value = {}, options = {}) {
56
60
  const persisted = value.executionSnapshot || value;
57
- const snapshot = Codec.normalizeSnapshot(persisted, { source: persisted.source || 'submitted' });
61
+ const snapshot = Codec.normalizeSnapshot(persisted, {
62
+ source: persisted.source || 'submitted',
63
+ migrateLegacyConfirm: true
64
+ });
58
65
  if (snapshot.providerId === 'builtin') return Codec.normalizeSnapshot(snapshot, { source: snapshot.source });
59
66
  const catalog = options.catalog || {};
60
67
  const provider = ProviderProfiles?.getProviderById
@@ -80,7 +87,10 @@
80
87
 
81
88
  function snapshotOf(value = {}) {
82
89
  const persisted = value.executionSnapshot || value;
83
- return Codec.normalizeSnapshot(persisted, { source: persisted.source || 'submitted' });
90
+ return Codec.normalizeSnapshot(persisted, {
91
+ source: persisted.source || 'submitted',
92
+ migrateLegacyConfirm: true
93
+ });
84
94
  }
85
95
 
86
96
  function cloneSnapshot(value = {}) {
@@ -8,7 +8,7 @@
8
8
  'use strict';
9
9
 
10
10
  const SCHEMA_VERSION = 1;
11
- const VALID_MODES = new Set(['ask', 'confirm', 'auto']);
11
+ const VALID_MODES = new Set(['ask', 'auto']);
12
12
  const VALID_SOURCES = new Set(['submitted', 'legacy-captured', 'legacy-inferred']);
13
13
  const TUPLE_FIELDS = Object.freeze([
14
14
  'mode', 'providerId', 'providerRevision', 'model', 'reasoningEffort',
@@ -19,12 +19,15 @@
19
19
  function normalizeSnapshot(value = {}, options = {}) {
20
20
  const input = value && typeof value === 'object' ? value : {};
21
21
  assertNoSecretFields(input);
22
+ if (input.mode === 'confirm' && options.migrateLegacyConfirm !== true) {
23
+ throw snapshotError('suggest_mode_removed', 'Suggest mode has been removed. Choose Ask or Auto.');
24
+ }
22
25
  const providerId = normalizeText(input.providerId) || 'builtin';
23
26
  const revision = normalizeRevisionResult(input.providerRevision);
24
27
  if (!revision.ok) throw snapshotError('provider_revision_conflict', revision.error);
25
28
  const snapshot = {
26
29
  schemaVersion: SCHEMA_VERSION,
27
- mode: VALID_MODES.has(input.mode) ? input.mode : 'ask',
30
+ mode: input.mode === 'auto' ? 'auto' : 'ask',
28
31
  providerId,
29
32
  providerRevision: providerId === 'builtin' ? '' : revision.value,
30
33
  model: normalizeText(input.model).slice(0, 160),