codex-overleaf-link 2.6.0 → 2.6.5
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 +9 -9
- package/README.zh-CN.md +9 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +70 -0
- package/extension/src/content/composerAttachments.js +35 -8
- package/extension/src/content/contentRuntime.js +200 -51
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +129 -42
- package/extension/src/content/generated/content.bundle.js +1283 -331
- package/extension/src/content/generated/content.bundle.meta.json +3 -3
- package/extension/src/content/localSkillsPanel.js +4 -2
- package/extension/src/content/markdownText.js +11 -9
- package/extension/src/content/modelPicker.js +28 -19
- package/extension/src/content/nativeCompatibilityController.js +23 -9
- package/extension/src/content/pageBridgeClient.js +5 -5
- package/extension/src/content/panelMaintenance.js +4 -0
- package/extension/src/content/panelRenderer.js +4 -1
- package/extension/src/content/providerSettingsDialog.js +26 -18
- package/extension/src/content/recentProjects.js +26 -16
- package/extension/src/content/runQueueScheduler.js +19 -9
- package/extension/src/content/runResultActions.js +51 -4
- package/extension/src/content/runReviewController.js +9 -3
- package/extension/src/content/scopedPersistencePanelState.js +34 -25
- package/extension/src/content/selectionContextView.js +30 -3
- package/extension/src/content/sharedSessionViewSync.js +9 -1
- package/extension/src/content/writebackOrchestrator.js +266 -50
- package/extension/src/content/writingStyleSettings.js +70 -17
- package/extension/src/page/overleafEditor.js +12 -3
- package/extension/src/page/overleafProjectSnapshot.js +56 -3
- package/extension/src/page/snapshotRouter.js +75 -66
- package/extension/src/page/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +152 -40
- package/extension/src/pageBridge.js +17 -7
- package/extension/src/shared/agentTranscript.js +23 -2
- package/extension/src/shared/auditRecords.js +1 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/composerAttachmentDraft.js +22 -7
- package/extension/src/shared/failureReasons.js +15 -0
- package/extension/src/shared/i18n.js +16 -0
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/projectFiles.js +2 -1
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
- package/extension/src/shared/staleGuard.js +58 -0
- package/extension/src/shared/storageMigration.js +104 -1
- package/extension/styles/panel.css +191 -63
- package/native-host/src/codexSessionRunner.js +1 -0
- package/native-host/src/debugLog.js +27 -3
- package/native-host/src/index.js +42 -11
- package/native-host/src/localSkills.js +1 -1
- package/native-host/src/mirrorWorkspace.js +47 -3
- package/native-host/src/nativeEnvironment.js +27 -5
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
- package/native-host/src/taskRunnerRuntime.js +11 -3
- package/native-host/src/updateManager.js +24 -6
- package/native-host/src/writingStyleRuntime.js +1 -1
- package/package.json +1 -1
|
@@ -697,6 +697,8 @@
|
|
|
697
697
|
showPluginConfirm, formatOperationFiles, getAppliedEntries, getSkippedEntries
|
|
698
698
|
});
|
|
699
699
|
const writebackOrchestrator = Modules.WritebackOrchestrator.create({
|
|
700
|
+
mirrorFenceStore: root.CodexOverleafModuleRegistry.resolve('ScopedPersistenceBrowserAdapter'),
|
|
701
|
+
getAccountScopeId: () => cachedAccountScopeId,
|
|
700
702
|
tr,
|
|
701
703
|
tx,
|
|
702
704
|
getLocale,
|
|
@@ -844,20 +846,19 @@
|
|
|
844
846
|
});
|
|
845
847
|
// v1.8.1 D3: the dashboard was a one-shot render — another tab finishing a
|
|
846
848
|
// run, clearing history or creating sessions never showed up. Refresh it
|
|
847
|
-
// (debounced)
|
|
848
|
-
// arrives while the dashboard view is active.
|
|
849
|
+
// (debounced) on visibility, cross-tab run signals, and shared-history changes.
|
|
849
850
|
let dashboardRefreshTimer = null;
|
|
850
851
|
function refreshDashboardSoon() {
|
|
851
|
-
if (panel?.dataset.view !== 'recent-projects') {
|
|
852
|
+
if (panel?.dataset.view !== 'recent-projects' || document.visibilityState !== 'visible') {
|
|
852
853
|
return;
|
|
853
854
|
}
|
|
854
855
|
clearTimeout(dashboardRefreshTimer);
|
|
855
856
|
dashboardRefreshTimer = setTimeout(() => {
|
|
856
|
-
if (panel?.dataset.view !== 'recent-projects') {
|
|
857
|
+
if (panel?.dataset.view !== 'recent-projects' || document.visibilityState !== 'visible') {
|
|
857
858
|
return;
|
|
858
859
|
}
|
|
859
860
|
// Never clobber an in-progress inline rename (fleet P2) — retry later.
|
|
860
|
-
if (panel.querySelector('.recent-projects-
|
|
861
|
+
if (panel.querySelector('.recent-projects-session-rename-input')) {
|
|
861
862
|
refreshDashboardSoon();
|
|
862
863
|
return;
|
|
863
864
|
}
|
|
@@ -865,11 +866,7 @@
|
|
|
865
866
|
const expanded = Array.from(panel.querySelectorAll('[data-project-row-wrap][data-expanded="true"]'))
|
|
866
867
|
.map(wrap => wrap.getAttribute('data-project-row-wrap'))
|
|
867
868
|
.filter(Boolean);
|
|
868
|
-
|
|
869
|
-
recentProjects.renderRecentProjectsVariant({
|
|
870
|
-
restoreExpanded: expanded,
|
|
871
|
-
restoreScrollTop: rootEl ? rootEl.scrollTop : undefined
|
|
872
|
-
}).catch(() => { /* keep last render */ });
|
|
869
|
+
recentProjects.refreshDashboardView({ restoreExpanded: expanded }).catch(() => { /* keep last render */ });
|
|
873
870
|
}, 400);
|
|
874
871
|
}
|
|
875
872
|
document.addEventListener('visibilitychange', () => {
|
|
@@ -1458,7 +1455,7 @@
|
|
|
1458
1455
|
primeCodexLocalSkillsCatalog().catch(() => { /* retry through settings, slash, or submission */ });
|
|
1459
1456
|
}
|
|
1460
1457
|
|
|
1461
|
-
PanelRenderer.setVisible(
|
|
1458
|
+
PanelRenderer.setVisible(panelRendererInstance, true);
|
|
1462
1459
|
}
|
|
1463
1460
|
|
|
1464
1461
|
// Panel prefetch and first submission share the same official catalog read.
|
|
@@ -2153,9 +2150,11 @@
|
|
|
2153
2150
|
}
|
|
2154
2151
|
|
|
2155
2152
|
let focusFiles = [...executionSnapshot.focusFiles];
|
|
2153
|
+
const mirrorOwner = currentRunView, mirrorRouteGeneration = spaRouteGeneration;
|
|
2156
2154
|
const warmStart = await awaitRunStep(resolveWarmRunStart({ focusFiles, mode: submittedMode }));
|
|
2155
|
+
const mirrorFence = await awaitRunStep(writebackOrchestrator.refreshMirrorReuseFence(mirrorOwner.runProjectId, mirrorOwner.runAccountScopeId));
|
|
2157
2156
|
let project = warmStart.project || null;
|
|
2158
|
-
let useExistingMirror = warmStart.useExistingMirror;
|
|
2157
|
+
let useExistingMirror = warmStart.useExistingMirror && !mirrorFence;
|
|
2159
2158
|
let fileOverlays = warmStart.fileOverlays || null;
|
|
2160
2159
|
let otWarmStart = warmStart.otWarmStart === true;
|
|
2161
2160
|
if (!useExistingMirror) {
|
|
@@ -2164,6 +2163,18 @@
|
|
|
2164
2163
|
status: 'running'
|
|
2165
2164
|
});
|
|
2166
2165
|
project = await awaitRunStep(getRunProjectSnapshot());
|
|
2166
|
+
if (mirrorFence) {
|
|
2167
|
+
const assertCurrent = () => {
|
|
2168
|
+
throwIfRunCancellationRequested();
|
|
2169
|
+
if (mirrorOwner !== currentRunView || mirrorRouteGeneration !== spaRouteGeneration
|
|
2170
|
+
|| mirrorOwner.runProjectId !== getCurrentProjectId()
|
|
2171
|
+
|| mirrorOwner.runAccountScopeId !== cachedAccountScopeId) throw createRunCancellationError();
|
|
2172
|
+
};
|
|
2173
|
+
await awaitRunStep(writebackOrchestrator.syncFencedMirror(mirrorOwner.runProjectId, project, {
|
|
2174
|
+
fence: mirrorFence, assertCurrent,
|
|
2175
|
+
sendNative: payload => nativeCompatibilityController.sendBackgroundNative(payload, { assertCurrent })
|
|
2176
|
+
}));
|
|
2177
|
+
}
|
|
2167
2178
|
}
|
|
2168
2179
|
throwIfRunCancellationRequested();
|
|
2169
2180
|
if (!useExistingMirror) {
|
|
@@ -2332,8 +2343,8 @@
|
|
|
2332
2343
|
submittedMode
|
|
2333
2344
|
})));
|
|
2334
2345
|
|
|
2335
|
-
//
|
|
2336
|
-
if (!response.ok && response.error?.code
|
|
2346
|
+
// A stale mirror or contradictory editor overlay requires a fresh read.
|
|
2347
|
+
if (!response.ok && ['mirror_stale', 'document_identity_mismatch'].includes(response.error?.code) && useExistingMirror) {
|
|
2337
2348
|
appendRunEvent({ title: tr('warmMirrorStaleRetryTitle'), status: 'running' });
|
|
2338
2349
|
const staleRetry = await awaitRunStep(prepareMirrorStaleRetry({
|
|
2339
2350
|
project: await awaitRunStep(getRunProjectSnapshot()),
|
|
@@ -2601,7 +2612,7 @@
|
|
|
2601
2612
|
});
|
|
2602
2613
|
runSettlementPersistence.applyToCurrentRun(syncOutcome.settlement);
|
|
2603
2614
|
if (runCancellationRequested) {
|
|
2604
|
-
throw createRunCancellationError();
|
|
2615
|
+
throw Object.assign(createRunCancellationError(), { writebackOutcome: syncOutcome });
|
|
2605
2616
|
}
|
|
2606
2617
|
await finalizeAuditRecord(runAuditDraft, {
|
|
2607
2618
|
...(syncOutcome.audit || {}),
|
|
@@ -2712,7 +2723,10 @@
|
|
|
2712
2723
|
changedDocument: false, evidence: { declinedChoice: true }
|
|
2713
2724
|
}),
|
|
2714
2725
|
nextStep: tr('failureReason_reviewing_unavailable_cancelled_next') });
|
|
2715
|
-
} else
|
|
2726
|
+
} else {
|
|
2727
|
+
runSettlementPersistence.applyToCurrentRun(error.writebackOutcome?.settlement);
|
|
2728
|
+
appendRunCancelledReport(error.writebackOutcome);
|
|
2729
|
+
}
|
|
2716
2730
|
await finishRunView(tx('Cancelled', '已取消'), 'cancelled');
|
|
2717
2731
|
void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
|
|
2718
2732
|
return;
|
|
@@ -2800,9 +2814,15 @@
|
|
|
2800
2814
|
nativeChannel.clearActiveRequest();
|
|
2801
2815
|
stopRunElapsedTick();
|
|
2802
2816
|
await flushQueuedSaveState().catch(() => {});
|
|
2817
|
+
// The card is already settled. Retain the owner while Native stops so
|
|
2818
|
+
// neither ordinary promotion nor a deferred session start races its lock.
|
|
2819
|
+
if (settlement.continueQueue) {
|
|
2820
|
+
settlement.continueQueue = await waitForCancelledNativeRun(settledView);
|
|
2821
|
+
}
|
|
2803
2822
|
currentRunView = null;
|
|
2804
2823
|
runCancellationRequested = false;
|
|
2805
2824
|
runCancellationController = null;
|
|
2825
|
+
syncComposerSendAvailability();
|
|
2806
2826
|
activeTurnControl.release(settlement.requestId);
|
|
2807
2827
|
if (isExperimentalOtEnabled()) {
|
|
2808
2828
|
try { await resumeOtWarmMirror('run-settled'); }
|
|
@@ -3240,7 +3260,7 @@
|
|
|
3240
3260
|
if (state?.activeSessionId !== sessionId || !isProjectEditorRoute(window.location)) return false;
|
|
3241
3261
|
if (sessionManager.isTransitioning()) {
|
|
3242
3262
|
deferredSessionStart = { sessionId, scope: getComposerDraftScope() };
|
|
3243
|
-
return
|
|
3263
|
+
return 'session_transition';
|
|
3244
3264
|
}
|
|
3245
3265
|
return true;
|
|
3246
3266
|
}
|
|
@@ -3286,18 +3306,21 @@
|
|
|
3286
3306
|
// codex_cancelled instead of grinding through the rest of the queue.
|
|
3287
3307
|
// Without the page-side signal, a cancel during writeback waits up to
|
|
3288
3308
|
// the applyOperations content-side timeout (30s) before the run settles.
|
|
3289
|
-
const activeRequestId =
|
|
3309
|
+
const activeRequestId = currentRunView.nativeRequestId;
|
|
3290
3310
|
const projectKey = currentRunView?.runProjectId || getCurrentProjectId() || '';
|
|
3291
3311
|
|
|
3292
3312
|
const cancelTargets = [];
|
|
3293
|
-
if (activeRequestId
|
|
3313
|
+
if (activeRequestId != null && activeRequestId !== '') {
|
|
3294
3314
|
cancelTargets.push(sendBackgroundNative({
|
|
3295
3315
|
method: 'codex.cancel',
|
|
3296
3316
|
params: {
|
|
3297
|
-
requestId: activeRequestId
|
|
3317
|
+
requestId: activeRequestId,
|
|
3298
3318
|
projectKey: projectKey || undefined
|
|
3299
3319
|
}
|
|
3300
3320
|
}));
|
|
3321
|
+
} else {
|
|
3322
|
+
// Preparation has no Native run to cancel; the local signal prevents dispatch.
|
|
3323
|
+
cancelTargets.push(Promise.resolve(null));
|
|
3301
3324
|
}
|
|
3302
3325
|
// The page-side cancel is best-effort — it only matters if a writeback
|
|
3303
3326
|
// is currently in flight. Swallow errors / unavailable bridge so the
|
|
@@ -3384,6 +3407,23 @@
|
|
|
3384
3407
|
});
|
|
3385
3408
|
}
|
|
3386
3409
|
|
|
3410
|
+
async function waitForCancelledNativeRun(runView) {
|
|
3411
|
+
const pending = runView?.pendingNativeRun;
|
|
3412
|
+
if (!pending) return true;
|
|
3413
|
+
const CANCELLED_NATIVE_SETTLEMENT_TIMEOUT_MS = 20000;
|
|
3414
|
+
let timer;
|
|
3415
|
+
try {
|
|
3416
|
+
return await Promise.race([
|
|
3417
|
+
pending.then(() => true, () => true),
|
|
3418
|
+
new Promise(resolve => {
|
|
3419
|
+
timer = setTimeout(() => resolve(false), CANCELLED_NATIVE_SETTLEMENT_TIMEOUT_MS);
|
|
3420
|
+
})
|
|
3421
|
+
]);
|
|
3422
|
+
} finally {
|
|
3423
|
+
clearTimeout(timer);
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3387
3427
|
function isRunCancellationError(error = {}) {
|
|
3388
3428
|
return error.code === 'codex_cancelled' || /cancelled by the user|was cancelled/i.test(error.message || '');
|
|
3389
3429
|
}
|
|
@@ -3547,21 +3587,26 @@
|
|
|
3547
3587
|
method: 'codex.run',
|
|
3548
3588
|
params
|
|
3549
3589
|
}, compatibilityGate.compatibility));
|
|
3550
|
-
if (
|
|
3551
|
-
|
|
3552
|
-
|
|
3590
|
+
if (owner) {
|
|
3591
|
+
owner.nativeRequestId = tracked.id;
|
|
3592
|
+
// Keep the actual response separate from awaitRunStep's local abort race.
|
|
3593
|
+
owner.pendingNativeRun = tracked.promise;
|
|
3594
|
+
const record = findRunRecord(owner.recordId, owner.sessionId);
|
|
3553
3595
|
if (record) {
|
|
3554
3596
|
record.nativeRequestId = tracked.id;
|
|
3555
3597
|
}
|
|
3556
3598
|
activeTurnControl.bind({
|
|
3557
3599
|
requestId: tracked.id,
|
|
3558
|
-
projectKey:
|
|
3559
|
-
clientRunId:
|
|
3560
|
-
sessionId:
|
|
3600
|
+
projectKey: owner.runProjectId,
|
|
3601
|
+
clientRunId: owner.recordId,
|
|
3602
|
+
sessionId: owner.sessionId
|
|
3561
3603
|
});
|
|
3562
3604
|
saveStateSoon();
|
|
3563
3605
|
}
|
|
3564
3606
|
return tracked.promise.finally(() => {
|
|
3607
|
+
if (owner?.pendingNativeRun === tracked.promise) {
|
|
3608
|
+
owner.pendingNativeRun = null;
|
|
3609
|
+
}
|
|
3565
3610
|
if (currentRunView?.nativeRequestId === tracked.id) {
|
|
3566
3611
|
currentRunView.activeTurn = null;
|
|
3567
3612
|
renderPendingInputs();
|
|
@@ -3704,8 +3749,8 @@
|
|
|
3704
3749
|
}).run(options);
|
|
3705
3750
|
}
|
|
3706
3751
|
|
|
3707
|
-
function appendRunCancelledReport() {
|
|
3708
|
-
const hasQueuedFollowUp = (getActiveSession(state)?.pendingInputs || [])
|
|
3752
|
+
function appendRunCancelledReport(writebackOutcome = {}, continueQueue = true) {
|
|
3753
|
+
const hasQueuedFollowUp = continueQueue && (getActiveSession(state)?.pendingInputs || [])
|
|
3709
3754
|
.some(item => item?.status === 'queued');
|
|
3710
3755
|
appendRunEvent({
|
|
3711
3756
|
title: tx('Current Codex task was cancelled.', '已中断当前 Codex 任务。'),
|
|
@@ -3715,8 +3760,9 @@
|
|
|
3715
3760
|
appendCompletionReport({ conclusionKey: 'cancelled',
|
|
3716
3761
|
conclusion: tx('This run was cancelled. It did not continue syncing or writing to Overleaf.', '这轮已中断,没有继续同步或写入 Overleaf。'),
|
|
3717
3762
|
status: 'cancelled',
|
|
3718
|
-
operations: [],
|
|
3719
|
-
applyResults: [],
|
|
3763
|
+
operations: writebackOutcome.operations || [],
|
|
3764
|
+
applyResults: writebackOutcome.applied ? [writebackOutcome.applied] : [],
|
|
3765
|
+
saveVerification: writebackOutcome.saveVerification,
|
|
3720
3766
|
nextStep: hasQueuedFollowUp
|
|
3721
3767
|
? tx('The first queued follow-up will start next.', '接下来会自动启动队首的后续任务。')
|
|
3722
3768
|
: tx('Edit the task and run again.', '可以修改任务后重新运行。')
|
|
@@ -3741,17 +3787,21 @@
|
|
|
3741
3787
|
const running = typeof runningOverride === 'boolean'
|
|
3742
3788
|
? runningOverride
|
|
3743
3789
|
: Boolean(currentRunView);
|
|
3790
|
+
const runningInActiveSession = running && (!currentRunView || currentRunView.sessionId === state?.activeSessionId);
|
|
3744
3791
|
const transitioning = sessionManager.isTransitioning();
|
|
3745
|
-
const action =
|
|
3792
|
+
const action = runningInActiveSession && (!hasInput || transitioning) ? 'cancel' : 'send';
|
|
3793
|
+
const attachmentsLoading = composerAttachmentController.hasPendingReads();
|
|
3746
3794
|
const label = action === 'cancel'
|
|
3747
3795
|
? tr('cancelRun')
|
|
3748
|
-
:
|
|
3749
|
-
? tr('
|
|
3750
|
-
:
|
|
3796
|
+
: attachmentsLoading
|
|
3797
|
+
? tr('attachmentsLoading')
|
|
3798
|
+
: running && hasInput
|
|
3799
|
+
? tr('queueNextInput')
|
|
3800
|
+
: tr('send');
|
|
3751
3801
|
runButton.dataset.action = action;
|
|
3752
3802
|
runButton.disabled = action === 'cancel'
|
|
3753
3803
|
? runCancellationRequested
|
|
3754
|
-
: !hasInput || transitioning;
|
|
3804
|
+
: !hasInput || transitioning || attachmentsLoading;
|
|
3755
3805
|
runButton.title = label;
|
|
3756
3806
|
runButton.setAttribute('aria-label', label);
|
|
3757
3807
|
if (currentRunView?.writebackRetry && hasInput && action !== 'cancel') runButton.disabled = true;
|
|
@@ -3762,6 +3812,11 @@
|
|
|
3762
3812
|
if (event.key === 'Enter' && !event.isComposing && event.keyCode !== 229) event.preventDefault();
|
|
3763
3813
|
return;
|
|
3764
3814
|
}
|
|
3815
|
+
if (composerAttachmentController.hasPendingReads() && event.key === 'Enter'
|
|
3816
|
+
&& !event.shiftKey && !event.isComposing && event.keyCode !== 229) {
|
|
3817
|
+
event.preventDefault();
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3765
3820
|
if (handleSlashMenuKeydown(event)) {
|
|
3766
3821
|
return;
|
|
3767
3822
|
}
|
|
@@ -3809,7 +3864,8 @@
|
|
|
3809
3864
|
if (!isCodexOverleafSkillEnabled(id)) {
|
|
3810
3865
|
continue;
|
|
3811
3866
|
}
|
|
3812
|
-
const title =
|
|
3867
|
+
const title = id === 'reviewer-response' ? tx('Reviewer response', '审稿回复')
|
|
3868
|
+
: String(skill?.title || skill?.name || id).trim().slice(0, 80) || id;
|
|
3813
3869
|
commands.push({
|
|
3814
3870
|
id: `skill:${id}`,
|
|
3815
3871
|
kind: 'codex-overleaf-skill',
|
|
@@ -4529,6 +4585,8 @@
|
|
|
4529
4585
|
Bridge: Modules.SharedSessionBridge, StorageDb: Modules.StorageDb,
|
|
4530
4586
|
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
|
|
4531
4587
|
getState: () => state, setState: next => { state = next; },
|
|
4588
|
+
isDashboard: () => panel?.dataset.view === 'recent-projects',
|
|
4589
|
+
refreshDashboard: refreshDashboardSoon,
|
|
4532
4590
|
// An in-flight Accept/Undo keeps its view until it settles; refresh applies afterwards.
|
|
4533
4591
|
isBusy: () => Boolean(pendingDraftSave || readLiveRunViewForSaveStateGuard() || trackedChangeInFlight.size
|
|
4534
4592
|
|| runReviewController?.hasActiveReview?.() || document.querySelector('dialog[open]')),
|
|
@@ -5545,8 +5603,9 @@
|
|
|
5545
5603
|
};
|
|
5546
5604
|
}
|
|
5547
5605
|
|
|
5548
|
-
async function getRunProjectSnapshot() {
|
|
5606
|
+
async function getRunProjectSnapshot(expectedProjectId = getCurrentProjectId()) {
|
|
5549
5607
|
const project = await callPageBridge('getProjectSnapshot', {
|
|
5608
|
+
projectId: expectedProjectId,
|
|
5550
5609
|
force: true,
|
|
5551
5610
|
maxAgeMs: 0,
|
|
5552
5611
|
preferLightweight: true,
|
|
@@ -5558,11 +5617,12 @@
|
|
|
5558
5617
|
zipTimeoutMs: RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
|
|
5559
5618
|
focusFiles: getActiveFocusFiles()
|
|
5560
5619
|
});
|
|
5561
|
-
const sanitizedProject = sanitizeRunProjectSnapshot(project);
|
|
5620
|
+
const sanitizedProject = sanitizeRunProjectSnapshot(project, expectedProjectId);
|
|
5562
5621
|
if (sanitizedProject?.capabilities?.fullProjectSnapshot || sanitizedProject?.capabilities?.pathIdentityComplete === false) {
|
|
5563
5622
|
return sanitizedProject;
|
|
5564
5623
|
}
|
|
5565
5624
|
const pageStateProject = await callPageBridge('getProjectSnapshot', {
|
|
5625
|
+
projectId: expectedProjectId,
|
|
5566
5626
|
force: true,
|
|
5567
5627
|
maxAgeMs: 0,
|
|
5568
5628
|
preferLightweight: true,
|
|
@@ -5572,17 +5632,21 @@
|
|
|
5572
5632
|
includeBinaryFiles: false,
|
|
5573
5633
|
focusFiles: getActiveFocusFiles()
|
|
5574
5634
|
});
|
|
5575
|
-
const sanitizedPageStateProject = sanitizeRunProjectSnapshot(pageStateProject);
|
|
5635
|
+
const sanitizedPageStateProject = sanitizeRunProjectSnapshot(pageStateProject, expectedProjectId);
|
|
5576
5636
|
if (sanitizedPageStateProject?.capabilities?.fullProjectSnapshot) {
|
|
5577
5637
|
return sanitizedPageStateProject;
|
|
5578
5638
|
}
|
|
5579
5639
|
return sanitizedProject;
|
|
5580
5640
|
}
|
|
5581
5641
|
|
|
5582
|
-
function sanitizeRunProjectSnapshot(project) {
|
|
5642
|
+
function sanitizeRunProjectSnapshot(project, expectedProjectId = project?.id) {
|
|
5583
5643
|
if (!project || typeof project !== 'object') return project;
|
|
5644
|
+
if (project.id !== expectedProjectId) return { ...project, files: [], capabilities: { ...project.capabilities,
|
|
5645
|
+
fullProjectSnapshot: false, skipped: [{ path: '', reason: 'document_identity_mismatch' }] } };
|
|
5584
5646
|
const normalize = Modules.ProjectFiles.normalizeSafeProjectPath;
|
|
5585
|
-
const files = Array.isArray(project.files) ? project.files
|
|
5647
|
+
const files = Array.isArray(project.files) ? project.files.map(file => file?.documentIdentity === undefined ? file
|
|
5648
|
+
: { ...file, documentIdentity: snapshotDocumentIdentity(file.documentIdentity, file.path, project.id) }) : [];
|
|
5649
|
+
project = { ...project, files };
|
|
5586
5650
|
const counts = new Map();
|
|
5587
5651
|
for (const file of files) {
|
|
5588
5652
|
const path = normalize(file?.path);
|
|
@@ -5619,12 +5683,16 @@
|
|
|
5619
5683
|
// --- Warm Mirror Controller ---
|
|
5620
5684
|
|
|
5621
5685
|
async function getMirrorFreshness() {
|
|
5686
|
+
const projectId = getCurrentProjectId(), accountScopeId = cachedAccountScopeId;
|
|
5687
|
+
if (await writebackOrchestrator.refreshMirrorReuseFence(projectId, accountScopeId)) return null;
|
|
5688
|
+
if (getCurrentProjectId() !== projectId || cachedAccountScopeId !== accountScopeId) return null;
|
|
5622
5689
|
try {
|
|
5623
5690
|
const response = await sendBackgroundNative({
|
|
5624
5691
|
method: 'mirror.status',
|
|
5625
|
-
params: { projectId
|
|
5692
|
+
params: { projectId }
|
|
5626
5693
|
});
|
|
5627
|
-
|
|
5694
|
+
const fence = await writebackOrchestrator.refreshMirrorReuseFence(projectId, accountScopeId);
|
|
5695
|
+
if (response?.ok && !fence && getCurrentProjectId() === projectId && cachedAccountScopeId === accountScopeId) {
|
|
5628
5696
|
return response.result;
|
|
5629
5697
|
}
|
|
5630
5698
|
} catch (error) { /* fall through */ }
|
|
@@ -5655,7 +5723,7 @@
|
|
|
5655
5723
|
}
|
|
5656
5724
|
|
|
5657
5725
|
const mirrorStatus = await getMirrorFreshness();
|
|
5658
|
-
const mirrorHealth = Modules.MirrorHealth.classifyMirrorHealth(mirrorStatus);
|
|
5726
|
+
const mirrorHealth = Modules.MirrorHealth.classifyMirrorHealth(mirrorStatus || {});
|
|
5659
5727
|
if (!mirrorStatus?.exists || !mirrorHealth.reusable) {
|
|
5660
5728
|
return { useExistingMirror: false, reason: 'mirror_not_fresh', mirrorStatus };
|
|
5661
5729
|
}
|
|
@@ -5684,17 +5752,33 @@
|
|
|
5684
5752
|
return candidates
|
|
5685
5753
|
.filter(file =>
|
|
5686
5754
|
typeof file.content === 'string' &&
|
|
5687
|
-
Modules.ProjectFiles.isUsableProjectFileContent(file.content) &&
|
|
5755
|
+
(Modules.ProjectFiles.isUsableProjectFileContent(file.content) || file.content === '') &&
|
|
5688
5756
|
file.path &&
|
|
5689
5757
|
!seen.has(file.path) &&
|
|
5690
5758
|
seen.add(file.path)
|
|
5691
5759
|
)
|
|
5692
5760
|
.map(file => ({
|
|
5693
5761
|
path: file.path,
|
|
5694
|
-
content: file.content
|
|
5762
|
+
content: file.content,
|
|
5763
|
+
kind: 'text',
|
|
5764
|
+
source: file.source,
|
|
5765
|
+
...(file.documentIdentity !== undefined
|
|
5766
|
+
? { documentIdentity: snapshotDocumentIdentity(file.documentIdentity, file.path, project.id) } : {})
|
|
5695
5767
|
}));
|
|
5696
5768
|
}
|
|
5697
5769
|
|
|
5770
|
+
function snapshotDocumentIdentity(identity, path, projectId) {
|
|
5771
|
+
// Preserve invalid evidence as invalid, never silently downgrade to the
|
|
5772
|
+
// missing-evidence fallback. Native independently hashes the content.
|
|
5773
|
+
if (!identity || identity.version !== 1 || identity.source !== 'cm-ranges-v1'
|
|
5774
|
+
|| identity.path !== path || identity.projectId !== projectId
|
|
5775
|
+
|| typeof identity.documentId !== 'string' || !/^[a-f0-9]{24}$/.test(identity.documentId)
|
|
5776
|
+
|| typeof identity.targetDocumentId !== 'string' || !/^[a-f0-9]{24}$/.test(identity.targetDocumentId)
|
|
5777
|
+
|| typeof identity.contentSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(identity.contentSha256)) return null;
|
|
5778
|
+
return { version: 1, source: 'cm-ranges-v1', projectId, path,
|
|
5779
|
+
documentId: identity.documentId, targetDocumentId: identity.targetDocumentId, contentSha256: identity.contentSha256 };
|
|
5780
|
+
}
|
|
5781
|
+
|
|
5698
5782
|
function normalizeSnapshotPath(path) {
|
|
5699
5783
|
// Snapshot/overlay/focus arrays already carry canonical names from file chips.
|
|
5700
5784
|
return Modules.ProjectFiles.normalizeSafeProjectPath(path);
|
|
@@ -5962,6 +6046,8 @@
|
|
|
5962
6046
|
if (!accountScopeId || isScopedPersistenceIdentityFailure(error)) {
|
|
5963
6047
|
throw error;
|
|
5964
6048
|
}
|
|
6049
|
+
const fallbackProjectId = getCurrentProjectId();
|
|
6050
|
+
const fallbackGeneration = spaRouteGeneration;
|
|
5965
6051
|
const fallbackKey = buildScopedFallbackStorageKey(storageKey, accountScopeId);
|
|
5966
6052
|
const stored = await chrome.storage.local.get([fallbackKey]);
|
|
5967
6053
|
const fallback = stored[fallbackKey] || {};
|
|
@@ -5969,6 +6055,10 @@
|
|
|
5969
6055
|
fallback?.__codexOverleafCompactFallback === true
|
|
5970
6056
|
&& fallback.__codexOverleafFallbackAccountScopeId === accountScopeId
|
|
5971
6057
|
) {
|
|
6058
|
+
if (accountScopeId === cachedAccountScopeId && fallbackProjectId === getCurrentProjectId()
|
|
6059
|
+
&& fallbackGeneration === spaRouteGeneration) {
|
|
6060
|
+
Modules.StorageMigration.adoptProjectPreferenceBaseline(fallback, accountScopeId, fallbackProjectId);
|
|
6061
|
+
}
|
|
5972
6062
|
return {
|
|
5973
6063
|
mode: fallback.mode || '', providerId: fallback.providerId || 'builtin',
|
|
5974
6064
|
model: fallback.model || '',
|
|
@@ -6050,9 +6140,16 @@
|
|
|
6050
6140
|
compactState.customInstructionsByProject = state.customInstructionsByProject;
|
|
6051
6141
|
compactState.reviewingAvailabilityByProject = state.reviewingAvailabilityByProject;
|
|
6052
6142
|
compactState.governanceRulesByProject = state.governanceRulesByProject;
|
|
6143
|
+
const projectPreferenceBaseline = Migration.getProjectPreferenceBaseline(
|
|
6144
|
+
options?.draftScope?.accountScopeId || cachedAccountScopeId, projectId);
|
|
6053
6145
|
const stateSnapshot = typeof structuredClone === 'function'
|
|
6054
6146
|
? structuredClone(state)
|
|
6055
6147
|
: JSON.parse(JSON.stringify(state));
|
|
6148
|
+
const projectPreferenceWrite = Migration.captureProjectPreferenceWrite({
|
|
6149
|
+
state: stateSnapshot, baseline: projectPreferenceBaseline,
|
|
6150
|
+
accountScopeId: options?.draftScope?.accountScopeId || cachedAccountScopeId, projectId,
|
|
6151
|
+
normalizeExperimentalOtByProject, normalizeGovernanceRulesByProject, normalizeCustomInstructionsByProject
|
|
6152
|
+
});
|
|
6056
6153
|
// Keep consumption provenance on the live run owner across ordinary
|
|
6057
6154
|
// session normalization; it is not a composer edit or a queued input.
|
|
6058
6155
|
const consumedSession = liveRunView?.draftConsumption && liveRunView.runProjectId === projectId
|
|
@@ -6077,6 +6174,28 @@
|
|
|
6077
6174
|
normalizeExperimentalOtByProject,
|
|
6078
6175
|
normalizeGovernanceRulesByProject,
|
|
6079
6176
|
normalizeCustomInstructionsByProject,
|
|
6177
|
+
projectPreferenceBaseline,
|
|
6178
|
+
projectPreferenceWrite,
|
|
6179
|
+
applyProjectPreferences: (prefs, captured, fields) => {
|
|
6180
|
+
if (retentionScope.accountScopeId !== cachedAccountScopeId || retentionScope.projectId !== getCurrentProjectId()
|
|
6181
|
+
|| retentionScope.generation !== spaRouteGeneration) return false;
|
|
6182
|
+
const adopted = [];
|
|
6183
|
+
for (const field of fields) {
|
|
6184
|
+
if (JSON.stringify(state[field]?.[projectId]) !== JSON.stringify(captured[field]?.[projectId])) continue;
|
|
6185
|
+
if (JSON.stringify(state[field]?.[projectId]) === JSON.stringify(prefs[field]?.[projectId])) continue;
|
|
6186
|
+
state[field] = { ...state[field], ...prefs[field] };
|
|
6187
|
+
if (!Object.hasOwn(prefs[field] || {}, projectId)) delete state[field][projectId];
|
|
6188
|
+
adopted.push(field);
|
|
6189
|
+
}
|
|
6190
|
+
if (adopted.includes('customInstructionsByProject')) syncCustomInstructionsEditorForProject(projectId);
|
|
6191
|
+
if (adopted.includes('governanceRulesByProject')) syncProjectSettingsEditorForProject();
|
|
6192
|
+
if (adopted.includes('experimentalOtByProject')) syncExperimentalOtToggleForProject(projectId);
|
|
6193
|
+
if (adopted.includes('reviewingAvailabilityByProject')) {
|
|
6194
|
+
syncReviewingTooltip();
|
|
6195
|
+
syncProbeStatus();
|
|
6196
|
+
}
|
|
6197
|
+
return true;
|
|
6198
|
+
},
|
|
6080
6199
|
persistenceContext
|
|
6081
6200
|
});
|
|
6082
6201
|
if (!options?.draftScope) {
|
|
@@ -6534,9 +6653,17 @@
|
|
|
6534
6653
|
if (!composerDraftPersistence) composerDraftPersistence = Modules.ComposerAttachmentDraft.createController({
|
|
6535
6654
|
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId() }),
|
|
6536
6655
|
getAttachments: () => composerAttachmentController.serializePersistableAttachments(),
|
|
6537
|
-
|
|
6656
|
+
getOmittedAttachmentNames: () => composerAttachmentController.getOmittedAttachmentNames(),
|
|
6657
|
+
setAttachments: value => {
|
|
6658
|
+
state.composerAttachments = value;
|
|
6659
|
+
syncComposerSendAvailability();
|
|
6660
|
+
},
|
|
6538
6661
|
clear: () => composerAttachmentController.clear({ notify: false }),
|
|
6539
6662
|
restore: value => composerAttachmentController.restorePersistedAttachments(value),
|
|
6663
|
+
onRestoreOmittedAttachments: ({ count, names }) => appendPlainLog(tx(
|
|
6664
|
+
`${count} attachment${count === 1 ? '' : 's'} could not be saved in the draft. Reattach before sending: ${names.join(', ')}.`,
|
|
6665
|
+
`${count} 个附件未能保存到草稿中,请在发送前重新添加:${names.join('、')}。`
|
|
6666
|
+
)),
|
|
6540
6667
|
read: scope => Modules.StorageDb.readComposerAttachmentDraft(scope),
|
|
6541
6668
|
write: (scope, value) => Modules.StorageDb.writeComposerAttachmentDraft(scope, value),
|
|
6542
6669
|
scheduleSave: () => saveStateSoon()
|
|
@@ -6638,7 +6765,9 @@
|
|
|
6638
6765
|
writingStyleSettings.sync();
|
|
6639
6766
|
applyPanelTheme(getThemePreference());
|
|
6640
6767
|
if ((state.providerId || 'builtin') === 'builtin' && !modelSelectHasOption(state.model)) {
|
|
6641
|
-
renderModelOptions(getModelCatalog().FALLBACK_MODELS, state.model
|
|
6768
|
+
renderModelOptions(getModelCatalog().FALLBACK_MODELS, state.model, {
|
|
6769
|
+
provisionalSelected: getModelDiscovery().provisional === true
|
|
6770
|
+
});
|
|
6642
6771
|
}
|
|
6643
6772
|
panel.querySelector('[data-model]').value = state.model;
|
|
6644
6773
|
const renderedModels = getRenderedModelEntries();
|
|
@@ -7614,13 +7743,16 @@
|
|
|
7614
7743
|
const originalStatus = record.status;
|
|
7615
7744
|
const retryAutoRecompile = typeof record.executionSnapshot?.autoRecompile === 'boolean'
|
|
7616
7745
|
? record.executionSnapshot.autoRecompile : undefined;
|
|
7746
|
+
const generation = spaRouteGeneration;
|
|
7747
|
+
const isOwnerScope = () => generation === spaRouteGeneration
|
|
7748
|
+
&& owner.projectId === getCurrentProjectId() && owner.accountScopeId === cachedAccountScopeId;
|
|
7617
7749
|
// State is renormalized during the awaits below (probes, session sync), which
|
|
7618
7750
|
// replaces run objects; always write through the current copy.
|
|
7619
|
-
const live = () => findRunRecord(runId, session.id) || record;
|
|
7751
|
+
const live = () => (isOwnerScope() && findRunRecord(runId, session.id)) || record;
|
|
7620
7752
|
let finished = false;
|
|
7753
|
+
let retryOutcome = null;
|
|
7621
7754
|
runCancellationRequested = false;
|
|
7622
7755
|
runCancellationController = new AbortController();
|
|
7623
|
-
const generation = spaRouteGeneration;
|
|
7624
7756
|
const requestId = record.nativeRequestId || `writeback:${owner.sessionId}:${runId}`;
|
|
7625
7757
|
const ownerToken = crypto.randomUUID();
|
|
7626
7758
|
const retryView = { recordId: runId, sessionId: session.id, runProjectId: owner.projectId,
|
|
@@ -7632,7 +7764,7 @@
|
|
|
7632
7764
|
if (runCancellationRequested || currentRunView !== retryView) {
|
|
7633
7765
|
throw Object.assign(new Error('Save checking was cancelled.'), { code: 'codex_cancelled' });
|
|
7634
7766
|
}
|
|
7635
|
-
if (
|
|
7767
|
+
if (!isOwnerScope()) {
|
|
7636
7768
|
throw Object.assign(new Error('The project or account changed.'), { code: 'aborted_project_changed' });
|
|
7637
7769
|
}
|
|
7638
7770
|
};
|
|
@@ -7649,6 +7781,9 @@
|
|
|
7649
7781
|
Object.assign(retryView, { root, events: root?.querySelector('[data-run-events]'),
|
|
7650
7782
|
report: root?.querySelector('[data-run-report]'), status: root?.querySelector('[data-run-status]'),
|
|
7651
7783
|
runProcess: root?.querySelector('[data-run-process]'), processLabel: root?.querySelector('[data-run-process-summary]') });
|
|
7784
|
+
const pendingMirror = getPendingMirrorRefresh?.();
|
|
7785
|
+
if (pendingMirror) await awaitRunStep(pendingMirror.catch(() => {}));
|
|
7786
|
+
assertOwner();
|
|
7652
7787
|
if (intent?.requestId) {
|
|
7653
7788
|
const receipt = await callPageBridge('getWritebackReceipt', { requestId: intent.requestId, runProjectId: owner.projectId });
|
|
7654
7789
|
if (receipt?.state === 'running') throw new Error(tx('The previous upload is still finishing. Try again shortly.', '上一轮上传仍在收尾,请稍后再试。'));
|
|
@@ -7662,6 +7797,7 @@
|
|
|
7662
7797
|
? record.executionSnapshot.requireReviewing : intent.requireReviewing,
|
|
7663
7798
|
autoRecompile: retryAutoRecompile,
|
|
7664
7799
|
retryCreates: true, assistantMessageKey: 'retry_sync', assistantMessage: tx('Retried the remaining file transfers for this task.', '已重试本轮未完成的文件同步。') });
|
|
7800
|
+
retryOutcome = result;
|
|
7665
7801
|
assertOwner();
|
|
7666
7802
|
if (result.settlement?.facts) live().settlement = WritebackSettlement.mergeRetrySettlement(live().settlement, result.settlement.facts); // Keep files outside the retry.
|
|
7667
7803
|
await finishRunView(result.hasSkippedOperations ? tx('Sync incomplete', '同步未完成') : tx('Synced', '已同步'),
|
|
@@ -7682,6 +7818,19 @@
|
|
|
7682
7818
|
}
|
|
7683
7819
|
await flushQueuedSaveState();
|
|
7684
7820
|
} catch (error) {
|
|
7821
|
+
const outcome = error.writebackOutcome || retryOutcome;
|
|
7822
|
+
const current = findRunRecord(runId, session.id);
|
|
7823
|
+
if (outcome && (runCancellationRequested || isRunCancellationError(error))
|
|
7824
|
+
&& currentRunView === retryView && isOwnerScope()
|
|
7825
|
+
&& current && !current.forkSnapshot && current.runProjectId === owner.projectId) {
|
|
7826
|
+
// Stop cannot erase writes returned by Retry sync, including a saved
|
|
7827
|
+
// reply that arrived after cancellation. Keep earlier files' facts too.
|
|
7828
|
+
if (outcome.settlement?.facts) current.settlement = WritebackSettlement.mergeRetrySettlement(
|
|
7829
|
+
current.settlement, outcome.settlement.facts);
|
|
7830
|
+
appendRunCancelledReport(outcome, false);
|
|
7831
|
+
await finishRunView(tx('Cancelled', '已取消'), 'cancelled');
|
|
7832
|
+
finished = true;
|
|
7833
|
+
}
|
|
7685
7834
|
showPluginToast(error.message || String(error));
|
|
7686
7835
|
} finally {
|
|
7687
7836
|
// Only finishRunView settles a new terminal status; every other exit
|