codex-overleaf-link 2.5.0 → 2.5.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 (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  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 +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  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 +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -0,0 +1,58 @@
1
+ (function installUpdateSafetyReceiver(root) {
2
+ 'use strict';
3
+ if (root.__codexOverleafUpdateSafetyReceiverInstalled) return;
4
+ let binding, idle, inFlight = false;
5
+ const blocked = () => ({ idle: false, saved: false, blockers: ['tab_probe_unavailable'] });
6
+ function install(next, dependencies) {
7
+ if (!next?.transactionId || !next.token || next.url !== location.href) return { ok: false };
8
+ if (binding) {
9
+ if (binding.transactionId !== next.transactionId || binding.targetVersion !== next.targetVersion
10
+ || binding.documentId !== next.documentId || binding.url !== next.url || inFlight) return { ok: false };
11
+ binding = Object.freeze({ ...next });
12
+ return { ok: true, url: location.href };
13
+ }
14
+ binding = Object.freeze({ ...next });
15
+ const pageWrites = new Set();
16
+ const visible = node => !node.hidden && node.getAttribute('aria-hidden') !== 'true' && node.getClientRects().length > 0;
17
+ const { UpdateIdle, PageRpcContract } = dependencies;
18
+ idle = UpdateIdle.create({ document, includePanelActivity: true,
19
+ getBusyState: () => ({ reviewAction: pageWrites.size > 0
20
+ || [...document.querySelectorAll('#codex-overleaf-panel [data-run-accept-confirm], #codex-overleaf-panel button:disabled')]
21
+ .some(node => visible(node) && (node.hasAttribute('data-run-accept-confirm') || /Accepting|接受中/.test(node.textContent)
22
+ || (node.hasAttribute('data-run-undo') && !/Undone|已撤销/.test(node.textContent)
23
+ && !/Accepted|已接受/.test(node.closest('[data-run-id]')?.querySelector('[data-run-accept]')?.textContent || '')))),
24
+ run: document.querySelector('#codex-overleaf-panel')?.dataset.running === 'true',
25
+ cancelling: document.querySelector('#codex-overleaf-panel')?.dataset.cancelling === 'true',
26
+ dialog: [...document.querySelectorAll('[role="dialog"], dialog[open]')].some(visible) }),
27
+ checkSaved: async () => {
28
+ const response = await chrome.runtime.sendMessage({ type: 'codex-overleaf/migration-save-probe', ...binding,
29
+ nonce: activeNonce });
30
+ return response?.ok && response.nonce === activeNonce ? response.result : { ok: false, state: 'unavailable' };
31
+ } });
32
+ root.addEventListener('message', event => {
33
+ if (event.source !== root || event.origin !== location.origin || typeof event.data?.id !== 'string') return;
34
+ const data = event.data;
35
+ const mutation = PageRpcContract.getMethod(data.method)?.mutation;
36
+ if (data.source === 'codex-overleaf/content' && ['document', 'page_state', 'navigation'].includes(mutation)) {
37
+ pageWrites.add(data.id); idle.noteActivity();
38
+ } else if (data.source === 'codex-overleaf/page' && pageWrites.delete(data.id)) idle.noteActivity();
39
+ });
40
+ chrome.runtime.onMessage.addListener((message, sender, respond) => {
41
+ if (message?.type !== 'codex-overleaf/migration-idle-probe') return undefined;
42
+ if (sender?.id !== chrome.runtime.id || sender.tab || message.token !== binding.token
43
+ || message.transactionId !== binding.transactionId || typeof message.nonce !== 'string'
44
+ || !message.nonce || location.href !== binding.url || inFlight) { respond(blocked()); return false; }
45
+ inFlight = true;
46
+ activeNonce = message.nonce;
47
+ idle.buildIdleProbe().then(result => respond(location.href === binding.url ? {
48
+ ...result, transactionId: binding.transactionId, token: binding.token, nonce: message.nonce,
49
+ url: location.href, targetVersion: binding.targetVersion, safetyOnly: true
50
+ } : blocked())).catch(() => respond(blocked())).finally(() => { inFlight = false; activeNonce = ''; });
51
+ return true;
52
+ });
53
+ return { ok: true, url: location.href };
54
+ }
55
+ let activeNonce = '';
56
+ root.CodexOverleafUpdateSafetyReceiver = Object.freeze({ install });
57
+ root.__codexOverleafUpdateSafetyReceiverInstalled = true;
58
+ })(globalThis);
@@ -10,6 +10,13 @@
10
10
  function buildSyncApplyOperations(syncChanges = [], project = {}) {
11
11
  const existingPaths = new Set((project.files || []).map(file => file.path));
12
12
  return (syncChanges || []).map(change => {
13
+ // Keep an old Native move distinct so preflight can reject it safely.
14
+ // Falling through would turn a move into an empty create operation.
15
+ if (change.type === 'move') {
16
+ return { type: 'move', path: change.path, to: change.to,
17
+ moveProof: change.moveProof && typeof change.moveProof === 'object' ? { ...change.moveProof } : undefined,
18
+ reason: 'Move the existing Overleaf file while preserving its identity.' };
19
+ }
13
20
  if (change.type === 'binary-create' || change.type === 'overwrite-binary') {
14
21
  return {
15
22
  type: change.type,
@@ -22,6 +29,7 @@
22
29
  previousExists: change.previousExists === true,
23
30
  previousKind: change.previousKind || '',
24
31
  previousSize: Number.isFinite(Number(change.previousSize)) ? Number(change.previousSize) : undefined,
32
+ ...(typeof change.previousSha256 === 'string' ? { previousSha256: change.previousSha256 } : {}),
25
33
  reasonKey: change.type === 'overwrite-binary' ? 'localWorkspaceBinaryOverwrite' : 'localWorkspaceBinaryCreate',
26
34
  reason: change.type === 'overwrite-binary'
27
35
  ? 'Synced a binary asset replacement from the local Codex workspace.'
@@ -158,14 +166,18 @@
158
166
 
159
167
  if ((operation.type === 'rename' || operation.type === 'move') && operation.to) {
160
168
  const previous = filesByPath.get(operation.path) || originalByPath.get(operation.path);
169
+ const destination = filesByPath.get(operation.to);
170
+ const binary = operation.moveProof?.kind === 'binary' || previous?.kind === 'binary';
161
171
  filesByPath.delete(operation.path);
162
172
  filesByPath.set(operation.to, {
163
173
  ...(previous || {}),
174
+ ...(destination || {}),
164
175
  path: operation.to,
165
- kind: 'text',
166
- content: result.verifiedContent || previous?.content || '',
176
+ kind: binary ? 'binary' : 'text',
177
+ ...(!binary ? { content: result.verifiedContent ?? destination?.content ?? previous?.content ?? '' } : {}),
167
178
  source: 'verified-writeback'
168
179
  });
180
+ if (binary) delete filesByPath.get(operation.to).content;
169
181
  continue;
170
182
  }
171
183
 
@@ -227,10 +239,15 @@
227
239
  ? changeOrReason
228
240
  : { reason: changeOrReason };
229
241
  const reason = change.reason;
242
+ if (reason === 'snapshot_content_unavailable') {
243
+ return isEnglish
244
+ ? 'Current Overleaf content was not read; local changes were withheld until the file can be refreshed.'
245
+ : '未读取该文件当前的 Overleaf 内容;刷新取得内容前,本地改动不会写回。';
246
+ }
230
247
  if (reason === 'selection_scope_violation') {
231
248
  return isEnglish
232
- ? 'The local changes exceeded the captured selection. No changes from this task were written back.'
233
- : '本地改动超出了已附加的选区,本轮改动未写回。';
249
+ ? 'This local change exceeded the captured selection and was withheld. Independently valid selection changes may still be written.'
250
+ : '该项本地改动超出了已附加的选区,已阻止写回;独立且合法的选区内改动仍可写入。';
234
251
  }
235
252
  if (reason === 'generated_artifact') {
236
253
  return isEnglish
@@ -314,6 +331,13 @@
314
331
  if (!path || result.ok === false) { unsupported.push(path || 'unknown'); continue; }
315
332
  if (operation.type === 'delete') { expected.set(path, { absent: true }); continue; }
316
333
  if (operation.type === 'rename' || operation.type === 'move') {
334
+ const moveProof = result.moveProof || operation.moveProof;
335
+ if (moveProof?.v === 1 && moveProof.kind === 'binary' && /^[a-f0-9]{64}$/i.test(moveProof.sha256 || '')) {
336
+ if (!operation.to || (operation.moveProof?.sha256 && operation.moveProof.sha256 !== moveProof.sha256)) { unsupported.push(path); continue; }
337
+ if (operation.to !== path) expected.set(path, { absent: true });
338
+ expected.set(operation.to, { sha256: moveProof.sha256.toLowerCase() });
339
+ continue;
340
+ }
317
341
  const content = typeof result.verifiedContent === 'string'
318
342
  ? result.verifiedContent : original.get(path)?.content;
319
343
  if (!operation.to || typeof content !== 'string') { unsupported.push(path); continue; }
@@ -448,6 +472,30 @@
448
472
  } };
449
473
  }
450
474
 
475
+ async function buildMirrorWritebackConfirmation(applied = {}) {
476
+ const entries = Array.isArray(applied.applied) ? applied.applied : [];
477
+ if (!entries.length) return null;
478
+ const postImages = new Map();
479
+ for (const entry of entries) {
480
+ const operation = entry?.operation, result = entry?.result;
481
+ if (operation?.type !== 'edit' || typeof operation.path !== 'string' || !operation.path || result?.ok !== true
482
+ || result.verified !== true || typeof result.verifiedContent !== 'string') return null;
483
+ // Several edits can target one file; the last verified readback is its post-image.
484
+ postImages.set(operation.path, result.verifiedContent);
485
+ }
486
+ try {
487
+ const expectedFiles = [];
488
+ for (const [path, content] of postImages) {
489
+ const sha256 = await textDigest(content);
490
+ if (!/^[a-f0-9]{64}$/.test(sha256)) return null;
491
+ expectedFiles.push({ path, sha256 });
492
+ }
493
+ return { paths: expectedFiles.map(file => file.path), expectedFiles };
494
+ } catch (_error) {
495
+ return null; // No digest evidence: use the authoritative snapshot fallback.
496
+ }
497
+ }
498
+
451
499
  async function bytesDigest(bytes) {
452
500
  if (!globalThis.crypto?.subtle) return '';
453
501
  const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
@@ -543,6 +591,7 @@
543
591
 
544
592
  return {
545
593
  confirmPostWriteSave,
594
+ buildMirrorWritebackConfirmation,
546
595
  buildSaveCheck,
547
596
  confirmSaveCheck,
548
597
  buildSavedWritebackExpectations,
@@ -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({ 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({
@@ -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,20 @@
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
231
  appendCompletionReport({
199
232
  conclusion: assistantMessage || tx('Codex finished locally. There are no changes to sync back to Overleaf.', 'Codex 已完成本地处理,没有需要同步回 Overleaf 的改动。'),
200
- status: runMode === 'ask' ? tr('modeAsk') : 'completed',
233
+ status: additionalSkippedEntries.length ? 'failed' : runMode === 'ask' ? tr('modeAsk') : 'completed',
234
+ failure: blockedFailure,
201
235
  operations: [],
202
236
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
203
237
  unchangedReason: additionalSkippedEntries.find(item => item?.result?.reason)?.result.reason
@@ -208,14 +242,7 @@
208
242
  return {
209
243
  summaryLine: assistantMessage || tx('No changes to sync', '没有需要同步的改动'),
210
244
  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
- }),
245
+ settlement: blockedSettlement,
219
246
  audit: buildAuditSummaryFromApply({
220
247
  operations: buildSyncApplyOperations(syncChanges, project),
221
248
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
@@ -258,13 +285,20 @@
258
285
  additionalSkippedEntries.push(...binaryDecision.skipped);
259
286
 
260
287
  if (!operations.length) {
288
+ const approvalSettlement = buildSettlement({
289
+ operations: buildSyncApplyOperations(syncChanges, project),
290
+ applyResult: { ok: additionalSkippedEntries.length === 0, applied: [], skipped: additionalSkippedEntries }
291
+ });
292
+ const approvalFailure = approvalSettlement
293
+ ? writebackSettlement.projectRunSettlement({ settlement: approvalSettlement.facts }).primaryFailure : undefined;
261
294
  appendRunEvent({
262
295
  title: tx('No approved file changes remain to sync back to Overleaf.', '没有剩余已确认的文件改动需要同步回 Overleaf。'),
263
296
  status: 'completed'
264
297
  });
265
298
  appendCompletionReport({
266
299
  conclusion: assistantMessage || tx('No approved changes were written back to Overleaf.', '没有已确认的改动写回 Overleaf。'),
267
- status: runMode === 'ask' ? tr('modeAsk') : 'completed',
300
+ status: additionalSkippedEntries.length ? 'failed' : runMode === 'ask' ? tr('modeAsk') : 'completed',
301
+ failure: approvalFailure,
268
302
  operations: [],
269
303
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
270
304
  unchangedReason: additionalSkippedEntries.find(item => item?.result?.reason)?.result.reason
@@ -275,14 +309,7 @@
275
309
  return {
276
310
  summaryLine: tx('No changes applied', '没有应用改动'),
277
311
  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
- }),
312
+ settlement: approvalSettlement,
286
313
  audit: buildAuditSummaryFromApply({
287
314
  operations: buildSyncApplyOperations(syncChanges, project),
288
315
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
@@ -336,7 +363,7 @@
336
363
  status: 'running',
337
364
  activity: { v: 1, kind: 'sync', id: writebackActivityId, state: 'running', paths: operations.map(operation => operation.path).filter(Boolean) }
338
365
  });
339
- await stageWritebackRecovery?.(operations, saveTarget, runRequireReviewing);
366
+ await stageWritebackRecovery?.(operations, saveTarget, runRequireReviewing, project);
340
367
  const applied = operations.length
341
368
  ? mergeApplyResultSkipped(await assetTransferBroker.applyOperations({
342
369
  operations,
@@ -351,12 +378,8 @@
351
378
  }), additionalSkippedEntries)
352
379
  : mergeApplyResultSkipped({ ok: true, applied: [], skipped: [] }, additionalSkippedEntries);
353
380
  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.' } } });
381
+ for (const change of unsupportedChanges.filter(item => ['subagent_unfinished_output', 'subagent_unauthorized_edit', 'selection_scope_violation'].includes(item.reason))) {
382
+ applied.skipped.push(buildWithheldEntry(change));
360
383
  applied.ok = false;
361
384
  }
362
385
  // Record the apply result + undo checkpoint IMMEDIATELY, before any
@@ -367,7 +390,7 @@
367
390
  // landed in Overleaf. The manual-confirm path already records undo right
368
391
  // after its write — match that ordering here (v1.6.2).
369
392
  appendApplyResult(applied);
370
- recordUndoFromApply(project, applied);
393
+ await recordUndoFromApply(project, applied);
371
394
  const skippedEntries = getSkippedEntries(applied);
372
395
  if (skippedEntries.length) {
373
396
  appendPartialWritebackWarning(applied);
@@ -568,37 +591,29 @@
568
591
  return 'failed';
569
592
  }
570
593
 
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']);
594
+ // Text-only confirmation requires the page's verified post-image digest.
595
+ // Local files can change after writeback; Native must match those digests
596
+ // before advancing its baseline. Missing evidence and non-text/tree writes
597
+ // retain the authoritative ZIP fallback below.
579
598
 
580
599
  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
600
  try {
601
+ if (!runProjectId || getCurrentProjectId() !== runProjectId) return false;
602
+ const proof = await writebackController.buildMirrorWritebackConfirmation?.(applied);
603
+ if (!proof || getCurrentProjectId() !== runProjectId) return false;
594
604
  const response = await sendBackgroundNative({
595
605
  method: 'mirror.confirmWriteback',
596
606
  params: {
597
607
  projectId: runProjectId,
598
- paths: Array.from(new Set(paths))
608
+ ...proof
599
609
  }
600
610
  });
601
- return response?.ok === true && response?.result?.ok === true;
611
+ const confirmations = Array.isArray(response?.result?.confirmed) ? response.result.confirmed : [];
612
+ const confirmed = new Map(confirmations.map(file => [file?.path, file?.hash]));
613
+ return getCurrentProjectId() === runProjectId && response?.ok === true && response?.result?.ok === true
614
+ && confirmations.length === proof.expectedFiles.length
615
+ && confirmed.size === proof.expectedFiles.length
616
+ && proof.expectedFiles.every(file => confirmed.get(file.path) === file.sha256);
602
617
  } catch (error) {
603
618
  return false;
604
619
  }
@@ -751,12 +766,13 @@
751
766
  }
752
767
  return buildPostWriteCompileSummary({ result, logResult });
753
768
  } else {
754
- const reason = result?.reason || tx('unknown reason', '未知原因');
769
+ const reason = CompileAdapter.localizeCompileReason(result?.reason, getLocale()) || tx('unknown reason', '未知原因');
755
770
  appendRunEvent({ title: tx(`Post-write compile did not succeed: ${reason}`, `写后编译未成功:${reason}`), status: 'failed', activity: activity('failed', reason) });
756
771
  return buildPostWriteCompileSummary({ result });
757
772
  }
758
773
  } catch (error) {
759
- appendRunEvent({ title: tx(`Post-write compile failed: ${error.message}`, `写后编译出错:${error.message}`), status: 'failed', activity: activity('failed', error.message) });
774
+ const reason = CompileAdapter.localizeCompileReason(error.message, getLocale());
775
+ appendRunEvent({ title: tx(`Post-write compile failed: ${reason}`, `写后编译出错:${reason}`), status: 'failed', activity: activity('failed', reason) });
760
776
  return buildPostWriteCompileSummary({ error });
761
777
  }
762
778
  }
@@ -765,19 +781,11 @@
765
781
  return compileAdapter.buildPostWriteCompileSummary(input);
766
782
  }
767
783
 
768
- // First structured per-operation failure among the skipped writeback
769
- // entries — the run-level failure the completion report promotes.
784
+ // Use the same normalized failure selection as settlement and its UI, while
785
+ // preserving the selected producer's full evidence on the historical report.
770
786
  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;
787
+ const failures = writebackSettlement.collectFailuresFromResult(applied);
788
+ return writebackSettlement.selectPrimaryFailure(failures);
781
789
  }
782
790
 
783
791
  function appendCompileSummaryToConclusion(conclusion, compileSummary) {
@@ -807,9 +815,10 @@
807
815
  )
808
816
  : tx('Post-write compile was triggered; Overleaf may still be updating the result.', '已触发写后编译;Overleaf 可能仍在更新结果。');
809
817
  } else if (compileSummary.status === 'failed') {
818
+ const reason = compileAdapter.localizeCompileReason(compileSummary.reason, getLocale());
810
819
  summary = tx(
811
- `Post-write compile check failed: ${compileSummary.reason || 'unknown reason'}`,
812
- `写后编译检查失败:${compileSummary.reason || '未知原因'}`
820
+ `Post-write compile check failed: ${reason || 'unknown reason'}`,
821
+ `写后编译检查失败:${reason || '未知原因'}`
813
822
  );
814
823
  } else {
815
824
  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', '重试'), () => {