codex-overleaf-link 2.5.0 → 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 (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -100,6 +100,21 @@
100
100
  return pending;
101
101
  }
102
102
 
103
+ function selectionViolationNextStep() {
104
+ return tx('Select the intended region again and request only changes within it. Valid written parts can be undone.',
105
+ '请重新选择目标范围,仅请求修改该范围。已写入的合法部分可撤销。');
106
+ }
107
+
108
+ function buildWithheldEntry(change) {
109
+ const reason = writebackController.formatUnsupportedLocalChangeReason(change, getLocale());
110
+ const nextAction = change.reason === 'selection_scope_violation' ? selectionViolationNextStep()
111
+ : tx('The main agent must review the withheld file before syncing it.', '主代理需核对未同步文件后再写回。');
112
+ return { operation: { type: 'create', path: change.path }, result: { ok: false,
113
+ code: change.reason, reason, changedDocument: false, failure: { code: change.reason,
114
+ stage: 'preflight', severity: 'blocked', retryable: false, terminalState: 'blocked', changedDocument: false,
115
+ userMessage: reason, nextAction } } };
116
+ }
117
+
103
118
  async function applySyncChangesToOverleaf(syncChanges = [], project = {}, options = {}) {
104
119
  const activeRun = getCurrentRunView?.();
105
120
  const saveTarget = { runProjectId: activeRun?.runProjectId || '', recordId: activeRun?.recordId || '', sessionId: activeRun?.sessionId || '' };
@@ -110,20 +125,23 @@
110
125
  const assistantMessage = cleanFinalAnswer(options.assistantMessage || getAssistantAnswerForCurrentRun());
111
126
  const unsupportedChanges = Array.isArray(options.unsupportedChanges) ? options.unsupportedChanges : [];
112
127
  appendUnsupportedLocalChanges(unsupportedChanges);
113
- const withheldOnly = unsupportedChanges.filter(change => ['subagent_unfinished_output', 'subagent_unauthorized_edit'].includes(change.reason));
128
+ const withheldOnly = unsupportedChanges.filter(change => ['subagent_unfinished_output', 'subagent_unauthorized_edit', 'selection_scope_violation'].includes(change.reason));
114
129
  if (runMode !== 'ask' && !syncChanges.length && withheldOnly.length) {
115
- const skipped = withheldOnly.map(change => ({ operation: { type: 'create', path: change.path }, result: {
116
- ok: false, code: change.reason, changedDocument: false,
117
- reason: writebackController.formatUnsupportedLocalChangeReason(change, getLocale())
118
- } }));
130
+ const skipped = withheldOnly.map(buildWithheldEntry);
131
+ const hasSelectionViolation = withheldOnly.some(change => change.reason === 'selection_scope_violation');
119
132
  const applied = { ok: false, applied: [], skipped };
120
- appendCompletionReport({ conclusion: assistantMessage || tx('Some generated files still need review.', '部分生成文件仍需核对。'),
133
+ appendCompletionReport({ conclusionKey: options.assistantMessageKey || (!assistantMessage ? 'withheld_changes' : undefined), conclusion: assistantMessage || tx('Some generated changes were blocked.', '部分生成的改动已被阻止。'),
121
134
  status: 'failed', operations: [], applyResults: [applied], mode: runMode,
122
- nextStep: tx('The main agent must review and adopt the withheld files before syncing them.', '主代理需核对并接管未同步文件后再写回。') });
123
- return { summaryLine: tx('Generated files are awaiting review.', '生成文件待核对。'), hasSkippedOperations: true,
135
+ nextStep: hasSelectionViolation
136
+ ? selectionViolationNextStep()
137
+ : tx('The main agent must review and adopt the withheld files before syncing them.', '主代理需核对并接管未同步文件后再写回。') });
138
+ return { summaryLine: hasSelectionViolation
139
+ ? tx('Changes outside the selection were blocked.', '选区外改动已被阻止。')
140
+ : tx('Generated files are awaiting review.', '生成文件待核对。'), hasSkippedOperations: true,
124
141
  applied, settlement: buildSettlement({ operations: [], applyResult: applied }),
125
142
  audit: buildAuditSummaryFromApply({ operations: [], applyResults: [applied], resultStatus: 'completed_with_skips' }) };
126
143
  }
144
+
127
145
  // Guard on the immutable submitted mode, not the mutable panel state.
128
146
  if (options.mode === 'ask' && ((syncChanges || []).length || unsupportedChanges.length)) {
129
147
  appendRunEvent({
@@ -133,7 +151,7 @@
133
151
  ),
134
152
  status: 'warning'
135
153
  });
136
- appendCompletionReport({
154
+ appendCompletionReport({ conclusionKey: options.assistantMessageKey || (!assistantMessage ? 'ask_ignored_changes' : undefined),
137
155
  conclusion: assistantMessage || tx(
138
156
  'Codex finished in Ask mode. Any local file changes were ignored and were not synced to Overleaf.',
139
157
  'Codex 已在 Ask 模式完成;本地文件改动已忽略,未同步到 Overleaf。'
@@ -166,7 +184,16 @@
166
184
  }
167
185
  let operations = buildSyncApplyOperations(syncChanges, project);
168
186
  let visibleSyncChanges = syncChanges || [];
169
- let additionalSkippedEntries = [];
187
+ // A previous Native build can still report inferred metadata moves.
188
+ // Forward sync never promotes them to a new file or a new capability.
189
+ let additionalSkippedEntries = operations.filter(operation => operation.type === 'move').map(operation => ({
190
+ operation,
191
+ result: { ok: false, code: 'file_tree_controls_unavailable', changedDocument: false,
192
+ reason: tx('Automatic file moves are unavailable. Handle the path change in Overleaf before retrying.',
193
+ '暂不支持自动移动文件,请先在 Overleaf 中处理路径变化后再重试。') }
194
+ }));
195
+ operations = operations.filter(operation => operation.type !== 'move');
196
+ if (additionalSkippedEntries.length) visibleSyncChanges = filterSyncChangesByOperations(syncChanges, operations);
170
197
  let skippedFilesForAudit = additionalSkippedEntries;
171
198
  let appliedFilesForAudit = [];
172
199
  const pathSafety = partitionUnsafeProjectPathOperations(operations);
@@ -191,13 +218,27 @@
191
218
  visibleSyncChanges = filterSyncChangesByOperations(syncChanges, operations);
192
219
  }
193
220
  if (!operations.length) {
221
+ const blockedSettlement = buildSettlement({
222
+ operations: buildSyncApplyOperations(syncChanges, project),
223
+ applyResult: { ok: additionalSkippedEntries.length === 0, applied: [], skipped: additionalSkippedEntries }
224
+ });
225
+ const blockedFailure = blockedSettlement
226
+ ? writebackSettlement.projectRunSettlement({ settlement: blockedSettlement.facts }).primaryFailure : undefined;
194
227
  appendRunEvent({
195
228
  title: tx('Codex did not produce file changes that need to sync back to Overleaf.', 'Codex 没有产生需要同步回 Overleaf 的文件改动。'),
196
229
  status: 'completed'
197
230
  });
198
- appendCompletionReport({
199
- conclusion: assistantMessage || tx('Codex finished locally. There are no changes to sync back to Overleaf.', 'Codex 已完成本地处理,没有需要同步回 Overleaf 的改动。'),
200
- status: runMode === 'ask' ? tr('modeAsk') : 'completed',
231
+ const blockedConclusion = assistantMessage
232
+ ? `${assistantMessage}\n\n${tx('Sync note: no changes were written to Overleaf; every change was blocked before writing.', '同步提示:没有向 Overleaf 写入任何改动;所有改动都在写入前被拦下。')}`
233
+ : tx('No changes were written to Overleaf; every change was blocked before writing.', '没有向 Overleaf 写入任何改动;所有改动都在写入前被拦下。');
234
+ appendCompletionReport({ conclusionKey: additionalSkippedEntries.length ? undefined
235
+ : options.assistantMessageKey || (!assistantMessage ? 'no_sync_changes' : undefined),
236
+ conclusion: additionalSkippedEntries.length ? blockedConclusion
237
+ : assistantMessage || tx('Codex finished locally. There are no changes to sync back to Overleaf.', 'Codex 已完成本地处理,没有需要同步回 Overleaf 的改动。'),
238
+ conclusionSource: additionalSkippedEntries.length ? { origin: 'product', key: 'writeback',
239
+ outcome: 'blocked_sync', prefixLength: assistantMessage.length } : undefined,
240
+ status: additionalSkippedEntries.length ? 'failed' : runMode === 'ask' ? tr('modeAsk') : 'completed',
241
+ failure: blockedFailure,
201
242
  operations: [],
202
243
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
203
244
  unchangedReason: additionalSkippedEntries.find(item => item?.result?.reason)?.result.reason
@@ -208,14 +249,7 @@
208
249
  return {
209
250
  summaryLine: assistantMessage || tx('No changes to sync', '没有需要同步的改动'),
210
251
  hasSkippedOperations: additionalSkippedEntries.length > 0,
211
- settlement: buildSettlement({
212
- operations: buildSyncApplyOperations(syncChanges, project),
213
- applyResult: {
214
- ok: additionalSkippedEntries.length === 0,
215
- applied: [],
216
- skipped: additionalSkippedEntries
217
- }
218
- }),
252
+ settlement: blockedSettlement,
219
253
  audit: buildAuditSummaryFromApply({
220
254
  operations: buildSyncApplyOperations(syncChanges, project),
221
255
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
@@ -258,13 +292,20 @@
258
292
  additionalSkippedEntries.push(...binaryDecision.skipped);
259
293
 
260
294
  if (!operations.length) {
295
+ const approvalSettlement = buildSettlement({
296
+ operations: buildSyncApplyOperations(syncChanges, project),
297
+ applyResult: { ok: additionalSkippedEntries.length === 0, applied: [], skipped: additionalSkippedEntries }
298
+ });
299
+ const approvalFailure = approvalSettlement
300
+ ? writebackSettlement.projectRunSettlement({ settlement: approvalSettlement.facts }).primaryFailure : undefined;
261
301
  appendRunEvent({
262
302
  title: tx('No approved file changes remain to sync back to Overleaf.', '没有剩余已确认的文件改动需要同步回 Overleaf。'),
263
303
  status: 'completed'
264
304
  });
265
- appendCompletionReport({
305
+ appendCompletionReport({ conclusionKey: options.assistantMessageKey || (!assistantMessage ? 'no_approved_changes' : undefined),
266
306
  conclusion: assistantMessage || tx('No approved changes were written back to Overleaf.', '没有已确认的改动写回 Overleaf。'),
267
- status: runMode === 'ask' ? tr('modeAsk') : 'completed',
307
+ status: additionalSkippedEntries.length ? 'failed' : runMode === 'ask' ? tr('modeAsk') : 'completed',
308
+ failure: approvalFailure,
268
309
  operations: [],
269
310
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
270
311
  unchangedReason: additionalSkippedEntries.find(item => item?.result?.reason)?.result.reason
@@ -275,14 +316,7 @@
275
316
  return {
276
317
  summaryLine: tx('No changes applied', '没有应用改动'),
277
318
  hasSkippedOperations: additionalSkippedEntries.length > 0,
278
- settlement: buildSettlement({
279
- operations: buildSyncApplyOperations(syncChanges, project),
280
- applyResult: {
281
- ok: additionalSkippedEntries.length === 0,
282
- applied: [],
283
- skipped: additionalSkippedEntries
284
- }
285
- }),
319
+ settlement: approvalSettlement,
286
320
  audit: buildAuditSummaryFromApply({
287
321
  operations: buildSyncApplyOperations(syncChanges, project),
288
322
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
@@ -299,8 +333,8 @@
299
333
  if (!reviewing.ok) {
300
334
  const blocked = buildReviewingBlockedApplyResult(operations, reviewing);
301
335
  appendApplyResult(blocked);
302
- appendCompletionReport({
303
- conclusion: tx(
336
+ appendCompletionReport({ conclusionKey: 'reviewing_unavailable',
337
+ conclusion: reviewing.code === 'reviewing_unavailable' ? tr('failureReason_reviewing_unavailable_user') : tx(
304
338
  'No files were written: Track is enabled, but Codex could not verify Overleaf Reviewing/Track Changes.',
305
339
  '这轮没有写入:已开启“留痕”要求,但 Codex 没能确认 Overleaf 正在用 Reviewing/Track Changes。'
306
340
  ),
@@ -308,7 +342,7 @@
308
342
  operations,
309
343
  applyResults: [blocked],
310
344
  mode: runMode,
311
- nextStep: tx(
345
+ nextStep: reviewing.code === 'reviewing_unavailable' ? tr('failureReason_reviewing_unavailable_next') : tx(
312
346
  'Switch Overleaf to Reviewing/Track Changes manually and rerun, or turn off Track before writing.',
313
347
  '请在 Overleaf 手动切到 Reviewing/Track Changes 后重新运行,或关闭“留痕”再写入。'
314
348
  )
@@ -336,7 +370,7 @@
336
370
  status: 'running',
337
371
  activity: { v: 1, kind: 'sync', id: writebackActivityId, state: 'running', paths: operations.map(operation => operation.path).filter(Boolean) }
338
372
  });
339
- await stageWritebackRecovery?.(operations, saveTarget, runRequireReviewing);
373
+ await stageWritebackRecovery?.(operations, saveTarget, runRequireReviewing, project);
340
374
  const applied = operations.length
341
375
  ? mergeApplyResultSkipped(await assetTransferBroker.applyOperations({
342
376
  operations,
@@ -351,12 +385,8 @@
351
385
  }), additionalSkippedEntries)
352
386
  : mergeApplyResultSkipped({ ok: true, applied: [], skipped: [] }, additionalSkippedEntries);
353
387
  const hasConfirmedApplyResult = hasApplyResultEntries(applied);
354
- for (const change of unsupportedChanges.filter(item => ['subagent_unfinished_output', 'subagent_unauthorized_edit'].includes(item.reason))) {
355
- const reason = writebackController.formatUnsupportedLocalChangeReason(change, getLocale());
356
- applied.skipped.push({ operation: { type: 'create', path: change.path }, result: { ok: false,
357
- code: change.reason, reason, changedDocument: false, failure: { code: change.reason,
358
- stage: 'preflight', severity: 'blocked', retryable: false, terminalState: 'blocked', changedDocument: false,
359
- userMessage: reason, nextAction: 'The main agent must review the withheld file before syncing it.' } } });
388
+ for (const change of unsupportedChanges.filter(item => ['subagent_unfinished_output', 'subagent_unauthorized_edit', 'selection_scope_violation'].includes(item.reason))) {
389
+ applied.skipped.push(buildWithheldEntry(change));
360
390
  applied.ok = false;
361
391
  }
362
392
  // Record the apply result + undo checkpoint IMMEDIATELY, before any
@@ -367,7 +397,7 @@
367
397
  // landed in Overleaf. The manual-confirm path already records undo right
368
398
  // after its write — match that ordering here (v1.6.2).
369
399
  appendApplyResult(applied);
370
- recordUndoFromApply(project, applied);
400
+ await recordUndoFromApply(project, applied);
371
401
  const skippedEntries = getSkippedEntries(applied);
372
402
  if (skippedEntries.length) {
373
403
  appendPartialWritebackWarning(applied);
@@ -392,7 +422,7 @@
392
422
  ? await verifyPostWriteSaveState(applied, project, saveTarget)
393
423
  : { verification: { ok: false, state: 'not_checked', reason: 'No applied writeback operations were returned.' } };
394
424
  const saveVerification = confirmation.verification;
395
- await settleWritebackRecovery?.(applied, project, saveVerification, saveTarget);
425
+ await settleWritebackRecovery?.(applied, project, saveVerification, saveTarget, runRequireReviewing);
396
426
  if (appliedPaths.length) {
397
427
  appendPostWriteSaveVerificationWarning(saveVerification, saveTarget);
398
428
  appendRunEvent({ kind: 'technical', title: 'Post-write save confirmation', status: 'info', detail: saveVerification });
@@ -430,7 +460,7 @@
430
460
  title: tx(`Post-write compile failed: ${error.message}`, `写后编译出错:${error.message}`),
431
461
  status: 'failed'
432
462
  });
433
- return buildPostWriteCompileSummary({ error });
463
+ return compileAdapter.buildPostWriteCompileSummary({ error });
434
464
  })
435
465
  : null;
436
466
  const hasSkippedApplyResult = skippedEntries.length > 0;
@@ -462,6 +492,10 @@
462
492
  : tx('Review the synced file in Overleaf.', '请在 Overleaf 中查看同步后的文件。');
463
493
  appendCompletionReport({
464
494
  conclusion: appendCompileSummaryToConclusion(writebackConclusion, compileSummary),
495
+ conclusionSource: { origin: 'product', key: 'writeback',
496
+ outcome: !hasConfirmedApplyResult ? 'unconfirmed_sync' : hasSkippedApplyResult ? 'partial_sync' : 'synced',
497
+ prefixLength: assistantMessage.length, prefixKey: options.assistantMessageKey, hasCompile: Boolean(compileSummary), compileStatus: compileSummary?.status || '',
498
+ compileReason: compileAdapter.localizeCompileReason(compileSummary?.reason || '', getLocale()), errors: compileSummary?.errors?.slice(0, 3) || [], errorCount: compileSummary?.errors?.length || 0, warningCount: compileSummary?.warnings?.length || 0 },
465
499
  status: writebackIncomplete ? 'failed' : 'completed',
466
500
  operations,
467
501
  applyResults: [applied],
@@ -568,37 +602,29 @@
568
602
  return 'failed';
569
603
  }
570
604
 
571
- // v1.8.0: text-only writebacks confirm the mirror in place. The written
572
- // content ORIGINATED in the local workspace, so after a verified writeback
573
- // the workspace already equals Overleaf; only the baseline hashes and
574
- // freshness metadata are stale. mirror.confirmWriteback re-hashes the
575
- // written files without re-downloading the project. Anything unusual —
576
- // tree ops, binary writes, dirty mirror, an older native host without the
577
- // method — falls back to the full zip resync below.
578
- const MIRROR_CONFIRMABLE_OPERATION_TYPES = new Set(['edit']);
605
+ // Text-only confirmation requires the page's verified post-image digest.
606
+ // Local files can change after writeback; Native must match those digests
607
+ // before advancing its baseline. Missing evidence and non-text/tree writes
608
+ // retain the authoritative ZIP fallback below.
579
609
 
580
610
  async function tryConfirmMirrorWriteback(applied, runProjectId) {
581
- const entries = Array.isArray(applied?.applied) ? applied.applied : [];
582
- if (!entries.length) {
583
- return false;
584
- }
585
- const paths = [];
586
- for (const entry of entries) {
587
- const operation = entry?.operation || {};
588
- if (!MIRROR_CONFIRMABLE_OPERATION_TYPES.has(operation.type) || !operation.path) {
589
- return false;
590
- }
591
- paths.push(operation.path);
592
- }
593
611
  try {
612
+ if (!runProjectId || getCurrentProjectId() !== runProjectId) return false;
613
+ const proof = await writebackController.buildMirrorWritebackConfirmation?.(applied);
614
+ if (!proof || getCurrentProjectId() !== runProjectId) return false;
594
615
  const response = await sendBackgroundNative({
595
616
  method: 'mirror.confirmWriteback',
596
617
  params: {
597
618
  projectId: runProjectId,
598
- paths: Array.from(new Set(paths))
619
+ ...proof
599
620
  }
600
621
  });
601
- return response?.ok === true && response?.result?.ok === true;
622
+ const confirmations = Array.isArray(response?.result?.confirmed) ? response.result.confirmed : [];
623
+ const confirmed = new Map(confirmations.map(file => [file?.path, file?.hash]));
624
+ return getCurrentProjectId() === runProjectId && response?.ok === true && response?.result?.ok === true
625
+ && confirmations.length === proof.expectedFiles.length
626
+ && confirmed.size === proof.expectedFiles.length
627
+ && proof.expectedFiles.every(file => confirmed.get(file.path) === file.sha256);
602
628
  } catch (error) {
603
629
  return false;
604
630
  }
@@ -732,52 +758,45 @@
732
758
  let logResult = null;
733
759
  try {
734
760
  logResult = await callPageBridge('getCompileLog', {
761
+ captureId: result.compile?.captureId,
735
762
  triggerIfStale: false,
736
763
  maxAgeMs: 30000,
737
764
  waitForSaveMs: 0,
738
765
  runProjectId: options.runProjectId || getCurrentRunView()?.runProjectId || getCurrentProjectId()
739
766
  });
740
- } catch (_error) {
741
- logResult = null;
767
+ } catch (error) {
768
+ logResult = { ok: false, reason: error.message || 'compile_log_fetch_failed' };
742
769
  }
743
- const compile = result.compile;
770
+ const compile = compileAdapter.buildPostWriteCompileSummary({ result, logResult });
771
+ compile.errors = compile.errors.map(value => CompileAdapter.localizeCompileReason(value, getLocale()));
744
772
  if (compile?.status === 'success') {
745
773
  appendRunEvent({ title: tx('Compile succeeded.', '编译成功。'), status: 'completed',
746
774
  activity: activity('completed', [...(logResult?.errors || []), ...(logResult?.warnings || [])].slice(0, 5).map(CompileAdapter.formatCompileDiagnosticForSummary).join('\n')) });
747
775
  } else if (compile?.status === 'triggered') {
748
- appendRunEvent({ title: tx('Overleaf compile was triggered. The page will continue showing progress.', '已触发 Overleaf 编译;页面会继续显示编译进度。'), status: 'completed', activity: activity('triggered') });
776
+ appendRunEvent({ title: tx('Overleaf compile was triggered. The page will continue showing progress.', '已触发 Overleaf 编译;页面会继续显示编译进度。'), status: 'completed', activity: activity('triggered', compile.reason) });
749
777
  } else {
750
- appendRunEvent({ title: tx(`Compile finished with status: ${compile?.status || 'unknown'}`, `编译完成,状态:${compile?.status || '未知'}`), status: 'completed', activity: activity(compile?.status === 'failed' ? 'failed' : 'unknown') });
778
+ appendRunEvent({ title: compile.status === 'unconfirmed' ? tr('runCompileUnconfirmed')
779
+ : tx(`Compile finished with status: ${compile.status}`, `编译完成,状态:${compile.status}`),
780
+ status: 'completed', activity: activity(compile.status === 'failed' ? 'failed' : 'unknown', compile.errors.join('\n') || compile.reason) });
751
781
  }
752
- return buildPostWriteCompileSummary({ result, logResult });
782
+ return compile;
753
783
  } else {
754
- const reason = result?.reason || tx('unknown reason', '未知原因');
784
+ const reason = CompileAdapter.localizeCompileReason(result?.reason, getLocale()) || tx('unknown reason', '未知原因');
755
785
  appendRunEvent({ title: tx(`Post-write compile did not succeed: ${reason}`, `写后编译未成功:${reason}`), status: 'failed', activity: activity('failed', reason) });
756
- return buildPostWriteCompileSummary({ result });
786
+ return compileAdapter.buildPostWriteCompileSummary({ result });
757
787
  }
758
788
  } catch (error) {
759
- appendRunEvent({ title: tx(`Post-write compile failed: ${error.message}`, `写后编译出错:${error.message}`), status: 'failed', activity: activity('failed', error.message) });
760
- return buildPostWriteCompileSummary({ error });
789
+ const reason = CompileAdapter.localizeCompileReason(error.message, getLocale());
790
+ appendRunEvent({ title: tx(`Post-write compile failed: ${reason}`, `写后编译出错:${reason}`), status: 'failed', activity: activity('failed', reason) });
791
+ return compileAdapter.buildPostWriteCompileSummary({ error });
761
792
  }
762
793
  }
763
794
 
764
- function buildPostWriteCompileSummary(input = {}) {
765
- return compileAdapter.buildPostWriteCompileSummary(input);
766
- }
767
-
768
- // First structured per-operation failure among the skipped writeback
769
- // entries — the run-level failure the completion report promotes.
795
+ // Use the same normalized failure selection as settlement and its UI, while
796
+ // preserving the selected producer's full evidence on the historical report.
770
797
  function promoteWritebackSkippedFailure(applied) {
771
- for (const entry of getSkippedEntries(applied)) {
772
- const failure = entry?.result?.failure || entry?.failure;
773
- if (failure?.code) {
774
- if (!failure.file && entry?.path) {
775
- return { ...failure, file: entry.path };
776
- }
777
- return failure;
778
- }
779
- }
780
- return null;
798
+ const failures = writebackSettlement.collectFailuresFromResult(applied);
799
+ return writebackSettlement.selectPrimaryFailure(failures);
781
800
  }
782
801
 
783
802
  function appendCompileSummaryToConclusion(conclusion, compileSummary) {
@@ -806,10 +825,13 @@
806
825
  `已触发写后编译;最新日志显示 ${warnings.length} 个警告。`
807
826
  )
808
827
  : tx('Post-write compile was triggered; Overleaf may still be updating the result.', '已触发写后编译;Overleaf 可能仍在更新结果。');
828
+ } else if (compileSummary.status === 'unconfirmed') {
829
+ summary = tr('runCompileUnconfirmed');
809
830
  } else if (compileSummary.status === 'failed') {
831
+ const reason = compileAdapter.localizeCompileReason(compileSummary.reason, getLocale());
810
832
  summary = tx(
811
- `Post-write compile check failed: ${compileSummary.reason || 'unknown reason'}`,
812
- `写后编译检查失败:${compileSummary.reason || '未知原因'}`
833
+ `Post-write compile check failed: ${reason || 'unknown reason'}`,
834
+ `写后编译检查失败:${reason || '未知原因'}`
813
835
  );
814
836
  } else {
815
837
  summary = tx(
@@ -16,7 +16,18 @@
16
16
  let pickerOpen = false, query = '', filter = 'all';
17
17
  let activeGeneration = null, cancelling = false;
18
18
  let preview = null, previewBusy = false, previewFile = 'references/writing-style.md';
19
- const tx = (en, zh) => deps.tx(en, zh);
19
+ const ownMessagePairs = new Map();
20
+ const tx = (en, zh) => {
21
+ const pair = [en, zh];
22
+ ownMessagePairs.set(en, pair); ownMessagePairs.set(zh, pair);
23
+ return deps.tx(en, zh);
24
+ };
25
+ // Cached status text keeps its diagnostic value; only known copy is
26
+ // projected again when sync renders under a different language.
27
+ const displayMessage = value => {
28
+ const pair = ownMessagePairs.get(value);
29
+ return pair ? deps.tx(...pair) : value;
30
+ };
20
31
  const node = (tag, className, text) => {
21
32
  const element = document.createElement(tag);
22
33
  if (className) element.className = className;
@@ -62,6 +73,12 @@
62
73
  const value = deps.getScope();
63
74
  return value?.accountScopeId && value?.projectId ? value : null;
64
75
  }
76
+ function errorMessage(message, code) {
77
+ // codex.run currently wraps style-build error codes as codex_run_failed.
78
+ if (code === 'writing_style_busy' || message === 'Writing-style settings are being updated by another Native Host. Retry shortly.') return tx('Writing-style settings are being updated by another Native Host. Retry shortly.', '写作风格设置正在由另一个本地进程更新,请稍后重试。');
79
+ if (code === 'writing_style_changed' || message === 'The saved writing skill changed. Rebuild it in General settings.' || message === 'Writing-style asset integrity changed.') return tx(message || 'The saved writing skill changed. Rebuild it in General settings.', '写作风格资产不可用,请在常规设置中重新生成。');
80
+ return message;
81
+ }
65
82
  function alignScope() {
66
83
  const next = currentScope(), key = next ? next.accountScopeId + ':' + next.projectId : '';
67
84
  if (scopeKey !== key) {
@@ -83,7 +100,7 @@
83
100
  async function request(method, params = {}) {
84
101
  const response = await deps.request({ method, params });
85
102
  if (!response?.ok) {
86
- const error = new Error(response?.error?.message || tx('The request could not be completed.', '请求未能完成。'));
103
+ const error = new Error(errorMessage(response?.error?.message, response?.error?.code) || tx('The request could not be completed.', '请求未能完成。'));
87
104
  error.code = response?.error?.code || 'writing_style_failed'; throw error;
88
105
  }
89
106
  return response.result;
@@ -105,7 +122,7 @@
105
122
  meta = value;
106
123
  if (setupEnabled === null) desiredEnabled = value.enabled === true;
107
124
  if (!dirty && !activeGeneration) restoreSources(value);
108
- if (value.error && !message) { message = value.error; messageTone = 'error'; }
125
+ if (value.error && !message) { message = errorMessage(value.error, value.errorCode); messageTone = 'error'; }
109
126
  clearTimeout(pollTimer); pollTimer = null;
110
127
  if (value.building) pollTimer = setTimeout(() => { void ensureLoaded(true).catch(() => {}); }, 1800);
111
128
  else if (!activeGeneration) cancelling = false;
@@ -198,7 +215,8 @@
198
215
  activeGeneration = operation; cancelling = false; pickerOpen = false; preview = null;
199
216
  message = ''; messageTone = ''; expanded = true; render();
200
217
  const stillActive = () => {
201
- if (operation.cancelled || generation !== epoch) {
218
+ const current = currentScope();
219
+ if (operation.cancelled || generation !== epoch || current?.accountScopeId !== target.accountScopeId || current?.projectId !== target.projectId) {
202
220
  const error = new Error(tx('Style generation cancelled.', '已取消风格生成。'));
203
221
  error.code = 'codex_cancelled'; throw error;
204
222
  }
@@ -206,7 +224,7 @@
206
224
  try {
207
225
  const settings = await deps.getModelSettings();
208
226
  stillActive();
209
- const attachments = [], selected = [];
227
+ const attachments = [], selected = [], projectFiles = [];
210
228
  for (const source of chosen) {
211
229
  stillActive();
212
230
  if (source.kind === 'pdf') {
@@ -218,10 +236,11 @@
218
236
  const snapshot = await deps.getReferenceSnapshot(source.projectId, operation.readId);
219
237
  operation.readId = ''; stillActive();
220
238
  if (snapshot?.projectId !== source.projectId || snapshot?.capabilities?.referenceTextComplete !== true) {
221
- throw new Error(tx('The reference project could not be read completely: ', '参考项目未能完整读取:') + source.name);
239
+ throw new Error(tx('The reference project could not be read completely: ' + source.name, '参考项目未能完整读取:' + source.name));
222
240
  }
223
241
  const body = new Blob([JSON.stringify(snapshot)], { type: 'application/json' });
224
- if (body.size > LIMITS.maxProjectBytes) throw new Error(tx('Reference project is too large: ', '参考项目过大:') + source.name);
242
+ if (body.size > LIMITS.maxProjectBytes) throw new Error(tx('Reference project is too large: ' + source.name, '参考项目过大:' + source.name));
243
+ projectFiles.push(...(snapshot.files || []).map(file => ({ ...file, path: source.projectId + '/' + file.path })));
225
244
  const id = crypto.randomUUID();
226
245
  attachments.push({ id, name: source.projectId + '.writing-reference.json', size: body.size,
227
246
  mimeType: 'application/json', kind: 'file', contentBase64: await encode(body) });
@@ -229,6 +248,11 @@
229
248
  } else selected.push({ kind: 'saved', sourceId: source.sourceId });
230
249
  }
231
250
  stillActive();
251
+ if (projectFiles.length) {
252
+ const check = await deps.preflightProjectReferences({ project: { files: projectFiles }, assertCurrent: stillActive });
253
+ stillActive();
254
+ if (check.blocked) throw new Error(tx('Blocked: sensitive content review required', '已阻止:需要处理敏感内容'));
255
+ }
232
256
  operation.phase = 'learning'; operation.sourceName = ''; render();
233
257
  const tracked = await deps.startBuild({ ...settings, ...target, mode: 'ask',
234
258
  task: 'Generate the selected project writing skill and its section references.',
@@ -241,7 +265,7 @@
241
265
  const response = await tracked.promise;
242
266
  if (generation !== epoch) return;
243
267
  if (!response?.ok) {
244
- const error = new Error(response?.error?.message || tx('Style generation failed.', '风格生成失败。'));
268
+ const error = new Error(errorMessage(response?.error?.message, response?.error?.code) || tx('Style generation failed.', '风格生成失败。'));
245
269
  error.code = response?.error?.code; throw error;
246
270
  }
247
271
  const result = response.result?.writingStyle;
@@ -253,7 +277,7 @@
253
277
  } catch (error) {
254
278
  if (generation === epoch) {
255
279
  message = operation.cancelled || error.code === 'codex_cancelled'
256
- ? tx('Style generation cancelled.', '已取消风格生成。') : error.message;
280
+ ? tx('Style generation cancelled.', '已取消风格生成。') : errorMessage(error.message, error.code);
257
281
  messageTone = operation.cancelled || error.code === 'codex_cancelled' ? '' : 'error';
258
282
  }
259
283
  } finally {
@@ -340,7 +364,7 @@
340
364
  list.append(loading);
341
365
  } else if (projectError) {
342
366
  const error = node('div', 'ws2-picker-state ws2-picker-error');
343
- error.append(node('strong', '', tx('Projects could not be loaded', '项目列表未能加载')), node('p', '', projectError),
367
+ error.append(node('strong', '', tx('Projects could not be loaded', '项目列表未能加载')), node('p', '', displayMessage(projectError)),
344
368
  button(tx('Retry', '重试'), () => { void loadProjects(true); }, 'ws2-button', 'retry-projects'));
345
369
  list.append(error);
346
370
  } else {
@@ -540,7 +564,7 @@
540
564
  }
541
565
  }
542
566
  if (message || !meta) {
543
- const status = node('div', 'ws2-status', message || tx('Loading writing-style settings…', '正在加载写作风格设置…'));
567
+ const status = node('div', 'ws2-status', displayMessage(message) || tx('Loading writing-style settings…', '正在加载写作风格设置…'));
544
568
  status.dataset.tone = messageTone;
545
569
  status.setAttribute('role', messageTone === 'error' ? 'alert' : 'status');
546
570
  if (!meta && message) status.append(button(tx('Retry', '重试'), () => {