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
@@ -94,6 +94,7 @@
94
94
  const journals = await listJournals(projectKey).catch(() => []);
95
95
  let changed = false;
96
96
  const acknowledgeIds = [];
97
+ const recoveredRuns = [];
97
98
  for (const journal of journals) {
98
99
  if (!journal?.terminal && !journal?.ownerLost) {
99
100
  continue;
@@ -104,7 +105,8 @@
104
105
  continue;
105
106
  }
106
107
  const session = options.findSession?.(journal.sessionId);
107
- if (!session) {
108
+ if (!session || (session.projectId && session.projectId !== projectKey)
109
+ || (options.accountScopeId && session.accountScopeId && session.accountScopeId !== options.accountScopeId)) {
108
110
  continue;
109
111
  }
110
112
  let record = (session.runs || []).find(run => run.id === journal.clientRunId);
@@ -117,6 +119,8 @@
117
119
  if (!record) {
118
120
  continue;
119
121
  }
122
+ if (record.runProjectId && record.runProjectId !== projectKey) continue;
123
+ if (!record.runProjectId) record.runProjectId = projectKey;
120
124
  if (record.status === 'running') {
121
125
  options.normalizeInterruptedRun?.(record);
122
126
  }
@@ -163,8 +167,10 @@
163
167
  options.pauseSessionQueue?.(session, 'page_reload');
164
168
  changed = true;
165
169
  acknowledgeIds.push(journal.requestId);
170
+ recoveredRuns.push({ sessionId: session.id, runId: record.id,
171
+ projectId: projectKey, accountScopeId: options.accountScopeId });
166
172
  }
167
- return { changed, acknowledgeIds };
173
+ return { changed, acknowledgeIds, recoveredRuns };
168
174
  }
169
175
 
170
176
  function destroy() {
@@ -85,7 +85,8 @@
85
85
  */
86
86
  function normalizeSkippedFailure(item) {
87
87
  if (!FailureReasons || !FailureReasons.normalizeFailureReason) return null;
88
- const failure = FailureReasons.normalizeFailureReason(item?.result, item?.operation || {}, { locale: getLocale() });
88
+ const normalize = FailureReasons.normalizeWritebackFailure || FailureReasons.normalizeFailureReason;
89
+ const failure = normalize(item?.result, item?.operation || {}, { locale: getLocale() });
89
90
  if (!failure || !failure.code) return null;
90
91
  const localized = FailureReasons.localizeFailureReason
91
92
  ? FailureReasons.localizeFailureReason(failure, getLocale(), failureReasonI18nLookup)
@@ -98,7 +99,7 @@
98
99
 
99
100
  function pickPrimarySkippedFailure(failures) {
100
101
  if (!FailureReasons || !FailureReasons.selectPrimaryFailure) return null;
101
- return FailureReasons.selectPrimaryFailure(failures);
102
+ return (FailureReasons.selectPrimaryWritebackFailure || FailureReasons.selectPrimaryFailure)(failures);
102
103
  }
103
104
 
104
105
  function failureReasonI18nLookup(key) {
@@ -134,6 +135,8 @@
134
135
  const code = result.code || '';
135
136
  const filePath = result.reasonParams?.filePath || operation.path || operation.from || operation.to || '';
136
137
  const withDebug = reason => appendApplyResultDebug(reason, result);
138
+ const catalogReason = formatCatalogFailureReason(result, filePath);
139
+ if (catalogReason) return withDebug(catalogReason);
137
140
  if (key === 'missingBaseFile' || code === 'missing_base_file') {
138
141
  const target = filePath || tx('this file', '这个文件');
139
142
  return withDebug(tx(
@@ -236,82 +239,52 @@
236
239
  return parts.join(', ');
237
240
  }
238
241
 
242
+ function formatCatalogFailureReason(result, path) {
243
+ if (!FailureReasons?.formatFailureReasonForDisplay) return '';
244
+ // Compact historical settlements can omit canonical validation fields.
245
+ // Their display is still recoverable without re-normalizing stored facts.
246
+ const failure = result.failure && typeof result.failure === 'object'
247
+ ? { ...result.failure, file: result.failure.file || result.file || path }
248
+ : FailureReasons.normalizeFailureReason(result, { path: result.file || path });
249
+ const display = FailureReasons.formatFailureReasonForDisplay(failure, getLocale(), failureReasonI18nLookup);
250
+ return display.localized ? display.message : '';
251
+ }
252
+
239
253
  function formatBridgeResultReason(result = {}, fallbackPath = '') {
240
- const code = result.code || '';
241
- const path = result.path || fallbackPath || '';
242
- if (getLocale() !== 'en') {
243
- return localizeVisibleReason(result.reason || result.error || result.code || tr('unknownReason'));
244
- }
245
- if (code === 'missing_tracked_changes') {
246
- return 'No matching Overleaf tracked-change records were found for this run, so Codex did not undo with text patches.';
247
- }
248
- if (code === 'tracked_change_file_open_failed') {
249
- return path
250
- ? `Could not open ${path} to find this run's tracked changes. Handle them manually in the Overleaf review tools.`
251
- : 'Could not open the file to find this run\'s tracked changes. Handle them manually in the Overleaf review tools.';
252
- }
253
- if (code === 'tracked_change_not_found') {
254
- return 'No matching tracked changes were found on the Overleaf page for this run.';
255
- }
256
- if (code === 'tracked_change_reject_control_not_found') {
257
- return path
258
- ? `Some tracked changes in ${path} remain, but the matching Reject button was not found. Reject them manually in Overleaf.`
259
- : 'Some tracked changes remain, but the matching Reject button was not found. Reject them manually in Overleaf.';
260
- }
261
- if (code === 'tracked_change_reject_not_confirmed') {
262
- return 'Codex clicked Reject, but Overleaf still shows this tracked change. Reject it manually in the Overleaf review panel.';
263
- }
264
- if (code === 'tracked_change_accept_control_not_found') {
265
- return path
266
- ? `Some tracked changes in ${path} remain, but the matching Accept button was not found. Accept them manually in Overleaf.`
267
- : 'Some tracked changes remain, but the matching Accept button was not found. Accept them manually in Overleaf.';
268
- }
269
- if (code === 'tracked_change_accept_not_confirmed') {
270
- return 'Codex clicked Accept, but Overleaf still shows this tracked change. Accept it manually in the Overleaf review panel.';
271
- }
272
- if (code === 'tracked_change_editor_undo_open_failed') {
273
- return path ? `Could not open ${path} for Overleaf native undo.` : 'Could not open the file for Overleaf native undo.';
274
- }
275
- if (code === 'tracked_change_editor_undo_current_mismatch') {
276
- return path
277
- ? `${path} no longer matches this run's written content, so Codex did not use Overleaf native undo.`
278
- : 'The current content no longer matches this run\'s written content, so Codex did not use Overleaf native undo.';
279
- }
280
- if (code === 'editor_undo_control_not_found') {
281
- return path
282
- ? `Could not find Overleaf's editor Undo button to undo this run in ${path}.`
283
- : 'Could not find Overleaf\'s editor Undo button to undo this run.';
284
- }
285
- if (code === 'editor_undo_no_progress') {
286
- return path
287
- ? `Codex clicked Overleaf Undo, but ${path} did not change.`
288
- : 'Codex clicked Overleaf Undo, but the file did not change.';
289
- }
290
- if (code === 'editor_undo_max_iterations') {
291
- return path
292
- ? `${path} did not return to its pre-run content after repeated Overleaf native undo steps. Codex stopped to avoid undoing other edits.`
293
- : 'The file did not return to its pre-run content after repeated Overleaf native undo steps. Codex stopped to avoid undoing other edits.';
294
- }
295
- if (code === 'tracked_change_undo_max_iterations') {
296
- return path
297
- ? `${path} still has tracked changes left. Codex stopped to avoid rejecting unrelated edits.`
298
- : 'Tracked changes are still left. Codex stopped to avoid rejecting unrelated edits.';
299
- }
300
- if (code === 'tracked_change_undo_verify_open_failed') {
301
- return path
302
- ? `Could not open ${path} after undo to verify content. Reload Overleaf and check manually.`
303
- : 'Could not open the file after undo to verify content. Reload Overleaf and check manually.';
304
- }
305
- if (code === 'tracked_change_undo_verify_failed') {
306
- return path
307
- ? `${path} did not return to its pre-run content after rejecting tracked changes. Check this run's changes in the Overleaf review panel.`
308
- : 'The file did not return to its pre-run content after rejecting tracked changes. Check this run\'s changes in the Overleaf review panel.';
309
- }
310
- return localizeVisibleReason(result.reason || result.error || result.code || tr('unknownReason'));
254
+ const path = result.file || result.path || fallbackPath || '';
255
+ return formatCatalogFailureReason(result, path)
256
+ || localizeVisibleReason(result.reason || result.error || result.code || tr('unknownReason'));
311
257
  }
312
258
 
259
+ const runtimeOwnedReasonPairs = [
260
+ ['The selected provider no longer exists.', '所选模型服务已不存在。'],
261
+ ['The selected provider has no usable revision.', '所选模型服务没有可用的配置版本。'],
262
+ ['The selected provider revision was not captured for this run.', '本轮任务未记录所选模型服务的配置版本。'],
263
+ ['The selected provider changed after this run was submitted.', '本轮任务提交后,所选模型服务的配置已发生变化。'],
264
+ ['The captured selection is invalid. Attach it again.', '已记录的选区无效,请重新添加。'],
265
+ ['Provider revision must be a finite non-negative integer.', '模型服务配置版本必须是有效的非负整数。'],
266
+ ['A stable matching Overleaf account is required for shared history.', '共享历史需要稳定且匹配的 Overleaf 账号。'],
267
+ ['The background storage connection was lost.', '后台存储连接已断开。'],
268
+ ['Shared history is unavailable. Reload the extension and this page.', '共享历史不可用,请重新加载扩展和当前页面。'],
269
+ ['The shared history lock disconnected.', '共享历史的存储锁连接已断开。'],
270
+ ['Another page is saving history. Retry after it finishes.', '其他页面正在保存历史,请待保存完成后重试。'],
271
+ ['The shared history lock was lost before completion.', '操作完成前,共享历史的存储锁已丢失。'],
272
+ ['A stable account scope is required for durable persistence.', '持久保存需要稳定的账号范围。'],
273
+ ['A cross-realm browser lock is required for durable persistence.', '持久保存需要能够跨页面协调的浏览器锁。'],
274
+ ['Shared session storage is unavailable.', '共享会话存储不可用。'],
275
+ ['Native host request failed.', '本地主机请求失败。'],
276
+ ['Native host disconnected', '本地主机连接已断开。']
277
+ ];
278
+
313
279
  function localizeVisibleReason(reason) {
314
280
  const text = String(reason || '').trim();
281
+ for (const [en, zh] of runtimeOwnedReasonPairs) {
282
+ if (text === en || text === zh) return tx(en, zh);
283
+ }
284
+ const scoped = /^(?:Scoped persistence failed: |限定范围的持久保存失败:)(invalid_commit|stale_view|revision_conflict|queue_revision_conflict|invalid_queue_mutation|queue_claim_conflict)$/.exec(text);
285
+ if (scoped) return tx(`Scoped persistence failed: ${scoped[1]}`, `限定范围的持久保存失败:${scoped[1]}`);
286
+ const localized = FailureReasons?.localizeFailureMessage?.(text, getLocale(), failureReasonI18nLookup);
287
+ if (localized && localized !== text) return localized;
315
288
  if (!text || getLocale() !== 'en') {
316
289
  return text;
317
290
  }
@@ -17,11 +17,13 @@
17
17
  const deadlineAt = Number.isFinite(input.deadlineAt) && input.deadlineAt > 0 ? input.deadlineAt : null;
18
18
  const invalidDeadline = input.deadlineAt !== undefined && deadlineAt === null;
19
19
  const expired = () => invalidDeadline || (deadlineAt !== null && now() >= deadlineAt);
20
- let textBatch = [], blocked = false;
20
+ let textBatch = [], blocked = false, cancelled = false, halted = false;
21
21
  function skipUndispatched(operations) {
22
22
  const result = normalizeTextResult({ ok: false,
23
- code: blocked ? 'writeback_tail_not_started' : 'writeback_deadline_exceeded',
24
- error: blocked ? 'An earlier writeback is unconfirmed. This operation was not dispatched.'
23
+ code: cancelled ? 'codex_cancelled' : blocked ? 'writeback_tail_not_started' : 'writeback_deadline_exceeded',
24
+ error: cancelled ? 'The run was cancelled. This operation was not dispatched.'
25
+ : halted ? 'An earlier writeback stopped this file plan. This operation was not dispatched.'
26
+ : blocked ? 'An earlier writeback is unconfirmed. This operation was not dispatched.'
25
27
  : invalidDeadline ? 'The writeback deadline is invalid. This operation was not dispatched.'
26
28
  : 'The writeback deadline has elapsed. This operation was not dispatched.',
27
29
  changedDocument: false }, operations);
@@ -32,26 +34,38 @@
32
34
  if (!textBatch.length) return;
33
35
  const operations = textBatch;
34
36
  textBatch = [];
35
- if (blocked || expired()) { skipUndispatched(operations); return; }
36
- const response = await callPageBridge('applyOperations', {
37
+ if (blocked || cancelled || expired()) { skipUndispatched(operations); return; }
38
+ let response;
39
+ try { response = await callPageBridge('applyOperations', {
37
40
  operations,
38
41
  baseFiles: input.baseFiles || [],
39
42
  reviewingPolicy: input.reviewingPolicy || '',
43
+ ...(input.reviewOwnerToken ? { reviewOwnerToken: input.reviewOwnerToken } : {}),
40
44
  ...(deadlineAt !== null ? { deadlineAt } : {}),
41
45
  requireReviewing: input.requireReviewing === true,
42
46
  requireEditing: input.requireEditing === true,
43
47
  retryCreates: input.retryCreates === true,
44
48
  runProjectId: input.runProjectId || ''
45
- });
46
- const recovered = response?.code === 'page_bridge_timeout' && response.requestId
49
+ }); } catch (error) {
50
+ if (error?.code !== 'codex_cancelled' && error?.cancelled !== true) throw error;
51
+ cancelled = true;
52
+ // A later cancelled batch must not discard earlier write/Undo evidence.
53
+ if (!error.requestId && !results.length) throw error;
54
+ response = { ok: false, code: 'codex_cancelled', error: error.message,
55
+ requestId: error.requestId, changedDocument: error.changedDocument !== false };
56
+ }
57
+ const recovered = ['page_bridge_timeout', 'codex_cancelled'].includes(response?.code) && response.requestId
47
58
  ? await recoverTextReceipt(response, operations, input.runProjectId || '', deadlineAt)
48
59
  : response;
49
60
  const result = normalizeTextResult(recovered, operations);
61
+ if (response?.code === 'codex_cancelled' && response.changedDocument === false) result.receiptUnconfirmed = false;
50
62
  results.push(result);
51
- blocked = result.receiptUnconfirmed === true;
63
+ cancelled ||= hasCancellation(result);
64
+ halted ||= result.haltRemainingOperations === true;
65
+ blocked = halted || result.receiptUnconfirmed === true;
52
66
  }
53
67
  for (const operation of Array.isArray(input.operations) ? input.operations : []) {
54
- if (blocked || expired()) {
68
+ if (blocked || cancelled || expired()) {
55
69
  skipUndispatched([operation]);
56
70
  continue;
57
71
  }
@@ -60,12 +74,13 @@
60
74
  continue;
61
75
  }
62
76
  await flushTextBatch();
63
- if (blocked || expired()) {
77
+ if (blocked || cancelled || expired()) {
64
78
  skipUndispatched([operation]);
65
79
  continue;
66
80
  }
67
81
  const binaryResult = await applyBinaryOperation(operation, input);
68
82
  results.push(binaryResult);
83
+ cancelled ||= hasCancellation(binaryResult);
69
84
  blocked = binaryResult.receiptUnconfirmed === true;
70
85
  }
71
86
  await flushTextBatch();
@@ -84,14 +99,16 @@
84
99
  receipt = await callPageBridge('getWritebackReceipt', { requestId: timeout.requestId, runProjectId,
85
100
  ...(deadlineAt !== null ? { deadlineAt } : {}) });
86
101
  } catch (error) {
87
- if (error?.code === 'codex_cancelled' || error?.cancelled === true) throw error;
102
+ if (error?.code === 'codex_cancelled' || error?.cancelled === true) {
103
+ recoveryError = error.message || 'Receipt settlement was cancelled.'; break;
104
+ }
88
105
  recoveryError = error?.message || String(error);
89
106
  if (!isTransientReceiptFailure(error)) break;
90
107
  transportFailed = true;
91
108
  }
92
109
  if (!transportFailed) {
93
110
  if (receipt?.code === 'codex_cancelled' || receipt?.cancelled === true) {
94
- throw responseError(receipt, 'codex_cancelled');
111
+ recoveryError = receipt.error || 'Receipt settlement was cancelled.'; break;
95
112
  }
96
113
  if (receipt?.ok !== true) {
97
114
  recoveryError = receipt?.error?.message || receipt?.error || receipt?.reason
@@ -130,8 +147,9 @@
130
147
  const transferId = `asset_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
131
148
  const assetRef = operation.assetRef || null;
132
149
  const safeOperation = sanitizeOperation(operation);
133
- let pageBegun = false, commitAttempted = false;
150
+ let beginAttempted = false, pageBegun = false, commitAttempted = false;
134
151
  try {
152
+ beginAttempted = true;
135
153
  const begin = await callPageBridge('binaryUploadBegin', {
136
154
  transferId,
137
155
  path: operation.path,
@@ -139,6 +157,7 @@
139
157
  sha256: assetRef?.sha256 || operation.sha256 || '',
140
158
  mimeType: assetRef?.mimeType || operation.mimeType || 'application/octet-stream',
141
159
  overwrite: operation.type === 'overwrite-binary',
160
+ previousSha256: operation.type === 'overwrite-binary' ? await binaryBaselineHash(operation, input.baseFiles) : undefined,
142
161
  runProjectId: input.runProjectId || ''
143
162
  });
144
163
  if (!begin?.ok) throw responseError(begin, 'binary_upload_begin_failed');
@@ -164,6 +183,8 @@
164
183
  if (!committed?.ok) throw responseError(committed, committed?.code || 'binary_upload_failed');
165
184
  return { ok: true, applied: [{ operation: safeOperation, result: committed }], skipped: [] };
166
185
  } catch (error) {
186
+ // A commit rejected before dispatch still leaves staged bytes to clean up.
187
+ if (error.requestDispatched === false) commitAttempted = false;
167
188
  let changedDocument = error?.changedDocument === true
168
189
  || (commitAttempted && error?.changedDocument !== false);
169
190
  if (commitAttempted && changedDocument) {
@@ -176,6 +197,7 @@
176
197
  || receipt.runProjectId !== input.runProjectId) break;
177
198
  if (receipt.result?.ok === true) {
178
199
  return { ok: true, applied: [{ operation: safeOperation, result: receipt.result }], skipped: [],
200
+ cancelled: error.code === 'codex_cancelled' || error.cancelled === true,
179
201
  receiptRecovery: { transferId, recovered: true, initialError: error.message } };
180
202
  }
181
203
  if (receipt.state === 'completed' && receipt.changedDocument === false) {
@@ -186,7 +208,8 @@
186
208
  await delay(Math.min(500, Math.max(1, deadline - now())));
187
209
  } while (now() <= deadline);
188
210
  }
189
- if (pageBegun && !commitAttempted) {
211
+ const uncertainBegin = beginAttempted && error.code === 'codex_cancelled' && error.requestDispatched !== false;
212
+ if (!commitAttempted && (pageBegun || uncertainBegin)) {
190
213
  await callPageBridge('binaryUploadAbort', { transferId, runProjectId: input.runProjectId || '' }).catch(() => null);
191
214
  }
192
215
  const code = error.code || 'binary_upload_failed';
@@ -203,6 +226,9 @@
203
226
  ok: false,
204
227
  code,
205
228
  reason: error.message || 'Binary asset upload failed.',
229
+ ...(Array.isArray(error.createdDirectories) ? { createdDirectories: error.createdDirectories,
230
+ folderChanged: error.folderChanged === true, fileUploadAttempted: error.fileUploadAttempted === true,
231
+ directoryCleanupRequired: error.createdDirectories.length > 0 } : {}),
206
232
  changedDocument,
207
233
  failure: { code, stage: 'write', file: operation.path, changedDocument }
208
234
  }
@@ -249,6 +275,7 @@
249
275
  const result = response && typeof response === 'object' ? { ...response } : { ok: false };
250
276
  result.applied = Array.isArray(result.applied) ? result.applied : [];
251
277
  result.skipped = Array.isArray(result.skipped) ? result.skipped.slice() : [];
278
+ if (result.skipped.some(entry => entry.result?.receiptUnconfirmed === true)) result.receiptUnconfirmed = true;
252
279
  const key = operation => [operation?.type, operation?.path, operation?.to || ''].join('\0');
253
280
  const known = new Map();
254
281
  for (const entry of [...result.applied, ...result.skipped]) {
@@ -286,6 +313,7 @@
286
313
  for (const result of results) {
287
314
  merged.applied.push(...(Array.isArray(result?.applied) ? result.applied : []));
288
315
  merged.skipped.push(...(Array.isArray(result?.skipped) ? result.skipped : []));
316
+ if (result?.haltRemainingOperations) merged.haltRemainingOperations = true;
289
317
  // Text writeback returns the Reviewing references captured by the page
290
318
  // bridge. They are lifecycle data, not diagnostics: dropping them makes
291
319
  // recordUndoFromApply classify a successful Track write as legacy Undo,
@@ -322,6 +350,31 @@
322
350
  return safe;
323
351
  }
324
352
 
353
+ function hasCancellation(result) {
354
+ return result?.cancelled === true || result?.code === 'codex_cancelled'
355
+ || result?.skipped?.some(entry => entry.result?.code === 'codex_cancelled');
356
+ }
357
+
358
+ async function binaryBaselineHash(operation, baseFiles = []) {
359
+ const valid = value => typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value);
360
+ const base = (baseFiles || []).find(file => file?.path === operation.path);
361
+ let captured = valid(base?.sha256) ? base.sha256.toLowerCase() : '';
362
+ if (typeof base?.contentBase64 === 'string' && globalThis.crypto?.subtle && typeof atob === 'function') {
363
+ const bytes = Uint8Array.from(atob(base.contentBase64), value => value.charCodeAt(0));
364
+ const hash = await globalThis.crypto.subtle.digest('SHA-256', bytes);
365
+ const digest = Array.from(new Uint8Array(hash), value => value.toString(16).padStart(2, '0')).join('');
366
+ if (captured && captured !== digest) throw codedError('binary_asset_baseline_conflict', 'The captured binary baseline has conflicting content and hash.');
367
+ captured = digest;
368
+ }
369
+ if (operation.previousSha256 != null) {
370
+ if (!valid(operation.previousSha256) || (captured && captured !== operation.previousSha256.toLowerCase())) {
371
+ throw codedError('binary_asset_baseline_conflict', 'The Native and browser binary baselines differ. Read the project again before replacement.');
372
+ }
373
+ return operation.previousSha256.toLowerCase();
374
+ }
375
+ return captured;
376
+ }
377
+
325
378
  function isTransientReceiptFailure(value) {
326
379
  const code = value?.code || value?.error?.code;
327
380
  if (code) return ['page_bridge_timeout', 'page_bridge_unavailable', 'page_bridge_transport_error',
@@ -337,6 +390,11 @@
337
390
  response?.reason || response?.error?.message || response?.error || 'Asset transfer failed.'
338
391
  );
339
392
  if (typeof response?.changedDocument === 'boolean') error.changedDocument = response.changedDocument;
393
+ if (Array.isArray(response?.createdDirectories)) {
394
+ error.createdDirectories = response.createdDirectories;
395
+ error.folderChanged = response.folderChanged === true;
396
+ error.fileUploadAttempted = response.fileUploadAttempted === true;
397
+ }
340
398
  error.stage = response?.failure?.stage || 'write';
341
399
  return error;
342
400
  }
@@ -6,6 +6,7 @@
6
6
  const tx = typeof options.tx === 'function' ? options.tx : (en) => en;
7
7
  const tr = typeof options.tr === 'function' ? options.tr : (key) => key;
8
8
  const appendPlainLog = typeof options.appendPlainLog === 'function' ? options.appendPlainLog : () => {};
9
+ const onChange = typeof options.onChange === 'function' ? options.onChange : () => {};
9
10
  const limits = options.limits || {};
10
11
  const maxAttachments = Number(limits.maxAttachments) || 8;
11
12
  const maxAttachmentBytes = Number(limits.maxAttachmentBytes) || 12 * 1024 * 1024;
@@ -15,6 +16,8 @@
15
16
  let attachments = [];
16
17
  const pendingAttachmentKeys = new Set();
17
18
  let pendingAttachmentBytes = 0;
19
+ let attachmentGeneration = 0;
20
+ let attachmentRevision = 0;
18
21
 
19
22
  function handlePaste(event) {
20
23
  const files = collectFilesFromDataTransfer(event.clipboardData);
@@ -59,51 +62,68 @@
59
62
  }
60
63
 
61
64
  async function addFiles(files) {
62
- for (const file of Array.from(files || [])) {
63
- const dedupeKey = buildAttachmentDedupeKey(file);
64
- if (isDuplicateAttachmentFile(dedupeKey)) {
65
- continue;
66
- }
67
- if (attachments.length + pendingAttachmentKeys.size >= maxAttachments) {
68
- appendPlainLog(tx(
69
- `Attachment limit reached (${maxAttachments}).`,
70
- `附件数量已达上限(${maxAttachments} 个)。`
71
- ));
72
- break;
73
- }
74
- validateAttachmentFile(file);
75
- const fileSize = Number(file.size) || 0;
76
- if (!canReserveAttachmentBytes(fileSize)) {
77
- appendPlainLog(tx(
78
- `Attachment total limit reached (${formatFileSize(maxAttachmentTotalBytes)}). Remove attachments or use smaller files.`,
79
- `附件总大小已达上限(${formatFileSize(maxAttachmentTotalBytes)})。请移除部分附件或使用更小的文件。`
80
- ));
81
- continue;
82
- }
83
- pendingAttachmentKeys.add(dedupeKey);
84
- pendingAttachmentBytes += fileSize;
85
- try {
86
- const dataUrl = await readFileAsDataUrl(file);
87
- const preview = await buildAttachmentPreviewData(file, dataUrl);
88
- const attachment = {
89
- id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
90
- name: normalizeAttachmentName(file.name),
91
- mimeType: String(file.type || ''),
92
- size: Number(file.size) || 0,
93
- kind: preview.kind,
94
- previewDataUrl: preview.previewDataUrl,
95
- contentBase64: extractBase64FromDataUrl(dataUrl),
96
- dedupeKey
97
- };
98
- if (!attachments.some(existing => buildAttachmentDedupeKey(existing) === dedupeKey)) {
99
- attachments.push(attachment);
65
+ const generation = attachmentGeneration;
66
+ try {
67
+ for (const file of Array.from(files || [])) {
68
+ if (generation !== attachmentGeneration) return;
69
+ const dedupeKey = buildAttachmentDedupeKey(file);
70
+ if (isDuplicateAttachmentFile(dedupeKey)) {
71
+ continue;
72
+ }
73
+ if (attachments.length + pendingAttachmentKeys.size >= maxAttachments) {
74
+ appendPlainLog(tx(
75
+ `Attachment limit reached (${maxAttachments}).`,
76
+ `附件数量已达上限(${maxAttachments} 个)。`
77
+ ));
78
+ break;
79
+ }
80
+ validateAttachmentFile(file);
81
+ const fileSize = Number(file.size) || 0;
82
+ if (!canReserveAttachmentBytes(fileSize)) {
83
+ appendPlainLog(tx(
84
+ `Attachment total limit reached (${formatFileSize(maxAttachmentTotalBytes)}). Remove attachments or use smaller files.`,
85
+ `附件总大小已达上限(${formatFileSize(maxAttachmentTotalBytes)})。请移除部分附件或使用更小的文件。`
86
+ ));
87
+ continue;
88
+ }
89
+ attachmentRevision += 1;
90
+ pendingAttachmentKeys.add(dedupeKey);
91
+ pendingAttachmentBytes += fileSize;
92
+ try {
93
+ const dataUrl = await readFileAsDataUrl(file, tx);
94
+ if (generation !== attachmentGeneration) return;
95
+ const preview = await buildAttachmentPreviewData(file, dataUrl);
96
+ if (generation !== attachmentGeneration) return;
97
+ const attachment = {
98
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
99
+ name: normalizeAttachmentName(file.name),
100
+ mimeType: String(file.type || ''),
101
+ size: Number(file.size) || 0,
102
+ kind: preview.kind,
103
+ previewDataUrl: preview.previewDataUrl,
104
+ contentBase64: extractBase64FromDataUrl(dataUrl),
105
+ dedupeKey
106
+ };
107
+ if (!attachments.some(existing => buildAttachmentDedupeKey(existing) === dedupeKey)) {
108
+ attachments.push(attachment);
109
+ attachmentRevision += 1;
110
+ onChange();
111
+ }
112
+ } catch (error) {
113
+ if (generation !== attachmentGeneration) return;
114
+ throw error;
115
+ } finally {
116
+ // Cleared work no longer owns reservations in the current composer.
117
+ if (generation === attachmentGeneration) {
118
+ pendingAttachmentBytes = Math.max(0, pendingAttachmentBytes - fileSize);
119
+ pendingAttachmentKeys.delete(dedupeKey);
120
+ }
100
121
  }
101
- } finally {
102
- pendingAttachmentBytes = Math.max(0, pendingAttachmentBytes - fileSize);
103
- pendingAttachmentKeys.delete(dedupeKey);
104
122
  }
123
+ } finally {
124
+ // A partial batch may have accepted files before validation or reading failed.
125
+ if (generation === attachmentGeneration) renderComposerAttachments();
105
126
  }
106
- renderComposerAttachments();
107
127
  }
108
128
 
109
129
  function getAttachmentsForRun() {
@@ -144,15 +164,21 @@
144
164
  });
145
165
  }
146
166
 
147
- function clear() {
167
+ function clear(options = {}) {
168
+ attachmentGeneration += 1;
169
+ attachmentRevision += 1;
148
170
  attachments = [];
149
171
  pendingAttachmentKeys.clear();
172
+ pendingAttachmentBytes = 0;
150
173
  renderComposerAttachments();
174
+ if (options.notify !== false) onChange();
151
175
  }
152
176
 
153
177
  function removeAttachment(id) {
178
+ const previousLength = attachments.length;
154
179
  attachments = attachments.filter(attachment => attachment.id !== id);
155
180
  renderComposerAttachments();
181
+ if (attachments.length !== previousLength) { attachmentRevision += 1; onChange(); }
156
182
  }
157
183
 
158
184
  function isDuplicateAttachmentFile(dedupeKey) {
@@ -260,14 +286,36 @@
260
286
  if (!usable.length) {
261
287
  return 0;
262
288
  }
289
+ attachmentRevision += 1;
263
290
  attachments = usable.map(item => ({ ...item }));
264
291
  renderComposerAttachments();
265
292
  return attachments.length;
266
293
  }
267
294
 
295
+ // Only a just-rejected local-skill submission uses this in-memory recovery.
296
+ // It keeps the normal composer limits, without durable-draft truncation.
297
+ function restoreSubmittedAttachments(value, expectedRevision) {
298
+ if (expectedRevision !== attachmentRevision || attachments.length || pendingAttachmentBytes || pendingAttachmentKeys.size) return false;
299
+ if (!Array.isArray(value) || value.length > maxAttachments) return false;
300
+ let total = 0;
301
+ for (const item of value) {
302
+ const size = Number(item?.size);
303
+ if (!item?.name || typeof item.contentBase64 !== 'string' || !Number.isFinite(size) || size < 0 || size > maxAttachmentBytes) return false;
304
+ total += size;
305
+ }
306
+ if (total > maxAttachmentTotalBytes) return false;
307
+ attachments = value.map((item, index) => ({ ...item, id: `restored-${Date.now()}-${index}`, dedupeKey: buildAttachmentDedupeKey(item) }));
308
+ attachmentRevision += 1;
309
+ renderComposerAttachments();
310
+ onChange();
311
+ return true;
312
+ }
313
+
268
314
  return {
269
315
  serializePersistableAttachments,
270
316
  restorePersistedAttachments,
317
+ restoreSubmittedAttachments,
318
+ getRevision: () => attachmentRevision,
271
319
  handlePaste,
272
320
  handleDragOver,
273
321
  handleDragLeave,
@@ -496,13 +544,13 @@
496
544
  /\.(?:png|jpe?g|gif|webp|bmp|svg)$/i.test(String(attachment.name || ''));
497
545
  }
498
546
 
499
- function readFileAsDataUrl(file) {
547
+ function readFileAsDataUrl(file, tx = en => en) {
500
548
  return new Promise((resolve, reject) => {
501
549
  const reader = new FileReader();
502
550
  reader.onload = () => {
503
551
  resolve(String(reader.result || ''));
504
552
  };
505
- reader.onerror = () => reject(reader.error || new Error('Could not read selected file'));
553
+ reader.onerror = () => reject(new Error(tx('Could not read selected file.', '无法读取所选文件。'), { cause: reader.error }));
506
554
  reader.readAsDataURL(file);
507
555
  });
508
556
  }
@@ -132,6 +132,7 @@
132
132
  `;
133
133
 
134
134
  bindEvents(instance);
135
+ bindMenuLayout(instance);
135
136
 
136
137
  return {
137
138
  setRunning: running => setRunning(instance, running),
@@ -193,6 +194,22 @@
193
194
  instance.listeners.push({ target, type, listener });
194
195
  }
195
196
 
197
+ function bindMenuLayout(instance) {
198
+ const view = instance.container.ownerDocument?.defaultView || window;
199
+ const update = () => instance.callbacks.onMenuLayoutChange?.();
200
+ bind(instance, view, 'resize', update);
201
+ bind(instance, view.visualViewport, 'resize', update);
202
+ bind(instance, view.visualViewport, 'scroll', update);
203
+ const form = instance.container.querySelector('[data-composer-form]');
204
+ if (form && typeof view.ResizeObserver === 'function') {
205
+ instance.menuResizeObserver = new view.ResizeObserver(update);
206
+ instance.menuResizeObserver.observe(form);
207
+ instance.menuResizeObserver.observe(instance.container);
208
+ const panel = instance.container.closest?.('#codex-overleaf-panel');
209
+ if (panel && panel !== instance.container) instance.menuResizeObserver.observe(panel);
210
+ }
211
+ }
212
+
196
213
  function setRunning(target, running) {
197
214
  const instance = target?._instance || target;
198
215
  const runButton = instance?.container?.querySelector('[data-run]');
@@ -276,6 +293,7 @@
276
293
  }
277
294
 
278
295
  function destroy(instance) {
296
+ instance.menuResizeObserver?.disconnect();
279
297
  for (const { target, type, listener } of instance.listeners.splice(0)) {
280
298
  target.removeEventListener?.(type, listener);
281
299
  }