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.
- package/README.md +38 -26
- package/README.zh-CN.md +39 -27
- package/extension/bootstrap/background.js +52 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.html +1 -0
- package/extension/bootstrap/popupFallback.js +55 -0
- package/extension/bootstrap/updateStatus.js +1 -0
- package/extension/src/background.js +206 -53
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +74 -14
- package/extension/src/backgroundUpdateCoordinator.js +118 -21
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +8 -2
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +93 -45
- package/extension/src/content/composerPanel.js +18 -0
- package/extension/src/content/contentRuntime.js +661 -676
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +30 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +6879 -2786
- package/extension/src/content/generated/content.bundle.meta.json +11 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +6 -0
- package/extension/src/content/localSkillsPanel.js +102 -12
- package/extension/src/content/markdownText.js +5 -6
- package/extension/src/content/modelPicker.js +7 -4
- package/extension/src/content/nativeCompatibilityController.js +45 -20
- package/extension/src/content/otWarmMirror.js +4 -5
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +15 -2
- package/extension/src/content/panelMaintenance.js +40 -12
- package/extension/src/content/panelRenderer.js +6 -0
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +39 -2
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +71 -13
- package/extension/src/content/recentProjects.js +11 -5
- package/extension/src/content/runActivitySummary.js +40 -6
- package/extension/src/content/runCompletionMeta.js +344 -0
- package/extension/src/content/runController.js +37 -14
- package/extension/src/content/runFailureNotice.js +21 -12
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runQueueScheduler.js +148 -42
- package/extension/src/content/runRecoveryActions.js +146 -0
- package/extension/src/content/runResultActions.js +8 -125
- package/extension/src/content/runReviewController.js +574 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +146 -153
- package/extension/src/content/scopedPersistencePanelState.js +51 -6
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +140 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +1 -1
- package/extension/src/content/settingsPanel.js +7 -3
- package/extension/src/content/settingsWorkbench.js +5 -0
- package/extension/src/content/sharedSessionViewSync.js +45 -8
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +60 -8
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +82 -73
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/ownedDirectoryCleanup.js +94 -0
- package/extension/src/page/referenceProjects.js +1 -0
- package/extension/src/page/reviewContext.js +113 -0
- package/extension/src/page/saveState.js +6 -4
- package/extension/src/page/snapshotRouter.js +84 -39
- package/extension/src/page/textFileCreator.js +105 -25
- package/extension/src/page/trackedChangeCapture.js +12 -2
- package/extension/src/page/trackedChangeOwnership.js +19 -1
- package/extension/src/page/trackedChangeReplay.js +41 -21
- package/extension/src/page/trackedChangesLifecycle.js +217 -199
- package/extension/src/page/treeOperations.js +7 -10
- package/extension/src/page/updateSafetyProbe.js +41 -0
- package/extension/src/page/writebackReceiptJournal.js +2 -0
- package/extension/src/page/writebackRouter.js +80 -79
- package/extension/src/pageBridge.js +19 -3
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +23 -0
- package/extension/src/shared/auditRecords.js +2 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +79 -57
- package/extension/src/shared/composerAttachmentDraft.js +151 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +309 -3
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +298 -2
- package/extension/src/shared/managedUpdateProjection.js +2 -2
- package/extension/src/shared/nativeRequestIdentity.js +1 -0
- package/extension/src/shared/pageRpcContract.js +5 -4
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- package/extension/src/shared/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +72 -27
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +37 -14
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +118 -13
- package/extension/src/shared/storageDb.js +58 -26
- package/extension/src/shared/storageMigration.js +71 -111
- package/extension/src/shared/storageRunActions.js +209 -26
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +136 -13
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +1 -1
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +38 -38
- package/extension/styles/panel.css +19 -5
- package/native-host/src/anthropicBridgeRequest.js +4 -2
- package/native-host/src/anthropicBridgeResponse.js +6 -2
- package/native-host/src/anthropicMessagesBridge.js +24 -16
- package/native-host/src/bootstrapMigration.js +296 -0
- package/native-host/src/bootstrapMigrationContract.js +101 -0
- package/native-host/src/bootstrapUpdate.js +233 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeResponse.js +10 -0
- package/native-host/src/chatCompletionsBridge.js +21 -15
- package/native-host/src/codexConfigPaths.js +106 -0
- package/native-host/src/codexHome.js +66 -73
- package/native-host/src/codexHomeScope.js +124 -0
- package/native-host/src/codexInheritedAuth.js +90 -0
- package/native-host/src/codexLocalSkills.js +301 -0
- package/native-host/src/codexPromptAssembly.js +31 -12
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +110 -183
- package/native-host/src/codexSessionTiming.js +18 -8
- package/native-host/src/codexSkillIsolation.js +170 -0
- package/native-host/src/codexThreadFork.js +19 -3
- package/native-host/src/index.js +19 -7
- package/native-host/src/managedInstall.js +110 -35
- package/native-host/src/managedInstallRecovery.js +140 -0
- package/native-host/src/managedRuntimeEvidence.js +57 -0
- package/native-host/src/mirrorWorkspace.js +92 -22
- package/native-host/src/nativeDoctor.js +15 -0
- package/native-host/src/nativeHelperLifecycle.js +21 -0
- package/native-host/src/nativeHostRegistration.js +33 -0
- package/native-host/src/nativeRunLifecycle.js +71 -0
- package/native-host/src/processStopBarrier.js +42 -7
- package/native-host/src/projectPathPlatform.js +30 -0
- package/native-host/src/providerHttp.js +80 -0
- package/native-host/src/providerProfile.js +4 -2
- package/native-host/src/providerReasoning.js +2 -7
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/providerStore.js +72 -14
- package/native-host/src/responsesPassthroughBridge.js +266 -52
- package/native-host/src/runtimeInstaller.js +21 -1
- package/native-host/src/selectionScope.js +36 -8
- package/native-host/src/subagentAdmission.js +26 -0
- package/native-host/src/subagentBroker.js +79 -66
- package/native-host/src/subagentTelemetry.js +19 -10
- package/native-host/src/subagentWorkspacePath.js +5 -2
- package/native-host/src/taskRunnerRuntime.js +77 -54
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +297 -39
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +1 -1
- 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
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
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
|
}
|