codex-overleaf-link 2.5.0 → 2.6.0
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 +155 -392
- package/README.zh-CN.md +154 -393
- 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 +283 -55
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +129 -16
- package/extension/src/backgroundUpdateCoordinator.js +242 -29
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +171 -8
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +104 -47
- package/extension/src/content/composerPanel.js +110 -12
- package/extension/src/content/contentRuntime.js +1555 -926
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +48 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +11077 -3412
- package/extension/src/content/generated/content.bundle.meta.json +14 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +10 -0
- package/extension/src/content/localSkillsPanel.js +104 -14
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +47 -11
- package/extension/src/content/modelPicker.js +8 -4
- package/extension/src/content/nativeCompatibilityController.js +74 -30
- package/extension/src/content/otWarmMirror.js +5 -6
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +17 -4
- package/extension/src/content/panelMaintenance.js +46 -14
- package/extension/src/content/panelRenderer.js +57 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +67 -6
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +73 -14
- package/extension/src/content/recentProjects.js +15 -8
- package/extension/src/content/runActivitySummary.js +160 -18
- package/extension/src/content/runCompletionMeta.js +532 -0
- package/extension/src/content/runController.js +39 -15
- package/extension/src/content/runFailureNotice.js +29 -13
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +157 -47
- package/extension/src/content/runRecoveryActions.js +147 -0
- package/extension/src/content/runResultActions.js +28 -143
- package/extension/src/content/runReviewController.js +633 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +367 -175
- package/extension/src/content/scopedPersistencePanelState.js +134 -20
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +153 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +50 -4
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +22 -5
- package/extension/src/content/settingsWorkbench.js +132 -89
- package/extension/src/content/sharedSessionViewSync.js +49 -11
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +118 -12
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +116 -94
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/overleafCapabilities.js +4 -1
- 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/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +114 -26
- 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 +103 -155
- package/extension/src/pageBridge.js +126 -69
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +373 -17
- package/extension/src/shared/auditRecords.js +4 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +129 -66
- package/extension/src/shared/composerAttachmentDraft.js +156 -0
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +349 -4
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +590 -24
- package/extension/src/shared/lineReferences.js +38 -31
- 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/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +132 -28
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +118 -27
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +166 -52
- package/extension/src/shared/storageDb.js +90 -26
- package/extension/src/shared/storageMigration.js +78 -112
- package/extension/src/shared/storageRunActions.js +267 -30
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +253 -32
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +17 -3
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +58 -40
- package/extension/styles/panel.css +1123 -493
- 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 +269 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +20 -1
- package/native-host/src/chatCompletionsBridge.js +48 -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 +114 -127
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +234 -198
- 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/providerContextError.js +20 -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 +92 -57
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +409 -46
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +5 -8
- package/scripts/install-managed.mjs +1 -0
|
@@ -4,11 +4,39 @@
|
|
|
4
4
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
|
+
const sameScope = (a, b) => a.accountScopeId === b.accountScopeId
|
|
8
|
+
&& a.projectId === b.projectId && a.generation === b.generation;
|
|
9
|
+
|
|
10
|
+
function applySnapshot(options, sessions) {
|
|
11
|
+
const previous = options.getState(), scope = options.getScope();
|
|
12
|
+
const next = options.normalize({ ...previous, sessions }, { restoreRunningRuns: false });
|
|
13
|
+
const unchangedSnapshot = JSON.stringify(next);
|
|
14
|
+
options.setState(next);
|
|
15
|
+
try {
|
|
16
|
+
options.render();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
// A callback may replace state, edit it in place or navigate. Restore only
|
|
19
|
+
// the unchanged view owned by this failed passive application.
|
|
20
|
+
let unchanged = false;
|
|
21
|
+
try {
|
|
22
|
+
unchanged = options.getState() === next && sameScope(scope, options.getScope())
|
|
23
|
+
&& JSON.stringify(options.getState()) === unchangedSnapshot;
|
|
24
|
+
} catch (_error) { /* Unknown state must not overwrite a newer edit. */ }
|
|
25
|
+
if (unchanged) {
|
|
26
|
+
options.setState(previous);
|
|
27
|
+
// A second render can also fail. The old merge base still protects
|
|
28
|
+
// subsequent saves, including values from partially restored controls.
|
|
29
|
+
try { options.render(); } catch (_error) { /* Report the first failure. */ }
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
7
35
|
function install(options) {
|
|
8
|
-
const { Bridge, StorageDb, getScope, getState, isBusy,
|
|
36
|
+
const { Bridge, StorageDb, getScope, getState, isBusy, notice } = options;
|
|
37
|
+
const apply = options.apply || (sessions => applySnapshot(options, sessions));
|
|
9
38
|
let saving = 0, version = 0, pending = false, timer = null, refreshing = false, stopped = false;
|
|
10
|
-
|
|
11
|
-
&& a.projectId === b.projectId && a.generation === b.generation;
|
|
39
|
+
let retryCount = 0;
|
|
12
40
|
const signature = state => JSON.stringify([state.activeSessionId,
|
|
13
41
|
(state.sessions || []).map(session => [session.id, session.updatedAt, session.task])]);
|
|
14
42
|
function schedule(delay = 250) {
|
|
@@ -21,9 +49,10 @@
|
|
|
21
49
|
if (!scope.projectId || !scope.accountScopeId) { pending = false; return; }
|
|
22
50
|
if (saving || isBusy()) { schedule(1000); return; }
|
|
23
51
|
const local = getState();
|
|
24
|
-
// A draft or a not-yet-persisted edit
|
|
25
|
-
// history refresh.
|
|
26
|
-
if (
|
|
52
|
+
// A locally edited draft or a not-yet-persisted edit takes priority over
|
|
53
|
+
// passive history refresh. Callers may distinguish imported shared drafts.
|
|
54
|
+
if ((options.hasLocalDraft ? options.hasLocalDraft(local) : Boolean(local.task))
|
|
55
|
+
|| (local.sessions || []).some(session => StorageDb.hasUnpersistedSharedSession(session))) {
|
|
27
56
|
schedule(1500); return;
|
|
28
57
|
}
|
|
29
58
|
const capturedVersion = version, capturedSignature = signature(local);
|
|
@@ -37,12 +66,21 @@
|
|
|
37
66
|
const ids = new Set(records.map(record => record.id));
|
|
38
67
|
const newLocal = (local.sessions || []).filter(session =>
|
|
39
68
|
!ids.has(session.id) && !StorageDb.wasSharedSessionStored(session.id));
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
StorageDb.adoptSharedSessionBaseline(records);
|
|
69
|
+
// Rendering may fail after only part of the composer was updated.
|
|
70
|
+
// Keep the prior merge base until the complete view accepts the data.
|
|
43
71
|
apply([...records, ...newLocal]);
|
|
72
|
+
if (stopped || !sameScope(scope, getScope())) return;
|
|
73
|
+
if (saving || version !== capturedVersion) { pending = true; return; }
|
|
74
|
+
StorageDb.adoptSharedSessionBaseline(records);
|
|
75
|
+
retryCount = 0;
|
|
44
76
|
} catch (error) {
|
|
45
77
|
notice?.(error);
|
|
78
|
+
// One retry can recover transient DOM failures. Local edits, navigation
|
|
79
|
+
// and persistent failures stop it; another notification starts afresh.
|
|
80
|
+
if (!stopped && sameScope(scope, getScope()) && version === capturedVersion
|
|
81
|
+
&& signature(getState()) === capturedSignature && retryCount < 1) {
|
|
82
|
+
retryCount++; pending = true;
|
|
83
|
+
}
|
|
46
84
|
} finally {
|
|
47
85
|
refreshing = false;
|
|
48
86
|
schedule(1000);
|
|
@@ -51,7 +89,7 @@
|
|
|
51
89
|
const unsubscribe = Bridge.subscribe(change => {
|
|
52
90
|
const scope = getScope();
|
|
53
91
|
if (!change.projectId || change.projectId === scope.projectId) {
|
|
54
|
-
pending = true; schedule();
|
|
92
|
+
retryCount = 0; pending = true; schedule();
|
|
55
93
|
}
|
|
56
94
|
});
|
|
57
95
|
return {
|
|
@@ -68,5 +106,5 @@
|
|
|
68
106
|
}
|
|
69
107
|
};
|
|
70
108
|
}
|
|
71
|
-
return { install };
|
|
109
|
+
return { install, applySnapshot };
|
|
72
110
|
});
|
|
@@ -3,18 +3,31 @@
|
|
|
3
3
|
})(typeof globalThis !== 'undefined' ? globalThis : window, function subagentActivityViewFactory(Agents, Activity) {
|
|
4
4
|
'use strict';
|
|
5
5
|
function create(deps) {
|
|
6
|
-
const { tx, getPanel, plain, make, icon, formatElapsed, renderBlocks } = deps;
|
|
6
|
+
const { tx, getPanel, plain, plainThought, make, icon, formatElapsed, renderBlocks, glyph = () => '' } = deps;
|
|
7
|
+
const withGlyph = (node, name, label) => {
|
|
8
|
+
node.insertAdjacentHTML('beforeend', glyph(name));
|
|
9
|
+
const text = make('span', ''); text.textContent = label; node.append(text);
|
|
10
|
+
return node;
|
|
11
|
+
};
|
|
7
12
|
let viewer = null;
|
|
8
13
|
function statusLabel(status) {
|
|
9
14
|
return ({ pending: tx('Starting', '启动中'), running: tx('Running', '运行中'),
|
|
10
15
|
completed: tx('Completed', '已完成'), failed: tx('Failed', '失败'),
|
|
11
16
|
cancelled: tx('Stopped', '已停止'), unknown: tx('Status unconfirmed', '状态未确认') })[status] || '';
|
|
12
17
|
}
|
|
18
|
+
// A row preview is one plain line: a final message keeps its words, not its
|
|
19
|
+
// Markdown ("- `main.tex` is …" reads "main.tex is …").
|
|
20
|
+
function plainMessage(value) {
|
|
21
|
+
return value.replace(/^\s*(?:#{1,6}\s+|>\s?|[-*+]\s+|\d+[.)]\s+)/gm, '')
|
|
22
|
+
.replace(/!?\[([^\]]*)\]\([^)\s]*\)/g, '$1').replace(/`([^`\n]+)`/g, '$1')
|
|
23
|
+
.replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, '$2').replace(/\s+/g, ' ').trim();
|
|
24
|
+
}
|
|
13
25
|
function latest(agent) {
|
|
14
26
|
const blocks = Activity.project({ status: agent.status, events: agent.events || [] }).blocks;
|
|
15
27
|
const last = blocks.at(-1);
|
|
16
28
|
const item = last?.kind === 'exploreGroup' ? last.items.at(-1) : last;
|
|
17
|
-
|
|
29
|
+
const text = plain(item?.meta?.target || item?.meta?.command || item?.event?.title || agent.task);
|
|
30
|
+
return (item?.kind === 'message' ? plainMessage(text) : text).slice(0, 180);
|
|
18
31
|
}
|
|
19
32
|
function elapsed(agent, run) {
|
|
20
33
|
const duration = Agents.elapsedMs(agent, run, Date.now());
|
|
@@ -42,9 +55,9 @@
|
|
|
42
55
|
shell.setAttribute('aria-label', tx('Subagent conversation', '子代理对话'));
|
|
43
56
|
const header = make('header', 'codex-subagent-view-head');
|
|
44
57
|
const back = make('button', 'codex-subagent-back');
|
|
45
|
-
back.type = 'button'; back
|
|
58
|
+
back.type = 'button'; withGlyph(back, 'chevronLeft', tx('Back to main task', '返回主任务'));
|
|
46
59
|
back.addEventListener('click', close);
|
|
47
|
-
const readonly = make('span', 'codex-subagent-readonly')
|
|
60
|
+
const readonly = withGlyph(make('span', 'codex-subagent-readonly'), 'lock', tx('Read only', '只读'));
|
|
48
61
|
header.append(back, readonly);
|
|
49
62
|
const parent = make('div', 'codex-subagent-parent');
|
|
50
63
|
const parentLabel = make('span', 'codex-subagent-parent-label');
|
|
@@ -112,7 +125,7 @@
|
|
|
112
125
|
const mark = icon('agent'), copy = make('span', 'run-subagent-copy');
|
|
113
126
|
const name = make('span', 'run-subagent-name'), activity = make('span', 'run-subagent-activity');
|
|
114
127
|
copy.append(name, activity);
|
|
115
|
-
const tail = make('span', 'run-subagent-tail'), status = make('span', ''), time = make('span', 'run-subagent-time');
|
|
128
|
+
const tail = make('span', 'run-subagent-tail'), status = make('span', 'run-subagent-status'), time = make('span', 'run-subagent-time');
|
|
116
129
|
tail.append(status, time);
|
|
117
130
|
el.append(mark, copy, tail);
|
|
118
131
|
el.addEventListener('click', () => open(card, agent.key, el));
|
|
@@ -154,15 +167,17 @@
|
|
|
154
167
|
const childStatus = Agents.effectiveStatus(agent, card.run);
|
|
155
168
|
viewer.title.textContent = plain(agent.title || agent.jobId || agent.threadId);
|
|
156
169
|
viewer.meta.textContent = [statusLabel(childStatus), elapsed(agent, card.run), agent.model, agent.reasoningEffort].filter(Boolean).join(' · ');
|
|
170
|
+
viewer.meta.dataset.state = childStatus;
|
|
157
171
|
viewer.prompt.textContent = plain(agent.task);
|
|
158
172
|
viewer.assignment.hidden = !viewer.prompt.textContent;
|
|
159
173
|
const parent = Activity.project(card.run), last = parent.blocks.at(-1);
|
|
160
174
|
const lastItem = last?.kind === 'exploreGroup' ? last.items.at(-1) : last;
|
|
161
|
-
const parentWork = plain(lastItem?.meta?.target || lastItem?.meta?.command || lastItem?.event?.title).slice(0, 150);
|
|
175
|
+
const parentWork = plainThought(plain(lastItem?.meta?.target || lastItem?.meta?.command || lastItem?.event?.title)).slice(0, 150);
|
|
162
176
|
viewer.parentLabel.textContent = tx('Main agent · ', '主 agent · ') + (card.run.status === 'running'
|
|
163
177
|
? parentWork || tx('Working', '处理中') : tx('Run ended', '本轮已结束'));
|
|
164
178
|
const running = (card.run.subagents || []).filter(item => ['pending', 'running'].includes(Agents.effectiveStatus(item, card.run))).length;
|
|
165
179
|
viewer.parentCount.textContent = tx(running + ' subagents active', running + ' 个子任务进行中');
|
|
180
|
+
viewer.parentCount.hidden = running === 0;
|
|
166
181
|
for (const sibling of card.run.subagents || []) {
|
|
167
182
|
let button = viewer.siblingRows.get(sibling.key);
|
|
168
183
|
if (!button) {
|
|
@@ -172,7 +187,8 @@
|
|
|
172
187
|
}
|
|
173
188
|
button.textContent = plain(sibling.title || sibling.jobId || sibling.threadId);
|
|
174
189
|
button.setAttribute('aria-pressed', String(sibling.key === agent.key));
|
|
175
|
-
button.
|
|
190
|
+
button.dataset.state = Agents.effectiveStatus(sibling, card.run);
|
|
191
|
+
button.title = statusLabel(button.dataset.state);
|
|
176
192
|
}
|
|
177
193
|
const childRun = { id: card.id + ':' + agent.key, status: ['pending', 'running'].includes(childStatus) ? 'running' : childStatus,
|
|
178
194
|
events: agent.events || [], mode: 'ask' };
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
let lastUserActivityAt = Date.now();
|
|
6
6
|
let stableSavedSince = 0;
|
|
7
7
|
let applying = false;
|
|
8
|
+
let saveIdentity;
|
|
8
9
|
const documentRef = options.document || root.document;
|
|
9
10
|
const noteActivity = event => {
|
|
10
|
-
if (event?.target?.closest?.('#codex-overleaf-panel')) return;
|
|
11
|
+
if (!options.includePanelActivity && event?.target?.closest?.('#codex-overleaf-panel')) return;
|
|
11
12
|
lastUserActivityAt = Date.now();
|
|
12
13
|
stableSavedSince = 0;
|
|
13
14
|
};
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
saveResult = { ok: false, state: 'unavailable' };
|
|
43
44
|
}
|
|
44
45
|
const saved = saveResult?.ok === true && ['verified_saved', 'verified_quiet'].includes(saveResult?.state);
|
|
46
|
+
if (saveResult?.identity !== saveIdentity) { stableSavedSince = 0; saveIdentity = saveResult?.identity; }
|
|
45
47
|
if (saved) {
|
|
46
48
|
stableSavedSince = stableSavedSince || Date.now();
|
|
47
49
|
} else {
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
|
|
63
65
|
return {
|
|
64
66
|
buildIdleProbe,
|
|
67
|
+
noteActivity,
|
|
65
68
|
handleMessage,
|
|
66
69
|
isApplying: () => applying
|
|
67
70
|
};
|
|
@@ -85,9 +85,20 @@
|
|
|
85
85
|
} catch (_) { manualCommandCopied = false; }
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
+
if (action === 'restore-previous' && !window.confirm(restorePreviousConfirmText())) return;
|
|
88
89
|
return runUpdateAction(action);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
function restorePreviousLabel() {
|
|
93
|
+
const version = currentView?.restorePrevious?.version || '';
|
|
94
|
+
return version ? tx(`Restore v${version}`, `恢复到 v${version}`) : tx('Restore previous version', '恢复到上一版本');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function restorePreviousConfirmText() {
|
|
98
|
+
return tx('Restore the previous version? The extension reloads, and saved, idle Overleaf tabs are refreshed. Sessions, settings and provider keys are kept.',
|
|
99
|
+
'恢复到上一版本?扩展会重新加载,已保存且空闲的 Overleaf 标签页会被刷新。会话、设置和模型服务的密钥都会保留。');
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
async function runUpdateAction(action) {
|
|
92
103
|
const cancelling = action === 'cancel' || action === 'later';
|
|
93
104
|
if (cancellationInFlight || ((actionInFlight || settingsActionInFlight) && !cancelling)) return;
|
|
@@ -105,7 +116,8 @@
|
|
|
105
116
|
cancel: 'codex-overleaf/consent-update-cancel',
|
|
106
117
|
recover: 'codex-overleaf/consent-update-recover',
|
|
107
118
|
dismiss: 'codex-overleaf/consent-update-dismiss',
|
|
108
|
-
reload: 'codex-overleaf/consent-update-reload'
|
|
119
|
+
reload: 'codex-overleaf/consent-update-reload',
|
|
120
|
+
'restore-previous': 'codex-overleaf/consent-update-rollback'
|
|
109
121
|
}[action]);
|
|
110
122
|
if (sequence === actionSequence) currentView = view;
|
|
111
123
|
} catch (error) {
|
|
@@ -226,6 +238,13 @@
|
|
|
226
238
|
button.dataset.updateSettingsBound = 'true';
|
|
227
239
|
button.addEventListener('click', handleSettingsAction);
|
|
228
240
|
}
|
|
241
|
+
const restore = panel.querySelector?.('[data-restore-previous-version]');
|
|
242
|
+
if (restore && restore.dataset.updateSettingsBound !== 'true') {
|
|
243
|
+
restore.dataset.updateSettingsBound = 'true';
|
|
244
|
+
restore.addEventListener('click', () => {
|
|
245
|
+
if (window.confirm(restorePreviousConfirmText())) void runUpdateAction('restore-previous');
|
|
246
|
+
});
|
|
247
|
+
}
|
|
229
248
|
renderSettings();
|
|
230
249
|
}
|
|
231
250
|
|
|
@@ -245,6 +264,7 @@
|
|
|
245
264
|
if (!currentView) return;
|
|
246
265
|
renderSettings();
|
|
247
266
|
if (!notice) return;
|
|
267
|
+
notice.setAttribute('aria-label', tx('Stable update', '稳定版更新'));
|
|
248
268
|
if (!currentView.showPanel) {
|
|
249
269
|
notice.hidden = true;
|
|
250
270
|
notice.replaceChildren();
|
|
@@ -355,7 +375,24 @@
|
|
|
355
375
|
let statusText = '';
|
|
356
376
|
let buttonText = tx('Check for updates', '检查更新');
|
|
357
377
|
|
|
358
|
-
if (
|
|
378
|
+
if (stateName === 'idle' && state.code === 'update_skipped_after_rollback') {
|
|
379
|
+
statusText = tx(
|
|
380
|
+
`v${latestVersion} is skipped because you restored the previous version. Check for updates to install it again.`,
|
|
381
|
+
`你已恢复到上一版本,自动更新会跳过 v${latestVersion}。点“检查更新”可以重新安装。`
|
|
382
|
+
);
|
|
383
|
+
} else if (stateName === 'idle' && state.code === 'update_cooldown') {
|
|
384
|
+
const until = Number(state.cooldownUntil || 0);
|
|
385
|
+
const time = until > 0 && Number.isFinite(until) ? new Date(until).toLocaleString(getLocale() || undefined) : '';
|
|
386
|
+
statusText = tx(
|
|
387
|
+
`Update v${latestVersion} is paused after a failed installation. ${time ? `Check again after ${time}.` : 'Check again after its cooldown ends.'}`,
|
|
388
|
+
`v${latestVersion} 安装失败后已暂停更新。${time ? `请在 ${time} 后再次检查。` : '请在冷却期结束后再次检查。'}`
|
|
389
|
+
);
|
|
390
|
+
} else if (stateName === 'idle' && !state.code && state.lastCheckedAt > 0
|
|
391
|
+
&& (!latestVersion || !isNewerStableVersion(latestVersion, currentVersion))) {
|
|
392
|
+
// A finished check with nothing newer still answers the user's question.
|
|
393
|
+
const time = new Date(state.lastCheckedAt).toLocaleTimeString(getLocale() || undefined, { hour: 'numeric', minute: '2-digit' });
|
|
394
|
+
statusText = tx(`You have the latest stable version. Checked at ${time}.`, `已是最新稳定版本。检查时间 ${time}。`);
|
|
395
|
+
} else if (['reload_required', 'reload_tabs_required'].includes(stateName)) {
|
|
359
396
|
summaryText = tx(`Installed v${latestVersion}; running extension v${currentVersion}.`, `已安装 v${latestVersion};扩展当前运行 v${currentVersion}。`);
|
|
360
397
|
statusText = getCopy(state).detail;
|
|
361
398
|
buttonText = stateName === 'reload_required' ? tx('Reload extension', '重新加载扩展') : tx('Refresh Overleaf tabs', '刷新 Overleaf 标签页');
|
|
@@ -373,7 +410,7 @@
|
|
|
373
410
|
statusText = getCopy(state).detail;
|
|
374
411
|
buttonText = tx('Update in progress', '更新进行中');
|
|
375
412
|
} else if (stateName === 'failed' || stateName === 'rolled_back') {
|
|
376
|
-
statusText = state
|
|
413
|
+
statusText = formatUpdateMessage(state) || getCopy(state).detail;
|
|
377
414
|
buttonText = tx('Retry', '重试');
|
|
378
415
|
} else if (stateName === 'committed') {
|
|
379
416
|
summaryText = tx(`Current v${currentVersion}. Update completed successfully.`, `当前版本 v${currentVersion}。更新已成功完成。`);
|
|
@@ -396,11 +433,19 @@
|
|
|
396
433
|
} else {
|
|
397
434
|
button.disabled = actionInFlight || settingsActionInFlight || ACTIVE_UPDATE_STATES.has(stateName);
|
|
398
435
|
}
|
|
436
|
+
const restore = settingsRoot.querySelector?.('[data-restore-previous-version]');
|
|
437
|
+
if (restore) {
|
|
438
|
+
restore.hidden = !currentView?.actions?.restorePrevious;
|
|
439
|
+
restore.textContent = restorePreviousLabel();
|
|
440
|
+
restore.disabled = actionInFlight || settingsActionInFlight;
|
|
441
|
+
}
|
|
399
442
|
}
|
|
400
443
|
|
|
401
444
|
function visibleActions(state) {
|
|
402
445
|
const value = currentView?.state || {};
|
|
403
|
-
|
|
446
|
+
const restore = currentView?.actions?.restorePrevious
|
|
447
|
+
? [{ id: 'restore-previous', label: restorePreviousLabel(), primary: false }] : [];
|
|
448
|
+
if (value.recoveryPending) return [...restore, { id: 'recover', label: tx('Check recovery', '检查恢复状态'), primary: true }];
|
|
404
449
|
if (value.cancelRequested || ['checking', 'downloading'].includes(state)) {
|
|
405
450
|
return [{ id: 'cancel', label: value.cancelRequested
|
|
406
451
|
? value.code ? tx('Retry cancellation', '重试取消') : tx('Cancelling…', '正在取消…')
|
|
@@ -434,7 +479,7 @@
|
|
|
434
479
|
];
|
|
435
480
|
}
|
|
436
481
|
if (state === 'committed') {
|
|
437
|
-
return [{ id: 'dismiss', label: tx('Done', '完成'), primary: true }];
|
|
482
|
+
return [...restore, { id: 'dismiss', label: tx('Done', '完成'), primary: true }];
|
|
438
483
|
}
|
|
439
484
|
return [];
|
|
440
485
|
}
|
|
@@ -443,11 +488,11 @@
|
|
|
443
488
|
const target = state.latestVersion ? 'v' + state.latestVersion : '';
|
|
444
489
|
if (state.cancelRequested) return {
|
|
445
490
|
eyebrow: tx('Cancelling update', '正在取消更新'), title: target,
|
|
446
|
-
detail: state
|
|
491
|
+
detail: formatUpdateMessage(state) || tx('Stopping this update and clearing its authorization.', '正在停止本次更新并清理更新授权。')
|
|
447
492
|
};
|
|
448
493
|
if (state.recoveryPending) return {
|
|
449
494
|
eyebrow: tx('Checking update recovery', '正在确认更新恢复状态'), title: target,
|
|
450
|
-
detail: state
|
|
495
|
+
detail: formatUpdateMessage(state) || tx('Waiting for the installed version to be confirmed.', '正在确认实际安装的版本。')
|
|
451
496
|
};
|
|
452
497
|
const blocker = blockersCopy(state.blockers?.length ? state.blockers : [state.blocker]);
|
|
453
498
|
return {
|
|
@@ -455,11 +500,11 @@
|
|
|
455
500
|
detail: tx('Reading the latest signed release.', '正在读取最新签名版本。') },
|
|
456
501
|
reload_required: {
|
|
457
502
|
eyebrow: tx('Installed, awaiting reload', '已安装,等待加载'), title: target,
|
|
458
|
-
detail: state
|
|
503
|
+
detail: formatUpdateMessage(state) || tx('The new files are installed. Reload the extension when Overleaf is saved and idle.', '新版文件已安装,请在 Overleaf 保存并空闲后重新加载扩展。')
|
|
459
504
|
},
|
|
460
505
|
reload_tabs_required: {
|
|
461
506
|
eyebrow: tx('Updated runtime is ready', '新版运行组件已就绪'), title: target,
|
|
462
|
-
detail: state
|
|
507
|
+
detail: formatUpdateMessage(state) || tx('Refresh the saved, idle Overleaf tabs to load the updated panel.', '刷新已保存且空闲的 Overleaf 标签页以加载新版面板。')
|
|
463
508
|
},
|
|
464
509
|
manual_install_required: {
|
|
465
510
|
eyebrow: tx('Reinstall needed for this update', '这次更新需要重新安装'),
|
|
@@ -504,12 +549,14 @@
|
|
|
504
549
|
rolled_back: {
|
|
505
550
|
eyebrow: tx('Previous version restored', '已恢复上一版本'),
|
|
506
551
|
title: state.currentVersion ? 'v' + state.currentVersion : '',
|
|
507
|
-
detail:
|
|
552
|
+
detail: state.code === 'update_user_rollback'
|
|
553
|
+
? tx('You restored the previous version. Automatic reminders for this version are paused for 24 hours. Check for updates to install it again.', '已恢复到上一版本,刚才退出的版本将暂停自动提醒 24 小时。点“检查更新”可以重新安装。')
|
|
554
|
+
: tx('The replacement failed its health check.', '替换版本未通过健康检查。')
|
|
508
555
|
},
|
|
509
556
|
failed: {
|
|
510
557
|
eyebrow: tx('Update could not continue', '更新无法继续'),
|
|
511
558
|
title: target,
|
|
512
|
-
detail: state
|
|
559
|
+
detail: formatUpdateMessage(state) || tx('Review the update details and retry.', '请查看更新详情后重试。')
|
|
513
560
|
}
|
|
514
561
|
}[state.state] || { eyebrow: '', title: '', detail: '' };
|
|
515
562
|
}
|
|
@@ -521,6 +568,46 @@
|
|
|
521
568
|
return `npm exec --yes codex-overleaf-link@${version} -- install-managed`;
|
|
522
569
|
}
|
|
523
570
|
|
|
571
|
+
function formatUpdateMessage(state = {}) {
|
|
572
|
+
const code = state.code || '';
|
|
573
|
+
const messages = {
|
|
574
|
+
update_phase_timeout: ['The update timed out and its download was stopped. Retry when the connection is available.', '更新超时,下载已停止。连接恢复后请重试。'],
|
|
575
|
+
update_not_applied: ['The update was stopped before installation. Retry when ready.', '更新已在安装前停止,可准备就绪后重试。'],
|
|
576
|
+
update_recovery_pending: ['The update result is being checked. Recovery has not been confirmed.', '正在确认更新结果,恢复状态尚未确认。'],
|
|
577
|
+
update_rolled_back: ['The previous version has been restored.', '已恢复上一版本。'],
|
|
578
|
+
update_user_rollback: ['You restored the previous version.', '你已恢复到上一版本。'],
|
|
579
|
+
update_not_managed: ['Run install-managed once to enable coordinated automatic updates.', '请先运行一次 install-managed,以启用扩展与本机服务的协调更新。'],
|
|
580
|
+
update_native_busy: ['Native Host is still processing a Codex task.', 'Native Host 仍在处理 Codex 任务。'],
|
|
581
|
+
update_revoke_too_late: ['The update is already being installed. Wait for its result.', '更新已进入安装,请等待结果。'],
|
|
582
|
+
update_cancel_unconfirmed: ['Update cancellation is unconfirmed. Check recovery before retrying.', '尚未确认更新已取消,请先检查恢复状态。'],
|
|
583
|
+
update_apply_unconfirmed: ['Update installation is unconfirmed. Check recovery before retrying.', '尚未确认更新安装结果,请先检查恢复状态。'],
|
|
584
|
+
update_rollback_unconfirmed: ['Restoring the previous version is unconfirmed. Check recovery before retrying.', '尚未确认旧版本已恢复,请先检查恢复状态。'],
|
|
585
|
+
update_reload_unavailable: ['No verified installed update is awaiting reload.', '当前没有已验证且等待重载的更新。'],
|
|
586
|
+
update_network_cancelled: ['The update download was cancelled.', '更新下载已取消。'],
|
|
587
|
+
update_apply_failed: ['The update could not be installed. Check recovery and retry.', '无法安装更新,请检查恢复状态后重试。']
|
|
588
|
+
};
|
|
589
|
+
if (messages[code]) return tx(...messages[code]);
|
|
590
|
+
if (['update_health_failed', 'update_request_failed', 'update_failed'].includes(code)) return tx('The updated components could not be confirmed. Check recovery before retrying.', '尚未确认更新后的组件正常,请先检查恢复状态。');
|
|
591
|
+
if (['update_bootstrap_health_timeout', 'update_bootstrap_reload_failed'].includes(code)) return tx('The updated startup components did not finish restarting and confirming. Check recovery before retrying.', '更新后的启动组件未能完成重启和确认,请先检查恢复状态。');
|
|
592
|
+
if (['update_bootstrap_source_mismatch', 'update_bootstrap_source_version_mismatch'].includes(code)) return tx('The installed startup files do not match a supported update source. Restore the previous version in Settings, or reinstall once.', '已安装的启动文件与支持的升级来源不一致。请在设置中恢复到上一版本,或重新安装一次。');
|
|
593
|
+
if (['update_bootstrap_record_missing', 'update_bootstrap_record_invalid', 'update_bootstrap_backup_mismatch', 'update_bootstrap_already_restoring'].includes(code)) return tx('The startup recovery material could not be confirmed. The update result remains unconfirmed.', '尚未确认启动组件的恢复材料有效,更新结果仍待确认。');
|
|
594
|
+
if (['update_bootstrap_confirmation_pending', 'update_bootstrap_native_restart_required'].includes(code)) return tx('Waiting for the updated startup components and Native Host to finish restarting and confirming.', '正在等待更新后的启动组件和本机服务完成重启及确认。');
|
|
595
|
+
if (code === 'update_bootstrap_monitor_unavailable') return tx('The update recovery monitor could not start. Check recovery before retrying.', '更新恢复监护程序未能启动,请先检查恢复状态。');
|
|
596
|
+
if (['update_bootstrap_transaction_mismatch', 'update_bootstrap_transaction_invalid'].includes(code)) return tx('The startup update transaction could not be matched. Check recovery before retrying.', '未能匹配启动组件的更新事务,请先检查恢复状态。');
|
|
597
|
+
if (code.startsWith('update_bootstrap_')) return tx('The startup update package or installed files could not be verified. Check recovery before retrying.', '无法验证启动组件更新包或已安装文件,请先检查恢复状态。');
|
|
598
|
+
if (['update_reload_required', 'update_reload_busy'].includes(code)) {
|
|
599
|
+
const match = /^Reload is waiting for Overleaf to be saved and idle \(([^)]+)\)\.$/.exec(state.message || '');
|
|
600
|
+
if (match) return blockersCopy(match[1].split(', '));
|
|
601
|
+
}
|
|
602
|
+
if (['update_check_timeout', 'update_activation_timeout', 'update_status_timeout'].includes(code)) return tx('The update operation timed out. Check recovery or retry when the connection is available.', '更新操作超时,请检查恢复状态,或在连接恢复后重试。');
|
|
603
|
+
if (['update_candidate_missing', 'update_candidate_stale', 'update_not_staged', 'update_staged_archive_invalid', 'update_recovery_superseded'].includes(code)) return tx('No current verified update is ready. Check for updates again.', '当前没有已验证且可安装的更新,请重新检查更新。');
|
|
604
|
+
if (['update_authorization_in_progress', 'update_transaction_in_progress', 'update_transaction_locked'].includes(code)) return tx('Another update operation is in progress. Wait for it to finish.', '另一项更新操作正在进行,请等待其完成。');
|
|
605
|
+
if (['update_authorization_invalid', 'update_consent_required', 'update_consent_mismatch', 'update_transaction_mismatch', 'update_transaction_state_changed'].includes(code)) return tx('The update authorization or transaction changed. Check for updates and authorize the current candidate again.', '更新授权或事务已变化,请重新检查更新并确认当前候选版本。');
|
|
606
|
+
if (['update_archive_checksum_invalid', 'update_archive_compressed_limit', 'update_archive_duplicate_path', 'update_archive_file_count_limit', 'update_archive_file_limit', 'update_archive_invalid', 'update_archive_path_forbidden', 'update_archive_path_too_long', 'update_archive_required_file_missing', 'update_archive_total_limit', 'update_archive_truncated', 'update_archive_type_forbidden', 'update_staging_not_empty', 'update_operation_invalid', 'update_bundle_hash_invalid', 'update_bundle_name_invalid', 'update_bundle_size_invalid', 'update_manifest_artifacts_invalid', 'update_manifest_commit_invalid', 'update_manifest_invalid', 'update_manifest_manual_install_invalid', 'update_manifest_origin_mismatch', 'update_manifest_protocol_invalid', 'update_manifest_schema_unsupported', 'update_manifest_timestamp_invalid', 'update_manifest_too_large', 'update_manifest_version_invalid', 'update_signature_algorithm_unsupported', 'update_signature_invalid', 'update_signature_key_invalid', 'update_signature_key_unknown', 'update_signature_too_large', 'update_version_invalid'].includes(code)) return tx('The signed update or archive could not be verified. Check the release details and retry.', '无法验证更新签名或压缩包,请检查发布详情后重试。');
|
|
607
|
+
if (['update_github_http_error', 'update_github_response_invalid', 'update_asset_missing', 'update_asset_url_forbidden', 'update_release_asset_invalid', 'update_release_manifest_mismatch', 'update_bundle_size_mismatch', 'update_bundle_hash_mismatch', 'update_native_version_mismatch', 'update_extension_version_mismatch', 'update_runtime_manifest_invalid', 'update_runtime_asset_missing', 'update_bootstrap_manifest_invalid', 'update_managed_layout_invalid', 'update_marker_invalid', 'update_current_version_invalid', 'update_health_version_mismatch', 'update_confirmation_unexpected', 'update_rollback_unavailable'].includes(code)) return tx('The update or installed files could not be verified. Check the update details and repair or retry.', '无法验证更新或已安装文件,请检查更新详情后修复或重试。');
|
|
608
|
+
return state.message || '';
|
|
609
|
+
}
|
|
610
|
+
|
|
524
611
|
function blockerCopy(value) {
|
|
525
612
|
return {
|
|
526
613
|
unsaved: tx('Overleaf has not confirmed that this document is saved.', 'Overleaf 尚未确认当前文档已保存。'),
|
|
@@ -541,6 +628,7 @@
|
|
|
541
628
|
tab_unavailable: tx('An Overleaf tab is unavailable for the safety check.', '某个 Overleaf 标签页当前无法完成安全检查。'),
|
|
542
629
|
idle_probe_failed: tx('The Overleaf safety check failed. Reload the affected tab and retry.', 'Overleaf 安全检查失败;请刷新受影响的标签页后重试。'),
|
|
543
630
|
native_project_locked: tx('Native Host still holds a project lock.', 'Native Host 仍持有项目锁。'),
|
|
631
|
+
native_process_stop_unconfirmed: tx('A previous Codex process has not yet confirmed its exit.', '前一次 Codex 进程尚未确认退出。'),
|
|
544
632
|
native_run_active: tx('Native Host is still processing a task.', 'Native Host 仍在处理任务。'),
|
|
545
633
|
busy: tx('A component is still busy.', '某个组件仍处于忙碌状态。')
|
|
546
634
|
}[value] || tx('Waiting until every Overleaf tab is saved and idle.', '正在等待所有 Overleaf 标签页完成保存并进入空闲状态。');
|
|
@@ -555,14 +643,32 @@
|
|
|
555
643
|
}
|
|
556
644
|
|
|
557
645
|
function manualReasonText(state = {}) {
|
|
646
|
+
if (state.code === 'prerelease_promotion_requires_manual_update') {
|
|
647
|
+
return tx('The installed release candidate has the same version number as the stable release. Reinstall to use the stable release.',
|
|
648
|
+
'已安装的候选版与稳定版使用相同版本号。请重新安装以使用稳定版。');
|
|
649
|
+
}
|
|
650
|
+
if (state.code === 'update_bootstrap_source_mismatch') {
|
|
651
|
+
return tx('The installed startup files differ from the published release this update starts from, for example after a release candidate or local build. Reinstall once to continue.',
|
|
652
|
+
'已安装的启动文件与这次更新所基于的正式版不一致(例如装过候选版或本地构建)。重新安装一次即可继续。');
|
|
653
|
+
}
|
|
558
654
|
const reason = state.manualReason || {};
|
|
559
655
|
return tx(reason.en || reason.zh || '', reason.zh || reason.en || '');
|
|
560
656
|
}
|
|
561
657
|
|
|
658
|
+
// Compares the numeric x.y.z core only; a local build ahead of the release is not behind.
|
|
659
|
+
function isNewerStableVersion(candidate, current) {
|
|
660
|
+
const parts = value => String(value || '').replace(/^v/i, '').split('-')[0].split('.').map(part => Number(part) || 0);
|
|
661
|
+
const [left, right] = [parts(candidate), parts(current)];
|
|
662
|
+
for (let index = 0; index < 3; index++) {
|
|
663
|
+
if ((left[index] || 0) !== (right[index] || 0)) return (left[index] || 0) > (right[index] || 0);
|
|
664
|
+
}
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
|
|
562
668
|
function tx(english, chinese) {
|
|
563
669
|
const locale = String(getLocale() || document.documentElement?.lang || 'en');
|
|
564
670
|
return /^zh\b/i.test(locale) ? chinese : english;
|
|
565
671
|
}
|
|
566
672
|
|
|
567
|
-
return Object.freeze({ mount });
|
|
673
|
+
return Object.freeze({ mount, refreshLocale: render });
|
|
568
674
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
(function installUpdateSafetyReceiver(root) {
|
|
2
|
+
'use strict';
|
|
3
|
+
if (root.__codexOverleafUpdateSafetyReceiverInstalled) return;
|
|
4
|
+
let binding, idle, inFlight = false;
|
|
5
|
+
const blocked = () => ({ idle: false, saved: false, blockers: ['tab_probe_unavailable'] });
|
|
6
|
+
function install(next, dependencies) {
|
|
7
|
+
if (!next?.transactionId || !next.token || next.url !== location.href) return { ok: false };
|
|
8
|
+
if (binding) {
|
|
9
|
+
if (binding.transactionId !== next.transactionId || binding.targetVersion !== next.targetVersion
|
|
10
|
+
|| binding.documentId !== next.documentId || binding.url !== next.url || inFlight) return { ok: false };
|
|
11
|
+
binding = Object.freeze({ ...next });
|
|
12
|
+
return { ok: true, url: location.href };
|
|
13
|
+
}
|
|
14
|
+
binding = Object.freeze({ ...next });
|
|
15
|
+
const pageWrites = new Set();
|
|
16
|
+
const visible = node => !node.hidden && node.getAttribute('aria-hidden') !== 'true' && node.getClientRects().length > 0;
|
|
17
|
+
const { UpdateIdle, PageRpcContract } = dependencies;
|
|
18
|
+
idle = UpdateIdle.create({ document, includePanelActivity: true,
|
|
19
|
+
getBusyState: () => ({ reviewAction: pageWrites.size > 0
|
|
20
|
+
|| [...document.querySelectorAll('#codex-overleaf-panel [data-run-accept-confirm], #codex-overleaf-panel button:disabled')]
|
|
21
|
+
.some(node => visible(node) && (node.hasAttribute('data-run-accept-confirm') || /Accepting|接受中/.test(node.textContent)
|
|
22
|
+
|| (node.hasAttribute('data-run-undo') && !/Undone|已撤销/.test(node.textContent)
|
|
23
|
+
&& !/Accepted|已接受/.test(node.closest('[data-run-id]')?.querySelector('[data-run-accept]')?.textContent || '')))),
|
|
24
|
+
run: document.querySelector('#codex-overleaf-panel')?.dataset.running === 'true',
|
|
25
|
+
cancelling: document.querySelector('#codex-overleaf-panel')?.dataset.cancelling === 'true',
|
|
26
|
+
dialog: [...document.querySelectorAll('[role="dialog"], dialog[open]')].some(visible) }),
|
|
27
|
+
checkSaved: async () => {
|
|
28
|
+
const response = await chrome.runtime.sendMessage({ type: 'codex-overleaf/migration-save-probe', ...binding,
|
|
29
|
+
nonce: activeNonce });
|
|
30
|
+
return response?.ok && response.nonce === activeNonce ? response.result : { ok: false, state: 'unavailable' };
|
|
31
|
+
} });
|
|
32
|
+
root.addEventListener('message', event => {
|
|
33
|
+
if (event.source !== root || event.origin !== location.origin || typeof event.data?.id !== 'string') return;
|
|
34
|
+
const data = event.data;
|
|
35
|
+
const mutation = PageRpcContract.getMethod(data.method)?.mutation;
|
|
36
|
+
if (data.source === 'codex-overleaf/content' && ['document', 'page_state', 'navigation'].includes(mutation)) {
|
|
37
|
+
pageWrites.add(data.id); idle.noteActivity();
|
|
38
|
+
} else if (data.source === 'codex-overleaf/page' && pageWrites.delete(data.id)) idle.noteActivity();
|
|
39
|
+
});
|
|
40
|
+
chrome.runtime.onMessage.addListener((message, sender, respond) => {
|
|
41
|
+
if (message?.type !== 'codex-overleaf/migration-idle-probe') return undefined;
|
|
42
|
+
if (sender?.id !== chrome.runtime.id || sender.tab || message.token !== binding.token
|
|
43
|
+
|| message.transactionId !== binding.transactionId || typeof message.nonce !== 'string'
|
|
44
|
+
|| !message.nonce || location.href !== binding.url || inFlight) { respond(blocked()); return false; }
|
|
45
|
+
inFlight = true;
|
|
46
|
+
activeNonce = message.nonce;
|
|
47
|
+
idle.buildIdleProbe().then(result => respond(location.href === binding.url ? {
|
|
48
|
+
...result, transactionId: binding.transactionId, token: binding.token, nonce: message.nonce,
|
|
49
|
+
url: location.href, targetVersion: binding.targetVersion, safetyOnly: true
|
|
50
|
+
} : blocked())).catch(() => respond(blocked())).finally(() => { inFlight = false; activeNonce = ''; });
|
|
51
|
+
return true;
|
|
52
|
+
});
|
|
53
|
+
return { ok: true, url: location.href };
|
|
54
|
+
}
|
|
55
|
+
let activeNonce = '';
|
|
56
|
+
root.CodexOverleafUpdateSafetyReceiver = Object.freeze({ install });
|
|
57
|
+
root.__codexOverleafUpdateSafetyReceiverInstalled = true;
|
|
58
|
+
})(globalThis);
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
function buildSyncApplyOperations(syncChanges = [], project = {}) {
|
|
11
11
|
const existingPaths = new Set((project.files || []).map(file => file.path));
|
|
12
12
|
return (syncChanges || []).map(change => {
|
|
13
|
+
// Keep an old Native move distinct so preflight can reject it safely.
|
|
14
|
+
// Falling through would turn a move into an empty create operation.
|
|
15
|
+
if (change.type === 'move') {
|
|
16
|
+
return { type: 'move', path: change.path, to: change.to,
|
|
17
|
+
moveProof: change.moveProof && typeof change.moveProof === 'object' ? { ...change.moveProof } : undefined,
|
|
18
|
+
reason: 'Move the existing Overleaf file while preserving its identity.' };
|
|
19
|
+
}
|
|
13
20
|
if (change.type === 'binary-create' || change.type === 'overwrite-binary') {
|
|
14
21
|
return {
|
|
15
22
|
type: change.type,
|
|
@@ -22,6 +29,7 @@
|
|
|
22
29
|
previousExists: change.previousExists === true,
|
|
23
30
|
previousKind: change.previousKind || '',
|
|
24
31
|
previousSize: Number.isFinite(Number(change.previousSize)) ? Number(change.previousSize) : undefined,
|
|
32
|
+
...(typeof change.previousSha256 === 'string' ? { previousSha256: change.previousSha256 } : {}),
|
|
25
33
|
reasonKey: change.type === 'overwrite-binary' ? 'localWorkspaceBinaryOverwrite' : 'localWorkspaceBinaryCreate',
|
|
26
34
|
reason: change.type === 'overwrite-binary'
|
|
27
35
|
? 'Synced a binary asset replacement from the local Codex workspace.'
|
|
@@ -158,14 +166,18 @@
|
|
|
158
166
|
|
|
159
167
|
if ((operation.type === 'rename' || operation.type === 'move') && operation.to) {
|
|
160
168
|
const previous = filesByPath.get(operation.path) || originalByPath.get(operation.path);
|
|
169
|
+
const destination = filesByPath.get(operation.to);
|
|
170
|
+
const binary = operation.moveProof?.kind === 'binary' || previous?.kind === 'binary';
|
|
161
171
|
filesByPath.delete(operation.path);
|
|
162
172
|
filesByPath.set(operation.to, {
|
|
163
173
|
...(previous || {}),
|
|
174
|
+
...(destination || {}),
|
|
164
175
|
path: operation.to,
|
|
165
|
-
kind: 'text',
|
|
166
|
-
content: result.verifiedContent
|
|
176
|
+
kind: binary ? 'binary' : 'text',
|
|
177
|
+
...(!binary ? { content: result.verifiedContent ?? destination?.content ?? previous?.content ?? '' } : {}),
|
|
167
178
|
source: 'verified-writeback'
|
|
168
179
|
});
|
|
180
|
+
if (binary) delete filesByPath.get(operation.to).content;
|
|
169
181
|
continue;
|
|
170
182
|
}
|
|
171
183
|
|
|
@@ -227,10 +239,15 @@
|
|
|
227
239
|
? changeOrReason
|
|
228
240
|
: { reason: changeOrReason };
|
|
229
241
|
const reason = change.reason;
|
|
242
|
+
if (reason === 'snapshot_content_unavailable') {
|
|
243
|
+
return isEnglish
|
|
244
|
+
? 'Current Overleaf content was not read; local changes were withheld until the file can be refreshed.'
|
|
245
|
+
: '未读取该文件当前的 Overleaf 内容;刷新取得内容前,本地改动不会写回。';
|
|
246
|
+
}
|
|
230
247
|
if (reason === 'selection_scope_violation') {
|
|
231
248
|
return isEnglish
|
|
232
|
-
? '
|
|
233
|
-
: '
|
|
249
|
+
? 'This local change exceeded the captured selection and was withheld. Independently valid selection changes may still be written.'
|
|
250
|
+
: '该项本地改动超出了已附加的选区,已阻止写回;独立且合法的选区内改动仍可写入。';
|
|
234
251
|
}
|
|
235
252
|
if (reason === 'generated_artifact') {
|
|
236
253
|
return isEnglish
|
|
@@ -314,6 +331,13 @@
|
|
|
314
331
|
if (!path || result.ok === false) { unsupported.push(path || 'unknown'); continue; }
|
|
315
332
|
if (operation.type === 'delete') { expected.set(path, { absent: true }); continue; }
|
|
316
333
|
if (operation.type === 'rename' || operation.type === 'move') {
|
|
334
|
+
const moveProof = result.moveProof || operation.moveProof;
|
|
335
|
+
if (moveProof?.v === 1 && moveProof.kind === 'binary' && /^[a-f0-9]{64}$/i.test(moveProof.sha256 || '')) {
|
|
336
|
+
if (!operation.to || (operation.moveProof?.sha256 && operation.moveProof.sha256 !== moveProof.sha256)) { unsupported.push(path); continue; }
|
|
337
|
+
if (operation.to !== path) expected.set(path, { absent: true });
|
|
338
|
+
expected.set(operation.to, { sha256: moveProof.sha256.toLowerCase() });
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
317
341
|
const content = typeof result.verifiedContent === 'string'
|
|
318
342
|
? result.verifiedContent : original.get(path)?.content;
|
|
319
343
|
if (!operation.to || typeof content !== 'string') { unsupported.push(path); continue; }
|
|
@@ -448,6 +472,30 @@
|
|
|
448
472
|
} };
|
|
449
473
|
}
|
|
450
474
|
|
|
475
|
+
async function buildMirrorWritebackConfirmation(applied = {}) {
|
|
476
|
+
const entries = Array.isArray(applied.applied) ? applied.applied : [];
|
|
477
|
+
if (!entries.length) return null;
|
|
478
|
+
const postImages = new Map();
|
|
479
|
+
for (const entry of entries) {
|
|
480
|
+
const operation = entry?.operation, result = entry?.result;
|
|
481
|
+
if (operation?.type !== 'edit' || typeof operation.path !== 'string' || !operation.path || result?.ok !== true
|
|
482
|
+
|| result.verified !== true || typeof result.verifiedContent !== 'string') return null;
|
|
483
|
+
// Several edits can target one file; the last verified readback is its post-image.
|
|
484
|
+
postImages.set(operation.path, result.verifiedContent);
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const expectedFiles = [];
|
|
488
|
+
for (const [path, content] of postImages) {
|
|
489
|
+
const sha256 = await textDigest(content);
|
|
490
|
+
if (!/^[a-f0-9]{64}$/.test(sha256)) return null;
|
|
491
|
+
expectedFiles.push({ path, sha256 });
|
|
492
|
+
}
|
|
493
|
+
return { paths: expectedFiles.map(file => file.path), expectedFiles };
|
|
494
|
+
} catch (_error) {
|
|
495
|
+
return null; // No digest evidence: use the authoritative snapshot fallback.
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
451
499
|
async function bytesDigest(bytes) {
|
|
452
500
|
if (!globalThis.crypto?.subtle) return '';
|
|
453
501
|
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
|
|
@@ -543,6 +591,7 @@
|
|
|
543
591
|
|
|
544
592
|
return {
|
|
545
593
|
confirmPostWriteSave,
|
|
594
|
+
buildMirrorWritebackConfirmation,
|
|
546
595
|
buildSaveCheck,
|
|
547
596
|
confirmSaveCheck,
|
|
548
597
|
buildSavedWritebackExpectations,
|