codex-overleaf-link 2.5.1 → 2.6.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 (93) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +78 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +11 -2
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +920 -276
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsPanel.js +18 -0
  14. package/extension/src/content/generated/content.bundle.js +4496 -924
  15. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  16. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  17. package/extension/src/content/localSkillsPanel.js +2 -2
  18. package/extension/src/content/markdownDomRenderer.js +5 -3
  19. package/extension/src/content/markdownText.js +42 -5
  20. package/extension/src/content/modelPicker.js +1 -0
  21. package/extension/src/content/nativeCompatibilityController.js +29 -10
  22. package/extension/src/content/otWarmMirror.js +1 -1
  23. package/extension/src/content/pageBridgeClient.js +2 -2
  24. package/extension/src/content/panelMaintenance.js +6 -2
  25. package/extension/src/content/panelRenderer.js +51 -27
  26. package/extension/src/content/pendingInputView.js +1 -1
  27. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  28. package/extension/src/content/providerSettingsDialog.js +2 -1
  29. package/extension/src/content/recentProjects.js +4 -3
  30. package/extension/src/content/runActivitySummary.js +122 -14
  31. package/extension/src/content/runCompletionMeta.js +194 -6
  32. package/extension/src/content/runController.js +2 -1
  33. package/extension/src/content/runFailureNotice.js +8 -1
  34. package/extension/src/content/runPresence.js +18 -8
  35. package/extension/src/content/runQueueScheduler.js +9 -5
  36. package/extension/src/content/runRecoveryActions.js +2 -1
  37. package/extension/src/content/runResultActions.js +22 -20
  38. package/extension/src/content/runReviewController.js +94 -35
  39. package/extension/src/content/runTimelineView.js +224 -25
  40. package/extension/src/content/scopedPersistencePanelState.js +92 -23
  41. package/extension/src/content/sensitivePreflight.js +14 -1
  42. package/extension/src/content/sessionManager.js +49 -3
  43. package/extension/src/content/sessionMenuView.js +54 -18
  44. package/extension/src/content/sessionPersistence.js +25 -5
  45. package/extension/src/content/settingsPanel.js +15 -2
  46. package/extension/src/content/settingsWorkbench.js +127 -89
  47. package/extension/src/content/sharedSessionViewSync.js +4 -3
  48. package/extension/src/content/subagentActivityView.js +23 -7
  49. package/extension/src/content/updateNotice.js +60 -6
  50. package/extension/src/content/writebackOrchestrator.js +35 -22
  51. package/extension/src/page/compileBridge.js +193 -180
  52. package/extension/src/page/overleafCapabilities.js +4 -1
  53. package/extension/src/page/textCoordinates.js +17 -1
  54. package/extension/src/page/textFileCreator.js +9 -1
  55. package/extension/src/page/writebackRouter.js +24 -77
  56. package/extension/src/pageBridge.js +107 -66
  57. package/extension/src/popup.js +1 -1
  58. package/extension/src/shared/agentTranscript.js +350 -17
  59. package/extension/src/shared/auditRecords.js +2 -0
  60. package/extension/src/shared/compatibility.js +1 -1
  61. package/extension/src/shared/compileAdapter.js +58 -17
  62. package/extension/src/shared/composerAttachmentDraft.js +6 -1
  63. package/extension/src/shared/contextUsage.js +125 -0
  64. package/extension/src/shared/failureReasons.js +42 -3
  65. package/extension/src/shared/i18n.js +299 -29
  66. package/extension/src/shared/lineReferences.js +38 -31
  67. package/extension/src/shared/pageRpcContract.js +1 -1
  68. package/extension/src/shared/panelIcons.js +39 -0
  69. package/extension/src/shared/reviewing.js +130 -1
  70. package/extension/src/shared/runActivityModel.js +70 -4
  71. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  72. package/extension/src/shared/sessionState.js +63 -4
  73. package/extension/src/shared/sessionTitle.js +14 -1
  74. package/extension/src/shared/sharedSessionBridge.js +81 -13
  75. package/extension/src/shared/staleGuard.js +72 -63
  76. package/extension/src/shared/storageDb.js +35 -3
  77. package/extension/src/shared/storageMigration.js +7 -1
  78. package/extension/src/shared/storageRunActions.js +62 -8
  79. package/extension/src/shared/undoOperations.js +126 -28
  80. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  81. package/extension/src/shared/writebackSettlement.js +21 -3
  82. package/extension/styles/panel.css +1106 -490
  83. package/native-host/src/bootstrapUpdate.js +52 -16
  84. package/native-host/src/chatBridgeRequest.js +60 -6
  85. package/native-host/src/chatBridgeResponse.js +10 -1
  86. package/native-host/src/chatCompletionsBridge.js +29 -2
  87. package/native-host/src/codexPromptAssembly.js +86 -118
  88. package/native-host/src/codexSessionRunner.js +126 -17
  89. package/native-host/src/providerContextError.js +20 -0
  90. package/native-host/src/taskRunnerRuntime.js +18 -6
  91. package/native-host/src/textPatch.js +473 -377
  92. package/native-host/src/updateManager.js +120 -15
  93. package/package.json +5 -8
@@ -2,16 +2,19 @@
2
2
  if (typeof module === 'object' && module.exports) {
3
3
  module.exports = factory({
4
4
  getFailureReasons: function getFailureReasonsCjs() { return require('./failureReasons.js'); },
5
- getI18n: function getI18nCjs() { return require('./i18n.js'); }
5
+ getI18n: function getI18nCjs() { return require('./i18n.js'); },
6
+ RunActivityModel: require('./runActivityModel.js')
6
7
  });
7
8
  } else {
8
9
  root.CodexOverleafModuleRegistry.define('AgentTranscript', [
9
10
  'FailureReasons',
10
- 'I18n'
11
- ], function createAgentTranscript(FailureReasons, I18n) {
11
+ 'I18n',
12
+ 'RunActivityModel'
13
+ ], function createAgentTranscript(FailureReasons, I18n, RunActivityModel) {
12
14
  return factory({
13
15
  getFailureReasons: function getFailureReasonsWindow() { return FailureReasons; },
14
- getI18n: function getI18nWindow() { return I18n; }
16
+ getI18n: function getI18nWindow() { return I18n; },
17
+ RunActivityModel
15
18
  });
16
19
  });
17
20
  }
@@ -25,6 +28,221 @@
25
28
  ? deps.getI18n
26
29
  : function noI18n() { return null; };
27
30
 
31
+ // Only explicit product producers may attach these semantic identities.
32
+ const PRODUCT_CONCLUSIONS = {
33
+ "context_compacted": ["Conversation context compacted.", "会话上下文已压缩。"],
34
+ "compact_busy": ["Wait for the current task to finish before compacting context.", "请等待当前任务结束后再压缩上下文。"],
35
+ "compact_unsupported": ["This local Codex connection does not support manual compaction. Update Codex and reload the extension.", "当前本地 Codex 连接不支持手动压缩,请更新 Codex 并重新加载扩展。"],
36
+ "compact_failed": ["Context compaction did not complete. Check Technical Details before retrying.", "上下文压缩未完成,请查看技术详情后再重试。"],
37
+ "full_project_unread": [
38
+ "This run did not continue: the full Overleaf project was not read.",
39
+ "这轮没有继续:没有读到完整的 Overleaf 项目内容。"
40
+ ],
41
+ "empty_result": [
42
+ "Codex completed, but it produced no assistant response and no local file changes.",
43
+ "Codex 已完成,但没有产生助手回复,也没有产生本地文件改动。"
44
+ ],
45
+ "skill_installer_completed": [
46
+ "Skill installer completed.",
47
+ "Skill installer 已完成。"
48
+ ],
49
+ "launch_not_started": [
50
+ "This task did not start: Codex did not run and no files were written.",
51
+ "这轮任务没有开始:Codex 没有运行,也没有写入文件。"
52
+ ],
53
+ "cancelled": [
54
+ "This run was cancelled. It did not continue syncing or writing to Overleaf.",
55
+ "这轮已中断,没有继续同步或写入 Overleaf。"
56
+ ],
57
+ "reload_interrupted": [
58
+ "The plugin reloaded while this run was still marked in progress. It has been marked interrupted to avoid showing a stale status — run the task again to continue.",
59
+ "插件重新加载时发现这轮任务还标记为处理中。为了避免继续显示过期状态,已把它标记为中断;可以重新运行任务。"
60
+ ],
61
+ "ask_inspected": [
62
+ "This run only inspected and explained; no files were written.",
63
+ "这轮只做了检查和说明,没有写入文件。"
64
+ ],
65
+ "delete_handled": [
66
+ "Changes were written, and delete items were handled after your confirmation.",
67
+ "已写入修改,并按你的确认处理删除项。"
68
+ ],
69
+ "delete_rejected": [
70
+ "Non-delete changes were kept; delete items were not written.",
71
+ "已保留非删除修改;删除项没有写入。"
72
+ ],
73
+ "run_complete": [
74
+ "This run is complete.",
75
+ "这轮任务已完成。"
76
+ ],
77
+ "withheld_changes": [
78
+ "Some generated changes were blocked.",
79
+ "部分生成的改动已被阻止。"
80
+ ],
81
+ "ask_ignored_changes": [
82
+ "Codex finished in Ask mode. Any local file changes were ignored and were not synced to Overleaf.",
83
+ "Codex 已在 Ask 模式完成;本地文件改动已忽略,未同步到 Overleaf。"
84
+ ],
85
+ "no_sync_changes": [
86
+ "Codex finished locally. There are no changes to sync back to Overleaf.",
87
+ "Codex 已完成本地处理,没有需要同步回 Overleaf 的改动。"
88
+ ],
89
+ "no_approved_changes": [
90
+ "No approved changes were written back to Overleaf.",
91
+ "没有已确认的改动写回 Overleaf。"
92
+ ],
93
+ "reviewing_unavailable": [
94
+ "No files were written: Track is enabled, but Codex could not verify Overleaf Reviewing/Track Changes.",
95
+ "这轮没有写入:已开启“留痕”要求,但 Codex 没能确认 Overleaf 正在用 Reviewing/Track Changes。"
96
+ ],
97
+ "sensitive_blocked": [
98
+ "This run was blocked before Codex received project context because sensitive content was detected.",
99
+ "检测到敏感内容,本轮已在向 Codex 发送项目上下文前阻止。"
100
+ ],
101
+ "synced": [
102
+ "Local Codex changes were synced back to Overleaf.",
103
+ "本地 Codex 改动已同步回 Overleaf。"
104
+ ],
105
+ "partial_sync": [
106
+ "Local Codex changes were sent to Overleaf, but some items were skipped.",
107
+ "本地 Codex 改动已尝试同步回 Overleaf,但有部分项目被跳过。"
108
+ ],
109
+ "blocked_sync": [
110
+ "No changes were written to Overleaf; every change was blocked before writing.",
111
+ "没有向 Overleaf 写入任何改动;所有改动都在写入前被拦下。"
112
+ ],
113
+ "unconfirmed_sync": [
114
+ "Codex tried to write local changes to Overleaf, but the write result did not confirm any applied or skipped entries.",
115
+ "Codex 已尝试把本地改动写入 Overleaf,但写入结果没有确认任何已写入或已跳过条目。"
116
+ ],
117
+ "partial_sync_note": [
118
+ "Sync note: local Codex changes were sent to Overleaf, but some items were skipped.",
119
+ "同步提示:本地 Codex 改动已尝试写回 Overleaf,但有部分项目被跳过。"
120
+ ],
121
+ "blocked_sync_note": [
122
+ "Sync note: no changes were written to Overleaf; every change was blocked before writing.",
123
+ "同步提示:没有向 Overleaf 写入任何改动;所有改动都在写入前被拦下。"
124
+ ],
125
+ "unconfirmed_sync_note": [
126
+ "Writeback note: Codex could not confirm any Overleaf write result entries, so local mirror refresh and auto compile were skipped.",
127
+ "写入提示:Codex 没能确认任何 Overleaf 写入结果条目,因此已跳过本地 mirror 刷新和自动编译。"
128
+ ],
129
+ "retry_sync": [
130
+ "Retried the remaining file transfers for this task.",
131
+ "已重试本轮未完成的文件同步。"
132
+ ],
133
+ "default_no_write": [
134
+ "This task completed without writing Overleaf files.",
135
+ "这轮任务已完成,没有写入 Overleaf 文件。"
136
+ ],
137
+ "default_complete": [
138
+ "This task completed.",
139
+ "这轮任务已完成。"
140
+ ],
141
+ "warm_mirror_stale": [
142
+ "This run did not continue: Overleaf did not provide the full project, and the local workspace is not fresh enough.",
143
+ "这轮没有继续:Overleaf 没有提供完整项目内容,本地 workspace 也不够新。"
144
+ ]
145
+ };
146
+
147
+ const ERROR_CONCLUSIONS = {
148
+ "local_skill_unavailable": {
149
+ "code": "codex_local_skill_unavailable"
150
+ },
151
+ "local_skills_failed": {
152
+ "code": "codex_local_skills_failed"
153
+ },
154
+ "mode_removed": {
155
+ "message": "suggest_mode_removed"
156
+ },
157
+ "mode_write_required": {
158
+ "message": "Mode must be \"ask\" or \"auto\""
159
+ },
160
+ "invalid_json": {
161
+ "message": "Agent returned invalid JSON"
162
+ },
163
+ "invalid_output": {
164
+ "message": "Could not parse Codex output"
165
+ },
166
+ "timeout": {
167
+ "message": "timed out"
168
+ },
169
+ "output_limit": {
170
+ "message": "output limit exceeded"
171
+ },
172
+ "context_limit": {
173
+ "code": "codex_context_limit"
174
+ },
175
+ "codex_not_found": {
176
+ "message": "codex_not_found"
177
+ },
178
+ "project_locked": {
179
+ "message": "project_locked"
180
+ },
181
+ "reasoning_unsupported": {
182
+ "message": "unsupported_parameter reasoning.summary"
183
+ },
184
+ "storage_quota": {
185
+ "message": "quota"
186
+ },
187
+ "checkpoint": {
188
+ "message": "checkpoint"
189
+ },
190
+ "stale_source": {
191
+ "message": "changed while Codex was working"
192
+ },
193
+ "post_processing": {
194
+ "codexReturned": true
195
+ },
196
+ "ask_failed": {
197
+ "mode": "ask"
198
+ },
199
+ "auto_failed": {
200
+ "mode": "auto"
201
+ },
202
+ "provider_revision_conflict": {
203
+ "message": "provider_revision_conflict"
204
+ },
205
+ "provider_auth_rejected": {
206
+ "message": "provider_auth_rejected"
207
+ },
208
+ "provider_model_not_found": {
209
+ "message": "provider_model_not_found"
210
+ },
211
+ "provider_model_not_configured": {
212
+ "message": "provider_model_not_configured"
213
+ },
214
+ "provider_protocol_unverified": {
215
+ "message": "provider_protocol_unverified"
216
+ },
217
+ "provider_protocol_incompatible": {
218
+ "message": "provider_protocol_incompatible"
219
+ },
220
+ "provider_rate_limited": {
221
+ "message": "provider_rate_limited"
222
+ },
223
+ "provider_connection_timeout": {
224
+ "message": "provider_connection_timeout"
225
+ },
226
+ "provider_dns_failed": {
227
+ "message": "provider_dns_failed"
228
+ },
229
+ "provider_tls_failed": {
230
+ "message": "provider_tls_failed"
231
+ },
232
+ "provider_not_found": {
233
+ "message": "provider_not_found"
234
+ },
235
+ "provider_store_corrupt": {
236
+ "message": "provider_store_corrupt"
237
+ },
238
+ "provider_store_unavailable": {
239
+ "message": "provider_store_unavailable"
240
+ },
241
+ "provider_response_invalid": {
242
+ "message": "provider_response_invalid"
243
+ }
244
+ };
245
+
28
246
  const TECHNICAL_EVENT_PATTERNS = [
29
247
  /^agent\.command\./,
30
248
  /^native\.task\./,
@@ -436,16 +654,11 @@
436
654
  }
437
655
 
438
656
  function isContextCompactionEvent(event = {}) {
439
- const type = String(event.type || '');
440
- const method = String(event.detail?.method || event.title || '');
441
- const label = `${type} ${method}`;
442
- return /(compact|compaction|compacted)/i.test(label)
443
- && /(context|thread|turn|conversation|codex)/i.test(label);
657
+ return Boolean(deps.RunActivityModel.contextCompactionState(event));
444
658
  }
445
659
 
446
660
  function formatContextCompactionCheckpoint(event = {}, locale = 'en') {
447
- const method = String(event.detail?.method || event.title || '');
448
- const running = event.status === 'running' || /(started|starting|begin|prepar)/i.test(method);
661
+ const running = deps.RunActivityModel.contextCompactionState(event) === 'running';
449
662
  return {
450
663
  kind: 'checkpoint',
451
664
  visible: true,
@@ -728,7 +941,7 @@
728
941
  };
729
942
  }
730
943
 
731
- function translateRawError(message, context = {}) {
944
+ function translateRawErrorCopy(message, context = {}) {
732
945
  const locale = normalizeLocale(context);
733
946
  const text = String(message || '');
734
947
  // Each branch returns the user-visible {conclusion, nextStep} pair and,
@@ -753,6 +966,13 @@
753
966
  failureCode: 'codex_local_skills_failed'
754
967
  };
755
968
  }
969
+ if (getFailureReasonsModule()?.isContextLimitError(text, context.code)) {
970
+ return {
971
+ conclusion: textFor(locale, '本轮已停止:上下文超出模型的容量限制。', 'This run stopped because the model context limit was exceeded.'),
972
+ nextStep: textFor(locale, '请重试任务;如果再次超限,可使用 /compact,或新建会话并减少上下文。', 'Retry the task. If the context limit is reached again, use /compact or start a new conversation with less context.'),
973
+ failureCode: 'codex_context_limit'
974
+ };
975
+ }
756
976
  const providerError = translateProviderError(text, locale);
757
977
  if (providerError) {
758
978
  return providerError;
@@ -871,12 +1091,107 @@
871
1091
  };
872
1092
  }
873
1093
 
1094
+ function productConclusionText(key, locale = 'en') {
1095
+ if (Object.hasOwn(PRODUCT_CONCLUSIONS, key)) return PRODUCT_CONCLUSIONS[key][locale === 'zh' ? 1 : 0];
1096
+ const error = key.startsWith('error:') && Object.hasOwn(ERROR_CONCLUSIONS, key.slice(6)) && ERROR_CONCLUSIONS[key.slice(6)];
1097
+ return error ? translateRawErrorCopy(error.message || '', { ...error, locale }).conclusion : null;
1098
+ }
1099
+
1100
+ function translateRawError(message, context = {}) {
1101
+ const translated = translateRawErrorCopy(message, context), locale = normalizeLocale(context);
1102
+ const key = Object.keys(ERROR_CONCLUSIONS).find(id => productConclusionText(`error:${id}`, locale) === translated.conclusion);
1103
+ return { ...translated, ...(key ? { conclusionKey: `error:${key}` } : {}) };
1104
+ }
1105
+
1106
+ // Old Native failures retained the real transport error even when their
1107
+ // product summary used the generic empty-result category. Re-project only
1108
+ // those identified product events; model prose and stored records stay intact.
1109
+ function projectContextLimitEvent(event, locale = 'en') {
1110
+ const failure = event?.failure;
1111
+ if (event?.status !== 'failed' || event.streamRole || event.guidanceId || event.subagent) return event;
1112
+ if (failure?.code === 'codex_context_limit' && failure.stage === 'codex'
1113
+ && (!event.kind || event.kind === 'activity')
1114
+ && ['en', 'zh'].some(language => productConclusionText('error:context_limit', language) === event.title)) {
1115
+ return { ...event, title: productConclusionText('error:context_limit', locale) };
1116
+ }
1117
+ if (failure?.code !== 'codex_no_usable_result' || failure.stage !== 'codex'
1118
+ || !getFailureReasonsModule()?.isContextLimitError(failure.technicalMessage)) return event;
1119
+ const generic = ['ask_failed', 'auto_failed'].flatMap(key => ['en', 'zh'].map(language =>
1120
+ productConclusionText(`error:${key}`, language)));
1121
+ const translated = translateRawError(failure.technicalMessage, { locale });
1122
+ const projectedFailure = { ...failure, code: translated.failureCode,
1123
+ userMessage: translated.conclusion, nextAction: translated.nextStep };
1124
+ if ((!event.kind || event.kind === 'activity') && generic.includes(event.title)) {
1125
+ return { ...event, title: translated.conclusion, failure: projectedFailure };
1126
+ }
1127
+ const structured = event.detailStructured, source = structured?.conclusionSource;
1128
+ if (event.kind !== 'report' || source?.origin !== 'product'
1129
+ || !['error:ask_failed', 'error:auto_failed'].includes(source.key)
1130
+ || !generic.includes(structured.conclusion)) return event;
1131
+ const oldNext = ['en', 'zh'].map(language => translateRawError('', { locale: language }).nextStep);
1132
+ return { ...event, failure: projectedFailure, detailStructured: { ...structured,
1133
+ conclusion: translated.conclusion, conclusionSource: { origin: 'product', key: translated.conclusionKey },
1134
+ meta: (structured.meta || []).map(row => row.key === 'nextStep' && oldNext.includes(row.value)
1135
+ ? { ...row, value: translated.nextStep } : row) } };
1136
+ }
1137
+
1138
+ function normalizeConclusionSource(source) {
1139
+ if (source?.origin !== 'product' || typeof source.key !== 'string') return null;
1140
+ if (productConclusionText(source.key) !== null) return { origin: 'product', key: source.key };
1141
+ if (source.key !== 'writeback' || !['synced', 'partial_sync', 'blocked_sync', 'unconfirmed_sync'].includes(source.outcome)) return null;
1142
+ return { origin: 'product', key: 'writeback', outcome: source.outcome,
1143
+ prefixLength: Number.isSafeInteger(source.prefixLength) && source.prefixLength >= 0 ? source.prefixLength : -1,
1144
+ prefixKey: source.prefixKey === 'retry_sync' ? 'retry_sync' : '',
1145
+ hasCompile: source.hasCompile === true,
1146
+ compileStatus: typeof source.compileStatus === 'string' ? source.compileStatus : '',
1147
+ compileReason: typeof source.compileReason === 'string' ? source.compileReason : '',
1148
+ errorCount: Number.isSafeInteger(source.errorCount) && source.errorCount >= 0 ? source.errorCount : 0,
1149
+ errors: Array.isArray(source.errors) ? source.errors.slice(0, 3).filter(value => typeof value === 'string') : [],
1150
+ warningCount: Number.isSafeInteger(source.warningCount) && source.warningCount >= 0 ? source.warningCount : 0 };
1151
+ }
1152
+
1153
+ function projectProductConclusion(source, locale = 'en', localizeCompileReason = value => value, raw = '') {
1154
+ const identity = normalizeConclusionSource(source);
1155
+ if (!identity) return null;
1156
+ if (identity.key !== 'writeback') return productConclusionText(identity.key, locale);
1157
+ if (identity.prefixLength < 0 || identity.prefixLength > raw.length) return null;
1158
+ const originalPrefix = raw.slice(0, identity.prefixLength);
1159
+ if (identity.prefixKey && !['en', 'zh'].some(language => productConclusionText(identity.prefixKey, language) === originalPrefix)) return null;
1160
+ const compose = (language, projectReason) => {
1161
+ const tx = (en, zh) => language === 'zh' ? zh : en;
1162
+ const prefix = projectReason && identity.prefixKey ? productConclusionText(identity.prefixKey, language) : originalPrefix;
1163
+ const note = productConclusionText(identity.outcome + (prefix && identity.outcome !== 'synced' ? '_note' : ''), language);
1164
+ const conclusion = prefix ? [prefix, identity.outcome === 'synced' ? '' : note].filter(Boolean).join('\n\n') : note;
1165
+ const { compileStatus: status, errors, errorCount, warningCount: warnings } = identity;
1166
+ if (!identity.hasCompile) return conclusion;
1167
+ const reason = projectReason ? localizeCompileReason(identity.compileReason) : identity.compileReason;
1168
+ const summary = errorCount
1169
+ ? tx(`Post-write compile check: ${errorCount} remaining error(s): ${errors.slice(0, 3).join('; ')}`,
1170
+ `写后编译检查:仍有 ${errorCount} 个错误:${errors.slice(0, 3).join(';')}`)
1171
+ : status === 'success' ? (warnings
1172
+ ? tx(`Post-write compile check succeeded with ${warnings} warning(s).`, `写后编译检查已通过,但仍有 ${warnings} 个警告。`)
1173
+ : tx('Post-write compile check succeeded with no reported errors.', '写后编译检查已通过,未发现错误。'))
1174
+ : status === 'triggered' ? (warnings
1175
+ ? tx(`Post-write compile was triggered; latest log shows ${warnings} warning(s).`, `已触发写后编译;最新日志显示 ${warnings} 个警告。`)
1176
+ : tx('Post-write compile was triggered; Overleaf may still be updating the result.', '已触发写后编译;Overleaf 可能仍在更新结果。'))
1177
+ : status === 'unconfirmed' ? getI18nModule().t(language, 'runCompileUnconfirmed')
1178
+ : status === 'failed' ? tx(`Post-write compile check failed: ${reason || 'unknown reason'}`,
1179
+ `写后编译检查失败:${reason || '未知原因'}`)
1180
+ : tx(`Post-write compile finished with status: ${status || 'unknown'}.`, `写后编译完成,状态:${status || '未知'}。`);
1181
+ return [conclusion, summary].filter(Boolean).join('\n');
1182
+ };
1183
+ // A compacted or changed raw answer must never be reconstructed from metadata.
1184
+ if (!['en', 'zh'].some(language => compose(language, false) === raw)) return null;
1185
+ return compose(locale, true);
1186
+ }
1187
+
874
1188
  function buildHumanCompletionReport(input = {}) {
875
1189
  const locale = normalizeLocale(input);
876
1190
  const applyResults = normalizeApplyResults(input.applyResults);
877
1191
  const appliedCount = countApplyResultEntries(applyResults, 'applied');
878
1192
  const skippedCount = countApplyResultEntries(applyResults, 'skipped');
879
- const translatedError = input.errorMessage ? translateRawError(input.errorMessage, { mode: input.mode, locale }) : null;
1193
+ const translatedError = input.errorMessage ? translateRawError(input.errorMessage, { mode: input.mode, locale, code: input.failure?.code,
1194
+ codexReturned: input.conclusionKey === 'error:post_processing' }) : null;
880
1195
  const userReport = normalizeUserReport(input.userReport);
881
1196
  const report = userReport || buildFallbackReport(input, {
882
1197
  appliedCount,
@@ -928,6 +1243,16 @@
928
1243
  return report.text;
929
1244
  }
930
1245
 
1246
+ function completionConclusionText(report) {
1247
+ const source = report.conclusionSource;
1248
+ // The new blocked composite preserves its exact model prefix. Keep the
1249
+ // historical formatter for every other report shape.
1250
+ if (source?.key === 'writeback' && source.outcome === 'blocked_sync'
1251
+ && typeof report.conclusion === 'string' && ['en', 'zh'].some(locale =>
1252
+ projectProductConclusion(source, locale, undefined, report.conclusion) === report.conclusion)) return report.conclusion;
1253
+ return cleanVisibleMarkdownText(report.conclusion || '');
1254
+ }
1255
+
931
1256
  /**
932
1257
  * Splits the human report into a conclusion (the human-language outcome),
933
1258
  * a body of list-style content sections (checked / findings / planned /
@@ -938,7 +1263,7 @@
938
1263
  * shape for transcripts, storage fallback, and existing assertions.
939
1264
  */
940
1265
  function buildStructuredHumanReport(report = {}, locale = 'en') {
941
- const conclusion = cleanVisibleMarkdownText(report.conclusion || '');
1266
+ const conclusion = completionConclusionText(report);
942
1267
 
943
1268
  const bodySections = [];
944
1269
  addListSection(bodySections, textFor(locale, '检查范围', 'Checked'), report.checked, locale);
@@ -982,12 +1307,13 @@
982
1307
  }
983
1308
 
984
1309
  if (report.saveState) meta.push({ key: 'saveState', label: textFor(locale, '保存', 'Save'), value: report.saveState });
985
- return { conclusion, body: bodySections.join('\n\n'), meta };
1310
+ return { conclusion, body: bodySections.join('\n\n'), meta,
1311
+ ...(normalizeConclusionSource(report.conclusionSource) ? { conclusionSource: normalizeConclusionSource(report.conclusionSource) } : {}) };
986
1312
  }
987
1313
 
988
1314
  function formatHumanReport(report = {}, locale = 'en') {
989
1315
  const sections = [];
990
- const conclusion = cleanVisibleMarkdownText(report.conclusion || '');
1316
+ const conclusion = completionConclusionText(report);
991
1317
  if (conclusion) {
992
1318
  sections.push(textFor(locale, `结论:${conclusion}`, `Conclusion: ${conclusion}`));
993
1319
  }
@@ -1028,8 +1354,13 @@
1028
1354
  ? textFor(locale, '这轮任务已完成,没有写入 Overleaf 文件。', 'This task completed without writing Overleaf files.')
1029
1355
  : textFor(locale, '这轮任务已完成。', 'This task completed.'));
1030
1356
 
1357
+ const conclusionSource = counts.translatedError?.conclusionKey
1358
+ ? { origin: 'product', key: counts.translatedError.conclusionKey }
1359
+ : input.conclusionKey ? { origin: 'product', key: input.conclusionKey }
1360
+ : input.conclusionSource || (!input.conclusion && !input.notes
1361
+ ? { origin: 'product', key: noWrites ? 'default_no_write' : 'default_complete' } : null);
1031
1362
  return {
1032
- conclusion,
1363
+ conclusion, conclusionSource: normalizeConclusionSource(conclusionSource),
1033
1364
  checked: normalizeStringList(input.checked || input.userReport?.checked),
1034
1365
  findings: normalizeStringList(input.findings),
1035
1366
  plannedChanges: counts.appliedCount ? [] : operations.map(operation => formatOperationLine(operation, locale)),
@@ -1756,6 +2087,8 @@
1756
2087
 
1757
2088
  return {
1758
2089
  buildHumanCompletionReport,
2090
+ projectProductConclusion,
2091
+ projectContextLimitEvent,
1759
2092
  buildStructuredHumanReport,
1760
2093
  formatFallbackFinalReport,
1761
2094
  formatFailureBlock,
@@ -32,6 +32,8 @@
32
32
  createdAt: redactSecretLikeText(stringField(input.createdAt)) || now,
33
33
  completedAt: '',
34
34
  mode: redactSecretLikeText(stringField(input.mode)),
35
+ requireReviewing: typeof input.requireReviewing === 'boolean' ? input.requireReviewing : undefined,
36
+ trackingDecision: ['continue', 'disable'].includes(input.trackingDecision) ? input.trackingDecision : '',
35
37
  providerId: redactSecretLikeText(stringField(input.providerId)) || 'builtin',
36
38
  providerName: redactSecretLikeText(stringField(input.providerName)),
37
39
  providerRevision: finiteNumber(input.providerRevision) || 0,
@@ -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.5.1';
15
+ const BUILD_TARGET_VERSION = '2.6.0';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -11,31 +11,46 @@
11
11
  const COMPILABLE_EXTENSIONS = new Set(['.tex', '.bib', '.sty', '.cls', '.bst', '.bbx', '.cbx']);
12
12
  function parseCompileResponse(response) {
13
13
  if (!response || typeof response !== 'object') return { ok: false, reason: 'Invalid compile response: not an object' };
14
- if (!Array.isArray(response.outputFiles)) return { ok: false, reason: 'Invalid compile response: outputFiles is not an array' };
15
- const logFile = response.outputFiles.find(function (file) {
14
+ if (!Array.isArray(response.outputFiles)) return { ok: false, status: response.status, reason: 'Invalid compile response: outputFiles is not an array' };
15
+ const logFile = response.outputFiles.find(file => /(^|\/)output\.log$/.test(file?.path || '')) || response.outputFiles.find(function (file) {
16
16
  if (!file) return false;
17
17
  if (file.type === 'log') return true;
18
18
  if (typeof file.path === 'string' && file.path.endsWith('.log')) return true;
19
19
  return false;
20
20
  });
21
- if (!logFile) return { ok: false, reason: 'No log file found in compile output' };
22
- var logUrl = logFile.url || null;
23
- if (!logUrl) return { ok: false, reason: 'Log file has no URL' };
21
+ if (!logFile) return { ok: false, status: response.status, reason: 'No log file found in compile output' };
22
+ var logUrl = typeof logFile.url === 'string' ? logFile.url : null;
23
+ if (!logUrl) return { ok: false, status: response.status, reason: 'Log file has no URL' };
24
24
  return {
25
25
  ok: true,
26
26
  status: response.status,
27
+ fromCache: response.fromCache === true,
27
28
  logUrl: logUrl,
28
- logPath: logFile.path || null
29
+ logPath: logFile.path || null,
30
+ build: logFile.build || null,
31
+ pdfDownloadDomain: normalizePdfDownloadDomain(response.pdfDownloadDomain),
32
+ clsiServerId: typeof response.clsiServerId === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(response.clsiServerId)
33
+ ? response.clsiServerId : null
29
34
  };
30
35
  }
31
36
 
37
+ function normalizePdfDownloadDomain(value) {
38
+ if (typeof value !== 'string' || !/^https:\/\/[^\s\\]+$/i.test(value)) return null;
39
+ try {
40
+ const url = new URL(value);
41
+ return url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash ? value : null;
42
+ } catch (_error) {
43
+ return null;
44
+ }
45
+ }
46
+
32
47
  function extractErrorBlocks(lines) {
33
48
  var blocks = [];
34
49
  var i = 0;
35
50
 
36
51
  while (i < lines.length && blocks.length < 20) {
37
52
  var line = lines[i];
38
- if (line.startsWith('!') || /^l\.\d+/.test(line)) {
53
+ if (isLogErrorLine(line) || /^l\.\d+/.test(line)) {
39
54
  var block = [];
40
55
  // Collect surrounding context: go back up to 2 lines for pre-context
41
56
  var start = Math.max(0, i - 2);
@@ -106,6 +121,12 @@
106
121
  return COMPILABLE_EXTENSIONS.has(ext);
107
122
  }
108
123
 
124
+ function isLogErrorLine(line) {
125
+ // TeX's -file-line-error output (used by Overleaf) has no leading '!'.
126
+ return line.startsWith('!') || /^Runaway argument/.test(line)
127
+ || (/^(?:\.{0,2}\/|[A-Za-z]:[\\/])?.+?:\d+:\s+\S/.test(line) && !/\bWarning\b/i.test(line));
128
+ }
129
+
109
130
  function parseLogErrors(logContent) {
110
131
  var result = { errors: [], warnings: [] };
111
132
 
@@ -115,20 +136,23 @@
115
136
 
116
137
  var lines = logContent.split('\n');
117
138
 
118
- // Extract errors: lines starting with '!' plus up to 3 following context lines
139
+ // Retain the bounded error list; consumers limit displayed snippets separately.
119
140
  for (var i = 0; i < lines.length && result.errors.length < 50; i++) {
120
- if (lines[i].startsWith('!')) {
141
+ if (isLogErrorLine(lines[i])) {
121
142
  var errorLines = [lines[i]];
122
143
  for (var j = 1; j <= 3 && (i + j) < lines.length; j++) {
144
+ if (!lines[i + j].trim() || isLogErrorLine(lines[i + j])) break;
123
145
  errorLines.push(lines[i + j]);
124
146
  }
125
147
  result.errors.push(errorLines.join('\n'));
126
148
  }
127
149
  }
128
150
 
129
- // Extract warnings: lines matching /Warning/i that don't start with whitespace
151
+ // Count only the warning forms Overleaf's own log parser reports, so
152
+ // informational lines such as "Package: infwarerr … info/warning/error
153
+ // messages" are not shown as warnings.
130
154
  for (var k = 0; k < lines.length && result.warnings.length < 50; k++) {
131
- if (/Warning/i.test(lines[k]) && !/^\s/.test(lines[k])) {
155
+ if (/^LaTeX(?:3| Font)? Warning: /.test(lines[k]) || /^(?:Package|Class|Module) \b.+\b Warning:/.test(lines[k])) {
132
156
  result.warnings.push(lines[k]);
133
157
  }
134
158
  }
@@ -137,18 +161,35 @@
137
161
  }
138
162
 
139
163
  function buildPostWriteCompileSummary({ result = null, logResult = null, error = null } = {}) {
140
- const logAvailable = logResult?.ok === true;
164
+ const captured = result?.ok === true && result.resultCaptured === true
165
+ && Number.isSafeInteger(result.compile?.captureId) && result.compile.captureId > 0;
166
+ const logAvailable = captured && logResult?.ok === true && logResult.fresh === true
167
+ && result.compile?.captureId === logResult.captureId;
141
168
  const errors = logAvailable && Array.isArray(logResult.errors)
142
- ? logResult.errors.slice(0, 5).map(formatCompileDiagnosticForSummary)
169
+ ? logResult.errors.map(formatCompileDiagnosticForSummary)
143
170
  : [];
144
171
  const warnings = logAvailable && Array.isArray(logResult.warnings)
145
172
  ? logResult.warnings.slice(0, 5).map(formatCompileDiagnosticForSummary)
146
173
  : [];
147
- const failed = Boolean(error) || !result?.ok;
174
+ const compilerFailed = captured && logResult?.fresh !== false
175
+ && ['failed', 'failure', 'error', 'timedout', 'timeout'].includes(result.compile?.status);
176
+ if (compilerFailed && !errors.length) errors.push('Post-write compile failed.');
177
+ const failed = compilerFailed || errors.length > 0;
178
+ const success = captured && result.compile?.ok === true && result.compile.status === 'success'
179
+ && result.compile.fromCache !== true && logAvailable;
180
+ const status = failed ? 'failed' : success ? 'success' : result?.ok && !captured ? 'triggered' : 'unconfirmed';
181
+ let reason = error ? error?.message || 'Post-write compile failed.'
182
+ : !result?.ok ? result?.reason || 'Post-write compile did not succeed.' : result.reason || '';
183
+ if (status === 'unconfirmed' || status === 'triggered') {
184
+ // Technical evidence belongs in the existing activity output, not the outcome label.
185
+ reason ||= logResult?.reason || (!captured ? 'compile_capture_missing'
186
+ : result.compile?.reason || (result.compile.fromCache ? 'compile_response_from_cache'
187
+ : logResult?.captureId !== result.compile.captureId ? 'compile_log_capture_mismatch'
188
+ : logResult?.fresh === false ? 'compile_log_stale' : 'compile_result_unconfirmed'));
189
+ }
148
190
  return {
149
- status: failed ? 'failed' : result.compile?.status || 'triggered',
150
- reason: error ? error?.message || 'Post-write compile failed.'
151
- : !result?.ok ? result?.reason || 'Post-write compile did not succeed.' : result.reason || '',
191
+ status,
192
+ reason,
152
193
  errors,
153
194
  warnings,
154
195
  logAvailable
@@ -51,7 +51,12 @@
51
51
  if (!attachments.length) return;
52
52
  throw failure('composer_draft_cleared');
53
53
  }
54
- if (value.revision !== revision) throw failure('composer_draft_conflict');
54
+ if (value.revision !== revision) {
55
+ // Writing what another tab already stored changes nothing (for example
56
+ // both tabs clearing an empty draft on send); only a real edit conflicts.
57
+ if (JSON.stringify(attachments) === JSON.stringify(result.attachments)) return;
58
+ throw failure('composer_draft_conflict');
59
+ }
55
60
  if (attachments.length && value.unbased && clearedAt && value.changedAt <= clearedAt) {
56
61
  throw failure('composer_draft_cleared');
57
62
  }