codex-overleaf-link 1.8.6 → 1.9.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 (73) hide show
  1. package/README.md +45 -21
  2. package/bin/codex-overleaf-link.mjs +58 -0
  3. package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
  4. package/extension/assets/icons/codex-overleaf-icon.png +0 -0
  5. package/extension/assets/icons/icon128.png +0 -0
  6. package/extension/assets/icons/icon16.png +0 -0
  7. package/extension/assets/icons/icon32.png +0 -0
  8. package/extension/assets/icons/icon48.png +0 -0
  9. package/extension/bootstrap/background.js +404 -0
  10. package/extension/bootstrap/manifest.template.json +51 -0
  11. package/extension/bootstrap/popup.css +26 -0
  12. package/extension/bootstrap/popup.html +38 -0
  13. package/extension/bootstrap/popup.js +44 -0
  14. package/extension/bootstrap/runtimeContext.js +2 -0
  15. package/extension/runtime-manifest.json +60 -0
  16. package/extension/src/background.js +532 -0
  17. package/extension/src/content/applyResultFormatters.js +387 -0
  18. package/extension/src/content/composerAttachments.js +522 -0
  19. package/extension/src/content/composerPanel.js +254 -0
  20. package/extension/src/content/contentRuntime.js +8897 -0
  21. package/extension/src/content/contextTray.js +686 -0
  22. package/extension/src/content/diagnosticsController.js +659 -0
  23. package/extension/src/content/diagnosticsPanel.js +342 -0
  24. package/extension/src/content/diffReviewPanel.js +797 -0
  25. package/extension/src/content/localSkillsPanel.js +280 -0
  26. package/extension/src/content/markdownText.js +606 -0
  27. package/extension/src/content/mirrorHealth.js +92 -0
  28. package/extension/src/content/modelPicker.js +489 -0
  29. package/extension/src/content/nativeChannel.js +62 -0
  30. package/extension/src/content/otWarmMirror.js +964 -0
  31. package/extension/src/content/otWarmMirrorController.js +174 -0
  32. package/extension/src/content/panelMaintenance.js +367 -0
  33. package/extension/src/content/panelRenderer.js +253 -0
  34. package/extension/src/content/recentProjects.js +1063 -0
  35. package/extension/src/content/reviewHunks.js +247 -0
  36. package/extension/src/content/runController.js +242 -0
  37. package/extension/src/content/runTimelineView.js +1131 -0
  38. package/extension/src/content/sessionManager.js +487 -0
  39. package/extension/src/content/sessionPanel.js +263 -0
  40. package/extension/src/content/settingsPanel.js +527 -0
  41. package/extension/src/content/themeController.js +91 -0
  42. package/extension/src/content/updateIdle.js +67 -0
  43. package/extension/src/content/writebackController.js +300 -0
  44. package/extension/src/content/writebackOrchestrator.js +850 -0
  45. package/extension/src/contentScript.js +8 -0
  46. package/extension/src/page/compileBridge.js +460 -0
  47. package/extension/src/page/overleafCapabilities.js +100 -0
  48. package/extension/src/page/overleafEditor.js +282 -0
  49. package/extension/src/page/overleafProjectSnapshot.js +234 -0
  50. package/extension/src/page/overleafRealtimeObserver.js +491 -0
  51. package/extension/src/page/pageBridgeCapability.js +71 -0
  52. package/extension/src/page/snapshotRouter.js +948 -0
  53. package/extension/src/page/trackedChangesLifecycle.js +1506 -0
  54. package/extension/src/page/treeOperations.js +1584 -0
  55. package/extension/src/page/writeGuard.js +142 -0
  56. package/extension/src/page/writebackRouter.js +1934 -0
  57. package/extension/src/pageBridge.js +2224 -0
  58. package/extension/src/popup.js +247 -0
  59. package/extension/src/shared/agentTranscript.js +18 -4
  60. package/extension/src/shared/compatibility.js +1 -1
  61. package/extension/styles/panel.css +4048 -0
  62. package/native-host/src/index.js +9 -2
  63. package/native-host/src/managedInstall.js +393 -0
  64. package/native-host/src/managedLauncherRuntime.js +71 -0
  65. package/native-host/src/nativeHostPlatform.js +16 -0
  66. package/native-host/src/runtimeInstaller.js +6 -1
  67. package/native-host/src/taskRunnerRuntime.js +8 -0
  68. package/native-host/src/updateArchive.js +253 -0
  69. package/native-host/src/updateManager.js +548 -0
  70. package/native-host/src/updateTrust.js +156 -0
  71. package/package.json +11 -2
  72. package/scripts/install-managed.mjs +64 -0
  73. package/scripts/uninstall-managed.mjs +48 -0
@@ -0,0 +1,850 @@
1
+ (function initCodexOverleafWritebackOrchestrator() {
2
+ 'use strict';
3
+
4
+ // Writeback orchestration carved out of contentRuntime.js (v1.6.3
5
+ // structural-debt phase 6): the sync-writeback pipeline
6
+ // (applySyncChangesToOverleaf), post-write save verification, mirror
7
+ // refresh, auto recompile, and the compile/unsupported-change summary
8
+ // helpers. Code moved verbatim; the only rewrites are the mutable runtime
9
+ // bindings (state -> getState(), currentRunView -> getCurrentRunView()).
10
+ //
11
+ // Shape note: unlike the earlier carves, the functions live at the IIFE
12
+ // top level (same 2-space indentation as before the move) with
13
+ // collaborators injected into module-scoped bindings by create(). This
14
+ // keeps the source-contract tests (which match exact indentation spans)
15
+ // intact.
16
+
17
+ let tr;
18
+ let tx;
19
+ let getLocale;
20
+ let appendRunEvent;
21
+ // In-flight background mirror refresh (v1.7.5) — see the writeback path.
22
+ let pendingMirrorRefresh = null;
23
+ let appendChangeSummary;
24
+ let appendCompletionReport;
25
+ let appendOperationsPreview;
26
+ let appendPartialWritebackWarning;
27
+ let appendApplyResult;
28
+ let renderDiffReview;
29
+ let renderReadOnlyDiffReview;
30
+ let showPluginConfirm;
31
+ let callPageBridge;
32
+ let sendBackgroundNative;
33
+ let getCurrentProjectId;
34
+ let resetContextProject;
35
+ let sanitizeRunProjectSnapshot;
36
+ let getAssistantAnswerForCurrentRun;
37
+ let cleanFinalAnswer;
38
+ let recordUndoFromApply;
39
+ let getSkippedEntries;
40
+ let getAppliedOperationPaths;
41
+ let hasApplyResultEntries;
42
+ let getAppliedSyncChanges;
43
+ let mergeApplyResultSkipped;
44
+ let hasSkippedApplyOperations;
45
+ let formatWritebackSkippedNextStep;
46
+ let formatOperationFiles;
47
+ let summarizeOperationForAudit;
48
+ let buildAuditSummaryFromApply;
49
+ let buildSyncApplyOperations;
50
+ let partitionUnsafeProjectPathOperations;
51
+ let evaluateGovernedOperations;
52
+ let buildGovernanceSkippedApplyResult;
53
+ let buildReviewingBlockedApplyResult;
54
+ let ensureReviewingBeforeWrite;
55
+ let confirmBinaryOperations;
56
+ let filterSyncChangesByOperations;
57
+ let writebackController;
58
+ let RUN_SNAPSHOT_ZIP_TIMEOUT_MS;
59
+ let getState;
60
+ let getCurrentRunView;
61
+
62
+ async function applySyncChangesToOverleaf(syncChanges = [], project = {}, options = {}) {
63
+ const runMode = options.mode || getState().mode;
64
+ const runRequireReviewing = typeof options.requireReviewing === 'boolean'
65
+ ? options.requireReviewing
66
+ : getState().requireReviewing === true;
67
+ const assistantMessage = cleanFinalAnswer(options.assistantMessage || getAssistantAnswerForCurrentRun());
68
+ const unsupportedChanges = Array.isArray(options.unsupportedChanges) ? options.unsupportedChanges : [];
69
+ appendUnsupportedLocalChanges(unsupportedChanges);
70
+ // Guard on the immutable submitted mode, not the mutable panel state.
71
+ if (options.mode === 'ask' && ((syncChanges || []).length || unsupportedChanges.length)) {
72
+ appendRunEvent({
73
+ title: tx(
74
+ 'Ask mode ignored local file changes. Overleaf was not modified.',
75
+ 'Ask 模式已忽略本地文件改动;Overleaf 未被修改。'
76
+ ),
77
+ status: 'warning'
78
+ });
79
+ appendCompletionReport({
80
+ conclusion: assistantMessage || tx(
81
+ 'Codex finished in Ask mode. Any local file changes were ignored and were not synced to Overleaf.',
82
+ 'Codex 已在 Ask 模式完成;本地文件改动已忽略,未同步到 Overleaf。'
83
+ ),
84
+ status: tr('modeAsk'),
85
+ operations: [],
86
+ applyResults: [],
87
+ mode: runMode,
88
+ nextStep: tx(
89
+ 'Switch to Suggest or Auto only when you want Codex to edit files.',
90
+ '只有希望 Codex 修改文件时,才需要切换到建议修改或自动写入。'
91
+ )
92
+ });
93
+ return {
94
+ summaryLine: assistantMessage || tx('Ask mode completed without Overleaf changes', 'Ask 模式已完成,未修改 Overleaf'),
95
+ hasSkippedOperations: false,
96
+ audit: buildAuditSummaryFromApply({
97
+ operations: [],
98
+ resultStatus: 'ask_ignored_local_changes',
99
+ blockedFiles: [
100
+ ...(syncChanges || []).map(change => ({ path: change.path, type: change.type, reason: 'ask_mode' })),
101
+ ...unsupportedChanges.map(change => ({ path: change.path, type: change.type || 'unsupported', reason: change.reason || 'ask_mode' }))
102
+ ]
103
+ })
104
+ };
105
+ }
106
+ let operations = buildSyncApplyOperations(syncChanges, project);
107
+ let visibleSyncChanges = syncChanges || [];
108
+ let additionalSkippedEntries = [];
109
+ let skippedFilesForAudit = additionalSkippedEntries;
110
+ let appliedFilesForAudit = [];
111
+ const pathSafety = partitionUnsafeProjectPathOperations(operations);
112
+ if (pathSafety.skipped.length) {
113
+ additionalSkippedEntries.push(...pathSafety.skipped);
114
+ operations = pathSafety.safe;
115
+ visibleSyncChanges = filterSyncChangesByOperations(syncChanges, operations);
116
+ }
117
+ const governed = evaluateGovernedOperations(operations);
118
+ if (governed.blocked.length) {
119
+ const governanceSkipped = buildGovernanceSkippedApplyResult(governed.blocked);
120
+ additionalSkippedEntries.push(...getSkippedEntries(governanceSkipped));
121
+ appendApplyResult(governanceSkipped);
122
+ appendRunEvent({
123
+ title: tx(
124
+ `Project governance blocked ${governed.blocked.length} write(s) before review.`,
125
+ `项目治理规则在审核前阻止了 ${governed.blocked.length} 项写入。`
126
+ ),
127
+ status: 'failed'
128
+ });
129
+ operations = governed.allowed;
130
+ visibleSyncChanges = filterSyncChangesByOperations(syncChanges, operations);
131
+ }
132
+ if (!operations.length) {
133
+ appendRunEvent({
134
+ title: tx('Codex did not produce file changes that need to sync back to Overleaf.', 'Codex 没有产生需要同步回 Overleaf 的文件改动。'),
135
+ status: 'completed'
136
+ });
137
+ appendCompletionReport({
138
+ conclusion: assistantMessage || tx('Codex finished locally. There are no changes to sync back to Overleaf.', 'Codex 已完成本地处理,没有需要同步回 Overleaf 的改动。'),
139
+ status: runMode === 'ask' ? tr('modeAsk') : 'completed',
140
+ operations: [],
141
+ applyResults: [],
142
+ unchangedReason: assistantMessage
143
+ ? tx('No file changes need to sync back to Overleaf.', '没有产生需要同步回 Overleaf 的文件改动。')
144
+ : formatUnsupportedLocalChangeSummary(unsupportedChanges),
145
+ mode: runMode,
146
+ nextStep: tx('You can continue the conversation, or adjust @context and run again.', '可以继续追问,或调整 @context 后重新运行。')
147
+ });
148
+ return {
149
+ summaryLine: assistantMessage || tx('No changes to sync', '没有需要同步的改动'),
150
+ hasSkippedOperations: additionalSkippedEntries.length > 0,
151
+ audit: buildAuditSummaryFromApply({
152
+ operations: buildSyncApplyOperations(syncChanges, project),
153
+ applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
154
+ blockedFiles: additionalSkippedEntries.map(item => summarizeOperationForAudit(item.operation, item.result, 'blocked')),
155
+ resultStatus: additionalSkippedEntries.length ? 'blocked' : 'completed'
156
+ })
157
+ };
158
+ }
159
+
160
+ if (runMode === 'confirm') {
161
+ appendRunEvent({
162
+ title: tx(
163
+ `Local Codex produced ${operations.length} change(s). Review the diff before applying.`,
164
+ `本地 Codex 产生了 ${operations.length} 项改动,请查看差异后确认。`
165
+ ),
166
+ status: 'running'
167
+ });
168
+ const accepted = await renderDiffReview(visibleSyncChanges);
169
+ if (!accepted.length) {
170
+ appendRunEvent({
171
+ title: tx('Sync cancelled: Overleaf was not modified.', '已取消同步:Overleaf 没有被修改。'),
172
+ status: 'completed'
173
+ });
174
+ appendCompletionReport({
175
+ conclusion: tx('You cancelled syncing. Local Codex changes were not written back to Overleaf.', '你取消了同步,本地 Codex 改动没有写回 Overleaf。'),
176
+ status: 'rejected',
177
+ operations: [],
178
+ applyResults: [],
179
+ mode: runMode,
180
+ nextStep: tx('Run the task again, or switch to Auto if you want changes synced directly.', '可以重新运行任务,或切换到自动写入后再同步。')
181
+ });
182
+ return {
183
+ summaryLine: tx('Sync cancelled', '已取消同步'),
184
+ hasSkippedOperations: additionalSkippedEntries.length > 0,
185
+ audit: buildAuditSummaryFromApply({
186
+ operations,
187
+ applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
188
+ blockedFiles: additionalSkippedEntries.map(item => summarizeOperationForAudit(item.operation, item.result, 'blocked')),
189
+ resultStatus: 'rejected'
190
+ })
191
+ };
192
+ }
193
+ operations = buildSyncApplyOperations(accepted, project);
194
+ const acceptedPathSafety = partitionUnsafeProjectPathOperations(operations);
195
+ if (acceptedPathSafety.skipped.length) {
196
+ additionalSkippedEntries.push(...acceptedPathSafety.skipped);
197
+ operations = acceptedPathSafety.safe;
198
+ }
199
+ }
200
+
201
+ const deleteOperations = operations.filter(operation => operation.type === 'delete');
202
+ if (deleteOperations.length) {
203
+ const approved = await showPluginConfirm({
204
+ title: tr('deleteFilePromptTitle'),
205
+ message: tr('deleteFilePromptMessage', { files: formatOperationFiles(deleteOperations) }),
206
+ confirmLabel: tr('deleteFileConfirm'),
207
+ cancelLabel: tr('deleteFileCancel'),
208
+ destructive: true
209
+ });
210
+ if (!approved) {
211
+ additionalSkippedEntries.push(...deleteOperations.map(operation => ({
212
+ operation,
213
+ result: {
214
+ ok: false,
215
+ code: 'delete_confirmation_rejected',
216
+ reason: tx('Delete requires explicit confirmation and was skipped.', '删除需要显式确认,已跳过。')
217
+ }
218
+ })));
219
+ operations = operations.filter(operation => operation.type !== 'delete');
220
+ }
221
+ }
222
+
223
+ if (operations.some(operation => operation.type === 'binary-create' || operation.type === 'overwrite-binary')) {
224
+ appendRunEvent({
225
+ title: tx('Generated binary asset writeback requires explicit confirmation.', '生成的二进制资源写回需要显式确认。'),
226
+ status: 'running'
227
+ });
228
+ }
229
+ const binaryDecision = await confirmBinaryOperations(operations);
230
+ operations = binaryDecision.operations;
231
+ additionalSkippedEntries.push(...binaryDecision.skipped);
232
+
233
+ if (!operations.length) {
234
+ appendRunEvent({
235
+ title: tx('No approved file changes remain to sync back to Overleaf.', '没有剩余已确认的文件改动需要同步回 Overleaf。'),
236
+ status: 'completed'
237
+ });
238
+ appendCompletionReport({
239
+ conclusion: assistantMessage || tx('No approved changes were written back to Overleaf.', '没有已确认的改动写回 Overleaf。'),
240
+ status: runMode === 'ask' ? tr('modeAsk') : 'completed',
241
+ operations: [],
242
+ applyResults: [],
243
+ unchangedReason: tx('No approved file changes remain to sync back to Overleaf.', '没有剩余已确认的文件改动需要同步回 Overleaf。'),
244
+ mode: runMode,
245
+ nextStep: tx('You can continue the conversation, or adjust @context and run again.', '可以继续追问,或调整 @context 后重新运行。')
246
+ });
247
+ return {
248
+ summaryLine: tx('No changes applied', '没有应用改动'),
249
+ hasSkippedOperations: additionalSkippedEntries.length > 0,
250
+ audit: buildAuditSummaryFromApply({
251
+ operations: buildSyncApplyOperations(syncChanges, project),
252
+ applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
253
+ blockedFiles: additionalSkippedEntries
254
+ .filter(item => item.result?.code === 'governance_blocked')
255
+ .map(item => summarizeOperationForAudit(item.operation, item.result, 'blocked')),
256
+ resultStatus: additionalSkippedEntries.length ? 'completed_with_skips' : 'completed'
257
+ })
258
+ };
259
+ }
260
+
261
+ appendOperationsPreview(operations, tx('Sync local Codex changes to Overleaf', '同步本地 Codex 改动到 Overleaf'));
262
+ const reviewing = await ensureReviewingBeforeWrite(operations, { requireReviewing: runRequireReviewing });
263
+ if (!reviewing.ok) {
264
+ const blocked = buildReviewingBlockedApplyResult(operations, reviewing);
265
+ appendApplyResult(blocked);
266
+ appendCompletionReport({
267
+ conclusion: tx(
268
+ 'No files were written: Track is enabled, but Codex could not verify Overleaf Reviewing/Track Changes.',
269
+ '这轮没有写入:已开启“留痕”要求,但 Codex 没能确认 Overleaf 正在用 Reviewing/Track Changes。'
270
+ ),
271
+ status: 'failed',
272
+ operations,
273
+ applyResults: [blocked],
274
+ mode: runMode,
275
+ nextStep: tx(
276
+ 'Switch Overleaf to Reviewing/Track Changes manually and rerun, or turn off Track before writing.',
277
+ '请在 Overleaf 手动切到 Reviewing/Track Changes 后重新运行,或关闭“留痕”再写入。'
278
+ )
279
+ });
280
+ return {
281
+ summaryLine: tx(
282
+ 'Write blocked: Overleaf Reviewing/Track Changes was not verified',
283
+ '已阻止写入:未确认 Overleaf Reviewing/Track Changes'
284
+ ),
285
+ hasSkippedOperations: true,
286
+ audit: buildAuditSummaryFromApply({
287
+ operations,
288
+ applyResults: [blocked],
289
+ blockedFiles: additionalSkippedEntries
290
+ .filter(item => item.result?.code === 'governance_blocked')
291
+ .map(item => summarizeOperationForAudit(item.operation, item.result, 'blocked')),
292
+ resultStatus: 'blocked'
293
+ })
294
+ };
295
+ }
296
+ const applied = operations.length
297
+ ? mergeApplyResultSkipped(await callPageBridge('applyOperations', {
298
+ operations,
299
+ baseFiles: project?.files || [],
300
+ requireReviewing: runRequireReviewing,
301
+ requireEditing: !runRequireReviewing,
302
+ // Welcome-panel + write-guard:
303
+ // immutable per-run project id, checked by the page-side guard
304
+ // against `_ide.project._id` before any mutation.
305
+ runProjectId: getCurrentRunView()?.runProjectId || ''
306
+ }), additionalSkippedEntries)
307
+ : mergeApplyResultSkipped({ ok: true, applied: [], skipped: [] }, additionalSkippedEntries);
308
+ const hasConfirmedApplyResult = hasApplyResultEntries(applied);
309
+ // Record the apply result + undo checkpoint IMMEDIATELY, before any
310
+ // cancellable verify/mirror awaits below. A user-cancel during the
311
+ // post-write save-verify or mirror refresh throws codex_cancelled; if undo
312
+ // recording sat after those awaits (as it used to), a cancel skipped it and
313
+ // left the user no "Undo written parts" button for changes that already
314
+ // landed in Overleaf. The manual-confirm path already records undo right
315
+ // after its write — match that ordering here (v1.6.2).
316
+ appendApplyResult(applied);
317
+ recordUndoFromApply(project, applied);
318
+ const skippedEntries = getSkippedEntries(applied);
319
+ if (skippedEntries.length) {
320
+ appendPartialWritebackWarning(applied);
321
+ }
322
+ if (runMode === 'auto') {
323
+ renderReadOnlyDiffReview(getAppliedSyncChanges(syncChanges, applied));
324
+ }
325
+ const appliedPaths = getAppliedOperationPaths(applied);
326
+ // Only probe save-state + refresh the mirror when real writes landed. A
327
+ // zero-write run (every operation skipped) has nothing to save-verify, so
328
+ // skip the ~5s probe and its misleading "could not verify saved" warning
329
+ // (v1.6.2).
330
+ const saveVerification = appliedPaths.length
331
+ ? await verifyPostWriteSaveState()
332
+ : {
333
+ ok: false,
334
+ state: 'not_checked',
335
+ reason: 'No applied writeback operations were returned.'
336
+ };
337
+ if (appliedPaths.length) {
338
+ appendPostWriteSaveVerificationWarning(saveVerification);
339
+ }
340
+ // v1.7.5: the mirror refresh (zip snapshot + mirror.sync, often the
341
+ // slowest post-write step) no longer blocks the completion report. The
342
+ // pending promise is exposed so undoRun and the next runTask barrier on
343
+ // it — a mirror.sync landing AFTER an undo would otherwise push the
344
+ // pre-undo snapshot as the local baseline.
345
+ pendingMirrorRefresh = refreshProjectMirrorAfterWriteback(project, applied, saveVerification)
346
+ .catch(error => {
347
+ appendRunEvent({
348
+ title: tx(
349
+ `Overleaf was written, but the background workspace refresh failed: ${error.message}. The next run will reread the project.`,
350
+ `Overleaf 已写入,但后台刷新本地 workspace 失败:${error.message}。下一轮会重新读取项目。`
351
+ ),
352
+ status: 'failed'
353
+ });
354
+ })
355
+ .finally(() => {
356
+ pendingMirrorRefresh = null;
357
+ });
358
+ const compileSummary = appliedPaths.length
359
+ ? await autoRecompileAfterWriteback(appliedPaths, saveVerification).catch(error => {
360
+ appendRunEvent({
361
+ title: tx(`Post-write compile failed: ${error.message}`, `写后编译出错:${error.message}`),
362
+ status: 'failed'
363
+ });
364
+ return buildPostWriteCompileSummary({ error });
365
+ })
366
+ : null;
367
+ const hasSkippedApplyResult = skippedEntries.length > 0;
368
+ const writebackIncomplete = !hasConfirmedApplyResult || hasSkippedApplyResult;
369
+ const summaryLine = appendChangeSummary({
370
+ notes: hasConfirmedApplyResult
371
+ ? tx('Local Codex changes were synced back to Overleaf.', '本地 Codex 改动已同步回 Overleaf。')
372
+ : tx('Local Codex changes were sent to Overleaf, but Codex could not confirm the write result.', '本地 Codex 改动已发送到 Overleaf,但 Codex 没能确认写入结果。'),
373
+ operations,
374
+ applyResults: [applied],
375
+ status: writebackIncomplete ? 'writeback incomplete' : 'synced from local Codex workspace'
376
+ });
377
+ const syncedConclusion = assistantMessage || tx('Local Codex changes were synced back to Overleaf.', '本地 Codex 改动已同步回 Overleaf。');
378
+ const partialSyncConclusion = assistantMessage
379
+ ? `${assistantMessage}\n\n${tx('Sync note: local Codex changes were sent to Overleaf, but some items were skipped.', '同步提示:本地 Codex 改动已尝试写回 Overleaf,但有部分项目被跳过。')}`
380
+ : tx('Local Codex changes were sent to Overleaf, but some items were skipped.', '本地 Codex 改动已尝试同步回 Overleaf,但有部分项目被跳过。');
381
+ const unconfirmedSyncConclusion = assistantMessage
382
+ ? `${assistantMessage}\n\n${tx('Writeback note: Codex could not confirm any Overleaf write result entries, so local mirror refresh and auto compile were skipped.', '写入提示:Codex 没能确认任何 Overleaf 写入结果条目,因此已跳过本地 mirror 刷新和自动编译。')}`
383
+ : tx('Codex tried to write local changes to Overleaf, but the write result did not confirm any applied or skipped entries.', 'Codex 已尝试把本地改动写入 Overleaf,但写入结果没有确认任何已写入或已跳过条目。');
384
+ const writebackConclusion = !hasConfirmedApplyResult
385
+ ? unconfirmedSyncConclusion
386
+ : hasSkippedApplyResult
387
+ ? partialSyncConclusion
388
+ : syncedConclusion;
389
+ const writebackNextStep = !hasConfirmedApplyResult
390
+ ? tx('Check Overleaf manually before running again. Local mirror refresh and auto compile were skipped.', '再次运行前请先手动检查 Overleaf。本地 mirror 刷新和自动编译已跳过。')
391
+ : hasSkippedApplyResult
392
+ ? formatWritebackSkippedNextStep(applied)
393
+ : tx('Review the synced file in Overleaf.', '请在 Overleaf 中查看同步后的文件。');
394
+ appendCompletionReport({
395
+ conclusion: appendCompileSummaryToConclusion(writebackConclusion, compileSummary),
396
+ status: writebackIncomplete ? 'failed' : 'completed',
397
+ operations,
398
+ applyResults: [applied],
399
+ mode: runMode,
400
+ nextStep: writebackNextStep,
401
+ // Structured compile errors give the report a one-click fix action.
402
+ compileErrors: compileSummary?.status === 'failed' && Array.isArray(compileSummary.errors)
403
+ ? compileSummary.errors
404
+ : undefined,
405
+ // Promote the primary per-operation skip failure to the run level when
406
+ // the writeback failed outright: target-file and write-stage codes only
407
+ // ever exist inside applied.skipped[], and without this promotion the
408
+ // recovery registry's "Open <file>" / "Edit & resend" branches are
409
+ // unreachable for them (fleet finding, v1.7.5).
410
+ failure: writebackIncomplete ? promoteWritebackSkippedFailure(applied) : undefined
411
+ });
412
+
413
+ return {
414
+ summaryLine,
415
+ hasSkippedOperations: writebackIncomplete || hasSkippedApplyOperations([applied]),
416
+ // Welcome-panel + write-guard: expose the raw applied
417
+ // result so the post-navigation settlement (spec §5.7.1) can classify
418
+ // the run by inspecting the `applied.skipped` entries' failure codes.
419
+ // Existing call sites that only read `summaryLine` / `hasSkippedOperations`
420
+ // / `audit` are unaffected.
421
+ applied,
422
+ audit: buildAuditSummaryFromApply({
423
+ operations,
424
+ applyResults: [applied],
425
+ blockedFiles: additionalSkippedEntries
426
+ .filter(item => item.result?.code === 'governance_blocked')
427
+ .map(item => summarizeOperationForAudit(item.operation, item.result, 'blocked')),
428
+ resultStatus: writebackIncomplete ? 'completed_with_skips' : 'completed',
429
+ saveVerification
430
+ })
431
+ };
432
+ }
433
+
434
+ async function verifyPostWriteSaveState() {
435
+ try {
436
+ const result = await callPageBridge('waitForSaveState', {
437
+ deadlineMs: 5000,
438
+ requirePositiveSignal: true
439
+ });
440
+ if (result?.state === 'verified_saved') {
441
+ return {
442
+ ...result,
443
+ ok: true,
444
+ state: 'verified_saved'
445
+ };
446
+ }
447
+ if (result?.ok === true && !result?.state) {
448
+ return {
449
+ ...result,
450
+ ok: true,
451
+ state: 'verified_saved'
452
+ };
453
+ }
454
+ if (result?.state === 'unknown_timeout' || result?.state === 'unavailable') {
455
+ return {
456
+ ...result,
457
+ ok: false
458
+ };
459
+ }
460
+ return {
461
+ ...result,
462
+ ok: false,
463
+ state: 'unknown_timeout'
464
+ };
465
+ } catch (error) {
466
+ return {
467
+ ok: false,
468
+ state: 'unavailable',
469
+ reason: error.message
470
+ };
471
+ }
472
+ }
473
+
474
+ function appendPostWriteSaveVerificationWarning(saveVerification = {}) {
475
+ if (saveVerification?.state === 'verified_saved') {
476
+ return;
477
+ }
478
+ appendRunEvent({
479
+ title: tx(
480
+ 'Files were written or attempted in Overleaf, but Codex could not verify that Overleaf saved them. Local mirror refresh was skipped.',
481
+ '文件已写入或尝试写入 Overleaf,但 Codex 未能确认 Overleaf 已保存;已跳过本地 mirror 刷新。'
482
+ ),
483
+ status: 'warning'
484
+ });
485
+ }
486
+
487
+ // v1.8.0: text-only writebacks confirm the mirror in place. The written
488
+ // content ORIGINATED in the local workspace, so after a verified writeback
489
+ // the workspace already equals Overleaf; only the baseline hashes and
490
+ // freshness metadata are stale. mirror.confirmWriteback re-hashes the
491
+ // written files without re-downloading the project. Anything unusual —
492
+ // tree ops, binary writes, dirty mirror, an older native host without the
493
+ // method — falls back to the full zip resync below.
494
+ const MIRROR_CONFIRMABLE_OPERATION_TYPES = new Set(['edit']);
495
+
496
+ async function tryConfirmMirrorWriteback(applied) {
497
+ const entries = Array.isArray(applied?.applied) ? applied.applied : [];
498
+ if (!entries.length) {
499
+ return false;
500
+ }
501
+ const paths = [];
502
+ for (const entry of entries) {
503
+ const operation = entry?.operation || {};
504
+ if (!MIRROR_CONFIRMABLE_OPERATION_TYPES.has(operation.type) || !operation.path) {
505
+ return false;
506
+ }
507
+ paths.push(operation.path);
508
+ }
509
+ try {
510
+ const response = await sendBackgroundNative({
511
+ method: 'mirror.confirmWriteback',
512
+ params: {
513
+ projectId: getCurrentProjectId(),
514
+ paths: Array.from(new Set(paths))
515
+ }
516
+ });
517
+ return response?.ok === true && response?.result?.ok === true;
518
+ } catch (error) {
519
+ return false;
520
+ }
521
+ }
522
+
523
+ async function refreshProjectMirrorAfterWriteback(project = {}, applied = {}, saveVerification = {}) {
524
+ if (saveVerification?.state !== 'verified_saved') {
525
+ return;
526
+ }
527
+ if (!Array.isArray(applied?.applied) || !applied.applied.length) {
528
+ return;
529
+ }
530
+
531
+ if (await tryConfirmMirrorWriteback(applied)) {
532
+ appendRunEvent({
533
+ title: tx(
534
+ 'Local Codex workspace confirmed in place (no project re-download needed). The next run starts from the latest content.',
535
+ '本地 Codex workspace 已就地确认(无需重新下载项目),下一轮将从最新内容开始。'
536
+ ),
537
+ status: 'completed'
538
+ });
539
+ return;
540
+ }
541
+
542
+ appendRunEvent({
543
+ title: tx('Checking latest Overleaf content and refreshing the local Codex workspace.', '正在确认 Overleaf 最新内容,并刷新本地 Codex workspace。'),
544
+ status: 'running'
545
+ });
546
+
547
+ await callPageBridge('invalidateProjectSnapshot', {});
548
+ const freshProject = sanitizeRunProjectSnapshot(await callPageBridge('getProjectSnapshot', {
549
+ force: true,
550
+ maxAgeMs: 0,
551
+ preferLightweight: true,
552
+ allowZipFallback: true,
553
+ allowEditorNavigation: false,
554
+ requireFullProject: true,
555
+ includeBinaryFiles: true,
556
+ zipOnly: true,
557
+ zipTimeoutMs: RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
558
+ focusFiles: getAppliedOperationPaths(applied)
559
+ }));
560
+ if (!freshProject?.files?.length || freshProject?.capabilities?.fullProjectSnapshot === false) {
561
+ appendRunEvent({
562
+ title: tx(
563
+ 'Overleaf was written, but the full project was not read. The local Codex workspace baseline was not refreshed; the next run will reread it.',
564
+ 'Overleaf 已写入,但没有读到完整项目;暂不刷新本地 Codex workspace baseline,下一轮会重新读取。'
565
+ ),
566
+ status: 'failed'
567
+ });
568
+ return;
569
+ }
570
+
571
+ const syncedProject = mergeVerifiedAppliedFiles(freshProject, project, applied);
572
+ resetContextProject();
573
+ const response = await sendBackgroundNative({
574
+ method: 'mirror.sync',
575
+ params: {
576
+ projectId: getCurrentProjectId(),
577
+ project: syncedProject
578
+ }
579
+ });
580
+ if (response?.ok) {
581
+ appendRunEvent({
582
+ title: tx('Local Codex workspace refreshed. The next run will start from the latest Overleaf content.', '已刷新本地 Codex workspace,下一轮会从最新 Overleaf 内容开始。'),
583
+ status: 'completed'
584
+ });
585
+ return;
586
+ }
587
+
588
+ const message = response?.error?.message || tx('native host did not respond', 'native host 没有响应');
589
+ appendRunEvent({
590
+ title: tx(
591
+ `Overleaf was written, but refreshing the local workspace failed: ${message}`,
592
+ `Overleaf 已写入,但刷新本地 workspace 失败:${message}`
593
+ ),
594
+ status: 'failed'
595
+ });
596
+ }
597
+
598
+ function mergeVerifiedAppliedFiles(freshProject = {}, originalProject = {}, applied = {}) {
599
+ return writebackController.mergeVerifiedAppliedFiles(freshProject, originalProject, applied);
600
+ }
601
+
602
+ function appendUnsupportedLocalChanges(changes = []) {
603
+ if (!changes.length) {
604
+ return;
605
+ }
606
+ appendRunEvent({
607
+ title: formatUnsupportedLocalChangeSummary(changes),
608
+ status: 'completed'
609
+ });
610
+ }
611
+
612
+ function formatUnsupportedLocalChangeSummary(changes = []) {
613
+ return writebackController.formatUnsupportedLocalChangeSummary(changes, getLocale());
614
+ }
615
+
616
+ async function autoRecompileAfterWriteback(writtenPaths = [], saveVerification = {}) {
617
+ if (getState().autoRecompile === false) return null;
618
+ if (getState().mode === 'ask') return null;
619
+
620
+ const CompileAdapter = window.CodexOverleafCompileAdapter;
621
+ if (!CompileAdapter) return null;
622
+
623
+ const hasCompilableFile = writtenPaths.some(filePath => CompileAdapter.isCompilableFile(filePath));
624
+ if (!hasCompilableFile) return null;
625
+
626
+ appendRunEvent({
627
+ title: saveVerification?.state !== 'verified_saved'
628
+ ? tx(
629
+ 'Post-write compile: Overleaf save was not verified, but Auto Compile is on. Triggering Overleaf Recompile and letting Overleaf wait for the latest save.',
630
+ '正在写后编译:尚未确认 Overleaf 已保存,但已开启自动编译;将触发 Overleaf Recompile,并由 Overleaf 等待最新保存。'
631
+ )
632
+ : tx(
633
+ 'Post-write compile: LaTeX files were written, triggering Overleaf Recompile.',
634
+ '正在写后编译:已写入 LaTeX 文件,正在触发 Overleaf Recompile。'
635
+ ),
636
+ status: 'running'
637
+ });
638
+
639
+ try {
640
+ const result = await callPageBridge('triggerCompile', {
641
+ preferUiClick: true,
642
+ waitForSaveMs: 5000,
643
+ requireVerifiedSave: saveVerification?.state === 'verified_saved'
644
+ });
645
+ if (result?.ok) {
646
+ let logResult = null;
647
+ try {
648
+ logResult = await callPageBridge('getCompileLog', {
649
+ triggerIfStale: false,
650
+ maxAgeMs: 30000,
651
+ waitForSaveMs: 0
652
+ });
653
+ } catch (_error) {
654
+ logResult = null;
655
+ }
656
+ const compile = result.compile;
657
+ if (compile?.status === 'success') {
658
+ appendRunEvent({ title: tx('Compile succeeded.', '编译成功。'), status: 'completed' });
659
+ } else if (compile?.status === 'triggered') {
660
+ appendRunEvent({ title: tx('Overleaf compile was triggered. The page will continue showing progress.', '已触发 Overleaf 编译;页面会继续显示编译进度。'), status: 'completed' });
661
+ } else {
662
+ appendRunEvent({ title: tx(`Compile finished with status: ${compile?.status || 'unknown'}`, `编译完成,状态:${compile?.status || '未知'}`), status: 'completed' });
663
+ }
664
+ return buildPostWriteCompileSummary({ result, logResult });
665
+ } else {
666
+ const reason = result?.reason || tx('unknown reason', '未知原因');
667
+ appendRunEvent({ title: tx(`Post-write compile did not succeed: ${reason}`, `写后编译未成功:${reason}`), status: 'failed' });
668
+ return buildPostWriteCompileSummary({ result });
669
+ }
670
+ } catch (error) {
671
+ appendRunEvent({ title: tx(`Post-write compile failed: ${error.message}`, `写后编译出错:${error.message}`), status: 'failed' });
672
+ return buildPostWriteCompileSummary({ error });
673
+ }
674
+ }
675
+
676
+ function buildPostWriteCompileSummary({ result = null, logResult = null, error = null } = {}) {
677
+ const logAvailable = logResult?.ok === true;
678
+ const errors = logAvailable && Array.isArray(logResult.errors)
679
+ ? logResult.errors.slice(0, 5).map(formatCompileDiagnosticForSummary)
680
+ : [];
681
+ const warnings = logAvailable && Array.isArray(logResult.warnings)
682
+ ? logResult.warnings.slice(0, 5).map(formatCompileDiagnosticForSummary)
683
+ : [];
684
+ if (error) {
685
+ return {
686
+ status: 'failed',
687
+ reason: error?.message || 'Post-write compile failed.',
688
+ errors,
689
+ warnings,
690
+ logAvailable
691
+ };
692
+ }
693
+ if (!result?.ok) {
694
+ return {
695
+ status: 'failed',
696
+ reason: result?.reason || 'Post-write compile did not succeed.',
697
+ errors,
698
+ warnings,
699
+ logAvailable
700
+ };
701
+ }
702
+ return {
703
+ status: result.compile?.status || 'triggered',
704
+ reason: result.reason || '',
705
+ errors,
706
+ warnings,
707
+ logAvailable
708
+ };
709
+ }
710
+
711
+ // First structured per-operation failure among the skipped writeback
712
+ // entries — the run-level failure the completion report promotes.
713
+ function promoteWritebackSkippedFailure(applied) {
714
+ for (const entry of getSkippedEntries(applied)) {
715
+ const failure = entry?.result?.failure || entry?.failure;
716
+ if (failure?.code) {
717
+ if (!failure.file && entry?.path) {
718
+ return { ...failure, file: entry.path };
719
+ }
720
+ return failure;
721
+ }
722
+ }
723
+ return null;
724
+ }
725
+
726
+ function appendCompileSummaryToConclusion(conclusion, compileSummary) {
727
+ if (!compileSummary) {
728
+ return conclusion;
729
+ }
730
+ const errors = compileSummary.errors || [];
731
+ const warnings = compileSummary.warnings || [];
732
+ let summary;
733
+ if (errors.length) {
734
+ summary = tx(
735
+ `Post-write compile check: ${errors.length} remaining error(s): ${errors.slice(0, 3).join('; ')}`,
736
+ `写后编译检查:仍有 ${errors.length} 个错误:${errors.slice(0, 3).join(';')}`
737
+ );
738
+ } else if (compileSummary.status === 'success') {
739
+ summary = warnings.length
740
+ ? tx(
741
+ `Post-write compile check succeeded with ${warnings.length} warning(s).`,
742
+ `写后编译检查已通过,但仍有 ${warnings.length} 个警告。`
743
+ )
744
+ : tx('Post-write compile check succeeded with no reported errors.', '写后编译检查已通过,未发现错误。');
745
+ } else if (compileSummary.status === 'triggered') {
746
+ summary = warnings.length
747
+ ? tx(
748
+ `Post-write compile was triggered; latest log shows ${warnings.length} warning(s).`,
749
+ `已触发写后编译;最新日志显示 ${warnings.length} 个警告。`
750
+ )
751
+ : tx('Post-write compile was triggered; Overleaf may still be updating the result.', '已触发写后编译;Overleaf 可能仍在更新结果。');
752
+ } else if (compileSummary.status === 'failed') {
753
+ summary = tx(
754
+ `Post-write compile check failed: ${compileSummary.reason || 'unknown reason'}`,
755
+ `写后编译检查失败:${compileSummary.reason || '未知原因'}`
756
+ );
757
+ } else {
758
+ summary = tx(
759
+ `Post-write compile finished with status: ${compileSummary.status || 'unknown'}.`,
760
+ `写后编译完成,状态:${compileSummary.status || '未知'}。`
761
+ );
762
+ }
763
+ return [conclusion, '', summary].filter(Boolean).join('\n');
764
+ }
765
+
766
+ function formatCompileDiagnosticForSummary(value) {
767
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
768
+ return text.length > 180 ? `${text.slice(0, 179)}...` : text;
769
+ }
770
+
771
+ async function resolveCompileLogContext() {
772
+ try {
773
+ const result = await callPageBridge('getCompileLog', {
774
+ triggerIfStale: true,
775
+ maxAgeMs: 30000,
776
+ waitForSaveMs: 5000
777
+ });
778
+
779
+ if (!result?.ok) {
780
+ return { type: 'compile-log', available: false, reason: result?.reason || 'Could not get compile log' };
781
+ }
782
+
783
+ return {
784
+ type: 'compile-log',
785
+ available: true,
786
+ log: result.log,
787
+ errors: result.errors || [],
788
+ warnings: result.warnings || [],
789
+ compiledAt: result.compiledAt,
790
+ fresh: result.fresh
791
+ };
792
+ } catch (error) {
793
+ return { type: 'compile-log', available: false, reason: error.message };
794
+ }
795
+ }
796
+
797
+ function create(deps = {}) {
798
+ ({
799
+ tr,
800
+ tx,
801
+ getLocale,
802
+ appendRunEvent,
803
+ appendChangeSummary,
804
+ appendCompletionReport,
805
+ appendOperationsPreview,
806
+ appendPartialWritebackWarning,
807
+ appendApplyResult,
808
+ renderDiffReview,
809
+ renderReadOnlyDiffReview,
810
+ showPluginConfirm,
811
+ callPageBridge,
812
+ sendBackgroundNative,
813
+ getCurrentProjectId,
814
+ resetContextProject,
815
+ sanitizeRunProjectSnapshot,
816
+ getAssistantAnswerForCurrentRun,
817
+ cleanFinalAnswer,
818
+ recordUndoFromApply,
819
+ getSkippedEntries,
820
+ getAppliedOperationPaths,
821
+ hasApplyResultEntries,
822
+ getAppliedSyncChanges,
823
+ mergeApplyResultSkipped,
824
+ hasSkippedApplyOperations,
825
+ formatWritebackSkippedNextStep,
826
+ formatOperationFiles,
827
+ summarizeOperationForAudit,
828
+ buildAuditSummaryFromApply,
829
+ buildSyncApplyOperations,
830
+ partitionUnsafeProjectPathOperations,
831
+ evaluateGovernedOperations,
832
+ buildGovernanceSkippedApplyResult,
833
+ buildReviewingBlockedApplyResult,
834
+ ensureReviewingBeforeWrite,
835
+ confirmBinaryOperations,
836
+ filterSyncChangesByOperations,
837
+ writebackController,
838
+ RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
839
+ getState,
840
+ getCurrentRunView,
841
+ } = deps);
842
+ return {
843
+ applySyncChangesToOverleaf,
844
+ resolveCompileLogContext,
845
+ getPendingMirrorRefresh: () => pendingMirrorRefresh
846
+ };
847
+ }
848
+
849
+ window.CodexOverleafWritebackOrchestrator = { create };
850
+ })();