codex-overleaf-link 2.5.1 → 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 +149 -398
- package/README.zh-CN.md +153 -404
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +148 -3
- package/extension/src/backgroundSharedSessions.js +55 -2
- package/extension/src/backgroundUpdateCoordinator.js +128 -12
- package/extension/src/content/activeTurnControl.js +164 -7
- package/extension/src/content/composerAttachments.js +46 -10
- package/extension/src/content/composerPanel.js +92 -12
- package/extension/src/content/contentRuntime.js +1114 -321
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +147 -42
- package/extension/src/content/generated/content.bundle.js +5761 -1237
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +4 -0
- package/extension/src/content/localSkillsPanel.js +6 -4
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +53 -14
- package/extension/src/content/modelPicker.js +29 -19
- package/extension/src/content/nativeCompatibilityController.js +52 -19
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +7 -7
- package/extension/src/content/panelMaintenance.js +10 -2
- package/extension/src/content/panelRenderer.js +55 -28
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectSettingsCoordinator.js +30 -6
- package/extension/src/content/providerSettingsDialog.js +28 -19
- package/extension/src/content/recentProjects.js +30 -19
- package/extension/src/content/runActivitySummary.js +122 -14
- package/extension/src/content/runCompletionMeta.js +194 -6
- package/extension/src/content/runController.js +2 -1
- package/extension/src/content/runFailureNotice.js +8 -1
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +27 -13
- package/extension/src/content/runRecoveryActions.js +2 -1
- package/extension/src/content/runResultActions.js +73 -24
- package/extension/src/content/runReviewController.js +102 -37
- package/extension/src/content/runTimelineView.js +224 -25
- package/extension/src/content/scopedPersistencePanelState.js +124 -46
- package/extension/src/content/selectionContextView.js +30 -3
- package/extension/src/content/sensitivePreflight.js +14 -1
- package/extension/src/content/sessionManager.js +49 -3
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +15 -2
- package/extension/src/content/settingsWorkbench.js +127 -89
- package/extension/src/content/sharedSessionViewSync.js +13 -4
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateNotice.js +60 -6
- package/extension/src/content/writebackOrchestrator.js +301 -72
- package/extension/src/content/writingStyleSettings.js +70 -17
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/overleafCapabilities.js +4 -1
- 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/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +9 -1
- package/extension/src/page/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +172 -113
- package/extension/src/pageBridge.js +124 -73
- package/extension/src/popup.js +1 -1
- package/extension/src/shared/agentTranscript.js +372 -18
- package/extension/src/shared/auditRecords.js +3 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/compileAdapter.js +58 -17
- package/extension/src/shared/composerAttachmentDraft.js +27 -7
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/failureReasons.js +57 -3
- package/extension/src/shared/i18n.js +315 -29
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +2 -1
- 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/scopedPersistenceBrowserAdapter.js +34 -0
- package/extension/src/shared/sessionState.js +63 -4
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/sharedSessionBridge.js +81 -13
- package/extension/src/shared/staleGuard.js +130 -63
- package/extension/src/shared/storageDb.js +35 -3
- package/extension/src/shared/storageMigration.js +111 -2
- package/extension/src/shared/storageRunActions.js +62 -8
- package/extension/src/shared/undoOperations.js +126 -28
- package/extension/src/shared/updateRuntimeIdentity.js +16 -2
- package/extension/src/shared/writebackSettlement.js +21 -3
- package/extension/styles/panel.css +1293 -549
- package/native-host/src/bootstrapUpdate.js +52 -16
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +10 -1
- package/native-host/src/chatCompletionsBridge.js +29 -2
- package/native-host/src/codexPromptAssembly.js +86 -118
- package/native-host/src/codexSessionRunner.js +127 -17
- 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/providerContextError.js +20 -0
- 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 +28 -8
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/updateManager.js +144 -21
- package/native-host/src/writingStyleRuntime.js +1 -1
- package/package.json +5 -8
|
@@ -66,6 +66,8 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
66
66
|
'use strict';
|
|
67
67
|
|
|
68
68
|
const HOST_NAME = 'com.codex.overleaf';
|
|
69
|
+
const NATIVE_PING_TIMEOUT_MS = 20000;
|
|
70
|
+
const NATIVE_REQUEST_TIMEOUT_MS = 60000;
|
|
69
71
|
const MANAGED_UPDATE_STATE_KEY = 'codex-overleaf-managed-update-state-v1';
|
|
70
72
|
const MANAGED_UPDATE_CONSENT_KEY = 'codex-overleaf-update-consent-v1';
|
|
71
73
|
const MANAGED_UPDATE_TABS_KEY = 'codex-overleaf-managed-update-tabs-v1';
|
|
@@ -189,6 +191,17 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
189
191
|
.catch(error => sendResponse({ ok: false, journals: [], error: getErrorMessage(error, 'Journal read failed.') }));
|
|
190
192
|
return true;
|
|
191
193
|
}
|
|
194
|
+
if (message?.type === 'codex-overleaf/run-journal/known') {
|
|
195
|
+
if (!isAllowedOverleafSender(sender)) {
|
|
196
|
+
sendResponse({ ok: false, known: [] });
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
knownRunJournals(Array.isArray(message.requestIds) ? message.requestIds.map(String).slice(0, 64) : [],
|
|
200
|
+
Array.isArray(message.owners) ? message.owners.slice(0, 64) : [])
|
|
201
|
+
.then(result => sendResponse({ ok: true, ...result }))
|
|
202
|
+
.catch(error => sendResponse({ ok: false, known: [], error: getErrorMessage(error, 'Journal read failed.') }));
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
192
205
|
if (message?.type === 'codex-overleaf/run-journal/ack') {
|
|
193
206
|
if (!isAllowedOverleafSender(sender)) {
|
|
194
207
|
sendResponse({ ok: false });
|
|
@@ -266,6 +279,9 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
266
279
|
return;
|
|
267
280
|
}
|
|
268
281
|
if (message.type === 'release') {
|
|
282
|
+
const current = ownerBindings.get(portConnection);
|
|
283
|
+
if (!current || ['requestId', 'projectKey', 'clientRunId', 'sessionId'].some(key => current[key] !== message[key])
|
|
284
|
+
|| (current.browserOnly && current.ownerToken !== message.ownerToken)) return;
|
|
269
285
|
ownerBindings.delete(portConnection);
|
|
270
286
|
clearTimeout(ownerCancelTimers.get(requestId));
|
|
271
287
|
ownerCancelTimers.delete(requestId);
|
|
@@ -278,9 +294,20 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
278
294
|
documentId: String(portConnection.sender?.documentId || ''),
|
|
279
295
|
tabId: portConnection.sender?.tab?.id,
|
|
280
296
|
clientRunId: String(message.clientRunId || ''),
|
|
281
|
-
sessionId: String(message.sessionId || '')
|
|
297
|
+
sessionId: String(message.sessionId || ''),
|
|
298
|
+
browserOnly: message.browserOnly === true,
|
|
299
|
+
ownerToken: String(message.ownerToken || '')
|
|
282
300
|
};
|
|
301
|
+
if (binding.browserOnly && (!binding.projectKey || !binding.clientRunId || !binding.sessionId || !binding.ownerToken)) return;
|
|
302
|
+
if (binding.browserOnly && managedUpdateExecutionLocked) {
|
|
303
|
+
try { portConnection.postMessage?.({ type: 'bound', ...binding, ok: false }); } catch (_error) { /* port disconnected */ }
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
283
306
|
ownerBindings.set(portConnection, binding);
|
|
307
|
+
if (binding.browserOnly) {
|
|
308
|
+
try { portConnection.postMessage?.({ type: 'bound', ...binding }); } catch (_error) { /* port disconnected */ }
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
284
311
|
restoreRunJournal(binding).then(journal => {
|
|
285
312
|
if (ownerBindings.get(portConnection) !== binding || !journal || runJournals.get(requestId) !== journal) return;
|
|
286
313
|
clearTimeout(ownerCancelTimers.get(requestId));
|
|
@@ -295,7 +322,7 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
295
322
|
portConnection.onDisconnect.addListener(() => {
|
|
296
323
|
const binding = ownerBindings.get(portConnection);
|
|
297
324
|
ownerBindings.delete(portConnection);
|
|
298
|
-
if (!binding?.requestId) {
|
|
325
|
+
if (!binding?.requestId || binding.browserOnly) {
|
|
299
326
|
return;
|
|
300
327
|
}
|
|
301
328
|
const timer = setTimeout(() => interruptOwnerlessRun(binding), 150);
|
|
@@ -373,11 +400,18 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
373
400
|
method: request.method,
|
|
374
401
|
retryClass: getNativeRetryClass(request.method),
|
|
375
402
|
retryCount: 0,
|
|
403
|
+
deadlineTimer: null,
|
|
376
404
|
nativePort: null,
|
|
377
405
|
finalResponseReceived: false,
|
|
378
406
|
eventForwarded: false
|
|
379
407
|
};
|
|
380
408
|
pending.set(id, pendingRequest);
|
|
409
|
+
const timeoutMs = getNativeRequestTimeoutMs(request.method);
|
|
410
|
+
if (timeoutMs) {
|
|
411
|
+
pendingRequest.deadlineTimer = setTimeout(() => {
|
|
412
|
+
expireNativeRequest(id, pendingRequest);
|
|
413
|
+
}, timeoutMs);
|
|
414
|
+
}
|
|
381
415
|
if (request.method === 'codex.run') {
|
|
382
416
|
createRunJournal(id, pendingRequest);
|
|
383
417
|
}
|
|
@@ -390,12 +424,71 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
390
424
|
});
|
|
391
425
|
}
|
|
392
426
|
|
|
427
|
+
function getNativeRequestTimeoutMs(method) {
|
|
428
|
+
if (method === 'bridge.ping') return NATIVE_PING_TIMEOUT_MS;
|
|
429
|
+
switch (method) {
|
|
430
|
+
case 'mirror.status':
|
|
431
|
+
case 'codex.models':
|
|
432
|
+
case 'codex.providers.list':
|
|
433
|
+
case 'codex.providers.upsert':
|
|
434
|
+
case 'codex.providers.activate':
|
|
435
|
+
case 'codex.providers.clear-secret':
|
|
436
|
+
case 'codex.providers.delete':
|
|
437
|
+
case 'writing-style.get':
|
|
438
|
+
case 'writing-style.set':
|
|
439
|
+
case 'skills.list':
|
|
440
|
+
case 'asset.readChunk':
|
|
441
|
+
case 'update.status':
|
|
442
|
+
case 'update.check':
|
|
443
|
+
case 'update.canApply':
|
|
444
|
+
return NATIVE_REQUEST_TIMEOUT_MS;
|
|
445
|
+
default:
|
|
446
|
+
return 0;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function expireNativeRequest(id, pendingRequest) {
|
|
451
|
+
if (pending.get(id) !== pendingRequest || pendingRequest.finalResponseReceived) return;
|
|
452
|
+
pending.delete(id);
|
|
453
|
+
clearTimeout(pendingRequest.deadlineTimer);
|
|
454
|
+
pendingRequest.finalResponseReceived = true;
|
|
455
|
+
pendingRequest.resolve({
|
|
456
|
+
id,
|
|
457
|
+
ok: false,
|
|
458
|
+
error: {
|
|
459
|
+
code: 'native_request_timeout',
|
|
460
|
+
message: 'The local service did not respond.'
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
if (pendingRequest.method !== 'bridge.ping') return;
|
|
464
|
+
const failedPort = pendingRequest.nativePort;
|
|
465
|
+
if (!failedPort) return;
|
|
466
|
+
const hasActiveWork = Array.from(pending.values()).some(request => (
|
|
467
|
+
request.nativePort === failedPort && (
|
|
468
|
+
request.retryClass === 'safe_sync_retry' ||
|
|
469
|
+
(request.retryClass === 'no_silent_retry' && !getNativeRequestTimeoutMs(request.method))
|
|
470
|
+
)
|
|
471
|
+
));
|
|
472
|
+
// A failed diagnostic ping must not abort execution or mirror writes.
|
|
473
|
+
if (hasActiveWork) return;
|
|
474
|
+
if (port === failedPort) port = null;
|
|
475
|
+
// A hung Host is retired without replaying requests or reconnecting until
|
|
476
|
+
// the next caller asks, only when no execution or mirror write owns it.
|
|
477
|
+
for (const [pendingId, interrupted] of Array.from(pending.entries())) {
|
|
478
|
+
if (interrupted.nativePort === failedPort) {
|
|
479
|
+
rejectInterruptedNativeRequest(pendingId, interrupted, 'The local service did not respond.');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
try { failedPort.disconnect(); } catch (_error) { /* The port is already retired. */ }
|
|
483
|
+
}
|
|
484
|
+
|
|
393
485
|
async function requestManagedInternal(payload) {
|
|
394
486
|
if (managedBootstrapRuntime && ['update.status', 'update.confirm'].includes(payload?.method)) {
|
|
395
487
|
payload = { ...payload, params: { ...payload.params,
|
|
396
488
|
bootstrapRevision: String(globalThis.__CODEX_OVERLEAF_BOOTSTRAP_REVISION__ || '') } };
|
|
397
489
|
}
|
|
398
|
-
if (managedBootstrapRuntime && payload?.method === 'update.apply'
|
|
490
|
+
if (managedBootstrapRuntime && (payload?.method === 'update.apply'
|
|
491
|
+
|| (payload?.method === 'update.rollback' && payload?.params?.reasonCode === 'update_user_rollback'))) {
|
|
399
492
|
const gate = await verifyManagedUpdateSafety();
|
|
400
493
|
if (!gate.ok) {
|
|
401
494
|
return gate;
|
|
@@ -450,6 +543,7 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
450
543
|
}
|
|
451
544
|
const pendingState = globalThis.CodexOverleafNativeBridge?.getPendingState?.() || {};
|
|
452
545
|
const backgroundBlockers = Number(pendingState.executionRequests || 0) > 0
|
|
546
|
+
|| Array.from(ownerBindings.values()).some(binding => binding.browserOnly)
|
|
453
547
|
? ['background_execution_pending']
|
|
454
548
|
: [];
|
|
455
549
|
const blockers = [
|
|
@@ -813,6 +907,7 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
813
907
|
return;
|
|
814
908
|
}
|
|
815
909
|
pendingRequest.finalResponseReceived = true;
|
|
910
|
+
clearTimeout(pendingRequest.deadlineTimer);
|
|
816
911
|
pending.delete(id);
|
|
817
912
|
finalizeRunJournal(id, message).catch(() => {}).finally(() => {
|
|
818
913
|
pendingRequest.resolve(message);
|
|
@@ -874,6 +969,7 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
874
969
|
|
|
875
970
|
function rejectInterruptedNativeRequest(pendingId, pendingRequest, errorMessage) {
|
|
876
971
|
pending.delete(pendingId);
|
|
972
|
+
clearTimeout(pendingRequest.deadlineTimer);
|
|
877
973
|
finalizeRunJournal(pendingId, {
|
|
878
974
|
ok: false,
|
|
879
975
|
error: {
|
|
@@ -910,6 +1006,7 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
910
1006
|
if (pending.size === 1) {
|
|
911
1007
|
const [pendingId, pendingRequest] = pending.entries().next().value;
|
|
912
1008
|
pendingRequest.finalResponseReceived = true;
|
|
1009
|
+
clearTimeout(pendingRequest.deadlineTimer);
|
|
913
1010
|
pendingRequest.resolve({
|
|
914
1011
|
...message,
|
|
915
1012
|
id: pendingId
|
|
@@ -995,6 +1092,9 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
995
1092
|
clientRunId: String(params.clientRunId || ''),
|
|
996
1093
|
sessionId: String(params.clientSessionId || ''),
|
|
997
1094
|
task: sanitizeJournalString(params.task).slice(0, 12000),
|
|
1095
|
+
...(params.operation === 'compact' ? {
|
|
1096
|
+
operation: 'compact', threadId: String(params.threadId || '').slice(0, 160)
|
|
1097
|
+
} : {}),
|
|
998
1098
|
mode: String(params.mode || ''),
|
|
999
1099
|
model: String(params.model || ''),
|
|
1000
1100
|
ownerTabId: pendingRequest.tabId,
|
|
@@ -1176,6 +1276,51 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
1176
1276
|
return entries.slice(-16).map(([_key, value]) => value);
|
|
1177
1277
|
}
|
|
1178
1278
|
|
|
1279
|
+
// Browser-only retries outlive their original Native journal. Protect only
|
|
1280
|
+
// a live port whose request/project/session/run matches the queried record.
|
|
1281
|
+
async function knownRunJournals(requestIds, owners = []) {
|
|
1282
|
+
await Promise.allSettled(Array.from(journalLoads.values(), load => load.promise));
|
|
1283
|
+
await Promise.allSettled(Array.from(journalWrites.values()));
|
|
1284
|
+
const storage = chrome.storage?.session;
|
|
1285
|
+
if (!storage) throw new Error('Run journal storage is unavailable.');
|
|
1286
|
+
const values = requestIds.length ? await storage.get(requestIds.map(id => RUN_JOURNAL_PREFIX + id)) : {};
|
|
1287
|
+
// A persisted startup snapshot may lag behind its owner's Native dispatch.
|
|
1288
|
+
// Keep that same project/session/run alive through the request-id transition.
|
|
1289
|
+
const matches = (candidate, binding) => (candidate?.preDispatch === true
|
|
1290
|
+
? ['projectKey', 'clientRunId', 'sessionId'] : ['requestId', 'projectKey', 'clientRunId', 'sessionId'])
|
|
1291
|
+
.every(key => Boolean(candidate?.[key]) && candidate[key] === binding?.[key]);
|
|
1292
|
+
const known = requestIds.filter(id => runJournals.has(id) || Boolean(values?.[RUN_JOURNAL_PREFIX + id])
|
|
1293
|
+
|| owners.some(candidate => candidate?.requestId === id && Array.from(ownerBindings.values()).some(binding => matches(candidate, binding))));
|
|
1294
|
+
const missing = owners.filter(candidate => requestIds.includes(candidate?.requestId) && !known.includes(candidate.requestId));
|
|
1295
|
+
if (!missing.length) return { known, ownersChecked: true, preDispatchChecked: true };
|
|
1296
|
+
// A worker restart drops ports before hidden pages can process reconnect
|
|
1297
|
+
// timers. The page's existing health receiver can still report its binding.
|
|
1298
|
+
const unknown = () => ({ known, ownersChecked: false, preDispatchChecked: false, retryAfterMs: 1000 });
|
|
1299
|
+
const tabs = await chrome.tabs.query({ url: MANAGED_OVERLEAF_MATCHES }).catch(() => null);
|
|
1300
|
+
if (!Array.isArray(tabs)) return unknown();
|
|
1301
|
+
const projects = new Set(missing.map(candidate => candidate.projectKey));
|
|
1302
|
+
const relevant = tabs.filter(tab => isManagedEditorTab(tab) && !tab.discarded && tab.status !== 'unloaded'
|
|
1303
|
+
&& projects.has(new URL(tab.url).pathname.split('/')[2]));
|
|
1304
|
+
const responses = await Promise.all(relevant.map(async tab => {
|
|
1305
|
+
let timer;
|
|
1306
|
+
try {
|
|
1307
|
+
return await Promise.race([
|
|
1308
|
+
chrome.tabs.sendMessage(tab.id, { type: 'codex-overleaf/runtime-health-probe' }, { frameId: 0 }),
|
|
1309
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(null), 1500); })
|
|
1310
|
+
]);
|
|
1311
|
+
} catch (_error) { return null; }
|
|
1312
|
+
finally { clearTimeout(timer); }
|
|
1313
|
+
}));
|
|
1314
|
+
const needsStartupInventory = missing.some(candidate => candidate.preDispatch === true);
|
|
1315
|
+
if (responses.some(response => response?.ok !== true || ![1, 2].includes(response.runOwnerProtocol)
|
|
1316
|
+
|| (needsStartupInventory && response.runOwnerProtocol !== 2) || !Array.isArray(response.runOwners))) return unknown();
|
|
1317
|
+
for (const candidate of missing) {
|
|
1318
|
+
if (responses.some(response => response.runOwners.some(binding => matches(candidate, binding)))
|
|
1319
|
+
|| Array.from(ownerBindings.values()).some(binding => matches(candidate, binding))) known.push(candidate.requestId);
|
|
1320
|
+
}
|
|
1321
|
+
return { known, ownersChecked: true, preDispatchChecked: true };
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1179
1324
|
function pruneInMemoryRunJournals() {
|
|
1180
1325
|
if (runJournals.size <= 16) {
|
|
1181
1326
|
return;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
const clone = value => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
17
17
|
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
18
18
|
const failure = (code, message) => Object.assign(new Error(message), { code });
|
|
19
|
+
const conflict = () => failure('shared_session_conflict', 'The same session was edited in another tab. Refresh its history before retrying; the local draft was kept.');
|
|
19
20
|
function part(value) {
|
|
20
21
|
if (typeof value !== 'string' || !value.trim() || value.length > 256 || /[\0\r\n]/.test(value))
|
|
21
22
|
throw failure('shared_store_scope_invalid', 'The shared session identity is invalid.');
|
|
@@ -71,7 +72,59 @@
|
|
|
71
72
|
}
|
|
72
73
|
return result;
|
|
73
74
|
}
|
|
74
|
-
throw
|
|
75
|
+
throw conflict();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function titleValue(record) {
|
|
79
|
+
return { title: record.title || '', titleSource: record.titleSource === 'manual' ? 'manual' : 'auto' };
|
|
80
|
+
}
|
|
81
|
+
function manualTitleChange(base, next) {
|
|
82
|
+
return next.titleSource === 'manual' ? next.title !== base.title : base.titleSource === 'manual';
|
|
83
|
+
}
|
|
84
|
+
function mergeTitle(base, current, incoming) {
|
|
85
|
+
const before = titleValue(base), stored = titleValue(current), proposed = titleValue(incoming);
|
|
86
|
+
if (equal(proposed, before)) return stored;
|
|
87
|
+
if (equal(stored, before) || equal(stored, proposed)) return proposed;
|
|
88
|
+
// Auto derivations (including legacy normalization) carry no rename intent.
|
|
89
|
+
if (!manualTitleChange(before, proposed)) return stored;
|
|
90
|
+
if (!manualTitleChange(before, stored)) return proposed;
|
|
91
|
+
// The pair is one intent; merging its scalar members separately could
|
|
92
|
+
// combine a peer rename with this page's reset to auto.
|
|
93
|
+
throw conflict();
|
|
94
|
+
}
|
|
95
|
+
function draftConsumption(record) {
|
|
96
|
+
const marker = record.draftConsumption;
|
|
97
|
+
return record.task === '' && typeof marker?.task === 'string' && marker.task
|
|
98
|
+
&& typeof marker.runId === 'string' && marker.runId
|
|
99
|
+
&& (record.runs || []).some(run => run.id === marker.runId && !run.forkSnapshot
|
|
100
|
+
&& run.runProjectId === record.projectId) ? marker : null;
|
|
101
|
+
}
|
|
102
|
+
function mergeSession(base, current, incoming) {
|
|
103
|
+
const currentConsumption = draftConsumption(current), incomingConsumption = draftConsumption(incoming);
|
|
104
|
+
const consumed = marker => marker && marker.task === base.task && !equal(marker, base.draftConsumption);
|
|
105
|
+
let task;
|
|
106
|
+
if (equal(incoming.task, base.task)) task = current.task;
|
|
107
|
+
else if (equal(current.task, base.task) || equal(current.task, incoming.task)) task = incoming.task;
|
|
108
|
+
else if (consumed(incomingConsumption)) task = current.task;
|
|
109
|
+
else if (consumed(currentConsumption)) task = incoming.task;
|
|
110
|
+
else task = mergeValue(base.task, current.task, incoming.task, 'task');
|
|
111
|
+
|
|
112
|
+
const result = { ...mergeTitle(base, current, incoming) };
|
|
113
|
+
if (task !== undefined) result.task = clone(task);
|
|
114
|
+
// Summaries are derived metadata, never a second draft edit. Prefer the
|
|
115
|
+
// stored summary when both sides have the winning task (including legacy forms).
|
|
116
|
+
const taskOwner = equal(task, current.task) ? current : incoming;
|
|
117
|
+
if (taskOwner.safeTaskSummary !== undefined) result.safeTaskSummary = clone(taskOwner.safeTaskSummary);
|
|
118
|
+
if (task === '' && (incomingConsumption || currentConsumption))
|
|
119
|
+
result.draftConsumption = clone(consumed(incomingConsumption)
|
|
120
|
+
? incomingConsumption : currentConsumption || incomingConsumption);
|
|
121
|
+
const derived = new Set(['title', 'titleSource', 'task', 'safeTaskSummary', 'draftConsumption', 'sharedRevision']);
|
|
122
|
+
for (const key of new Set([...Object.keys(base), ...Object.keys(current), ...Object.keys(incoming)])) {
|
|
123
|
+
if (derived.has(key)) continue;
|
|
124
|
+
const value = mergeValue(base[key], current[key], incoming[key], key);
|
|
125
|
+
if (value !== undefined) Object.defineProperty(result, key, { value, enumerable: true, configurable: true, writable: true });
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
75
128
|
}
|
|
76
129
|
|
|
77
130
|
function createService(options = {}) {
|
|
@@ -232,7 +285,7 @@
|
|
|
232
285
|
if (!base) throw failure('shared_session_conflict', 'Reload the session before updating existing history.');
|
|
233
286
|
// Keep the established first-durable-review-decision policy.
|
|
234
287
|
const reviewed = options.mergeSessionReviewState?.(candidate, existing.record, message.reviewRunIds || []) || candidate;
|
|
235
|
-
next =
|
|
288
|
+
next = mergeSession(base, existing.record, reviewed);
|
|
236
289
|
} else if (base && !importing) {
|
|
237
290
|
throw failure('shared_session_deleted', 'The session no longer exists.');
|
|
238
291
|
}
|
|
@@ -37,8 +37,11 @@
|
|
|
37
37
|
'codex-overleaf/consent-update-cancel',
|
|
38
38
|
'codex-overleaf/consent-update-recover',
|
|
39
39
|
'codex-overleaf/consent-update-dismiss',
|
|
40
|
-
'codex-overleaf/consent-update-reload'
|
|
40
|
+
'codex-overleaf/consent-update-reload',
|
|
41
|
+
'codex-overleaf/consent-update-rollback'
|
|
41
42
|
]);
|
|
43
|
+
const USER_ROLLBACK_REASON = 'update_user_rollback';
|
|
44
|
+
const RECOVERABLE_TRANSACTION_STATES = new Set(['awaiting_health', 'bootstrap_pending', 'bootstrap_rolling_back']);
|
|
42
45
|
|
|
43
46
|
const policy = root.CodexOverleafUpdateConsent;
|
|
44
47
|
const revocation = root.CodexOverleafUpdateRevocation;
|
|
@@ -156,6 +159,8 @@
|
|
|
156
159
|
return dismissCompletedUpdate();
|
|
157
160
|
case 'codex-overleaf/consent-update-reload':
|
|
158
161
|
return reloadInstalledRuntime();
|
|
162
|
+
case 'codex-overleaf/consent-update-rollback':
|
|
163
|
+
return restorePreviousVersion();
|
|
159
164
|
default:
|
|
160
165
|
throw codedError('unknown_update_action', 'Unknown update action.');
|
|
161
166
|
}
|
|
@@ -174,14 +179,14 @@
|
|
|
174
179
|
code: '', message: '', blocker: '', blockers: [], cooldownUntil: 0, postponeUntil: LEGACY_GUARD }, { operation });
|
|
175
180
|
try {
|
|
176
181
|
const result = await withTimeout(requestNative('update.check', {
|
|
177
|
-
operationId: operation.id, currentVersion: currentVersion(), etag: currentState.etag || ''
|
|
182
|
+
operationId: operation.id, currentVersion: currentVersion(), etag: currentState.etag || '', manual: manual === true
|
|
178
183
|
}), CHECK_REQUEST_TIMEOUT_MS, codedError('update_check_timeout', 'The managed update check did not complete in time.'));
|
|
179
184
|
assertOperation(operation);
|
|
180
185
|
const checkedAt = Date.now();
|
|
181
186
|
if (result.available) {
|
|
182
187
|
const latestVersion = result.latestVersion || currentState.latestVersion;
|
|
183
188
|
await setConsentState({ ...consent,
|
|
184
|
-
...(latestVersion !== consent.snoozedVersion ? { snoozedVersion: '', snoozedUntil: 0 } : {}),
|
|
189
|
+
...(manual || latestVersion !== consent.snoozedVersion ? { snoozedVersion: '', snoozedUntil: 0 } : {}),
|
|
185
190
|
lastPromptedVersion: latestVersion,
|
|
186
191
|
lastPromptedAt: manual ? checkedAt : consent.lastPromptedAt
|
|
187
192
|
}, { operation });
|
|
@@ -198,7 +203,8 @@
|
|
|
198
203
|
currentVersion: result.currentVersion || currentVersion(), latestVersion: result.latestVersion,
|
|
199
204
|
managed: true, etag: result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
|
|
200
205
|
postponeUntil: LEGACY_GUARD, code: result.reason === 'prerelease_promotion_requires_manual_update'
|
|
201
|
-
? result.reason :
|
|
206
|
+
? result.reason : result.code === 'update_bootstrap_source_mismatch'
|
|
207
|
+
? result.code : 'update_bootstrap_upgrade_required', message: '', cooldownUntil: 0,
|
|
202
208
|
manualReason: result.manualInstall?.reason || {}, cancelRequested: false
|
|
203
209
|
}, { operation });
|
|
204
210
|
} else {
|
|
@@ -207,9 +213,11 @@
|
|
|
207
213
|
latestVersion: result.latestVersion || result.currentVersion || currentVersion(),
|
|
208
214
|
// A 304 without a verified Native candidate is a cache miss. Drop
|
|
209
215
|
// its validator so the next normal check can obtain fresh bytes.
|
|
210
|
-
|
|
216
|
+
// A skipped version also needs fresh bytes when checked explicitly.
|
|
217
|
+
etag: ['not_modified', 'cooldown'].includes(result.reason) ? '' : result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
|
|
211
218
|
postponeUntil: LEGACY_GUARD, transactionId: '',
|
|
212
|
-
code: result.reason === 'cooldown'
|
|
219
|
+
code: result.reason === 'cooldown'
|
|
220
|
+
? (result.cooldownReason === USER_ROLLBACK_REASON ? 'update_skipped_after_rollback' : 'update_cooldown') : '',
|
|
213
221
|
cooldownUntil: result.reason === 'cooldown' ? Number(result.cooldownUntil || 0) : 0,
|
|
214
222
|
manualReason: {}, message: '', cancelRequested: false
|
|
215
223
|
}, { operation });
|
|
@@ -508,7 +516,7 @@
|
|
|
508
516
|
async function cancelUpdateCore() {
|
|
509
517
|
const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
|
|
510
518
|
if (['applying', 'awaiting_health', 'rolling_back'].includes(state.state)
|
|
511
|
-
|| activeOperation?.phase
|
|
519
|
+
|| ['applying', 'rolling_back'].includes(activeOperation?.phase) || state.recoveryPending) {
|
|
512
520
|
throw codedError('update_revoke_too_late', 'The update is being installed or recovered.');
|
|
513
521
|
}
|
|
514
522
|
const operation = activeOperation;
|
|
@@ -563,6 +571,7 @@
|
|
|
563
571
|
|
|
564
572
|
|
|
565
573
|
function reconcileRecoveryState() {
|
|
574
|
+
if (activeOperation?.phase === 'rolling_back') return getUpdateState();
|
|
566
575
|
if (recoveryPromise) return recoveryPromise;
|
|
567
576
|
recoveryPromise = (async () => {
|
|
568
577
|
if (!await waitForBootstrapOwnership()) return getUpdateState();
|
|
@@ -652,10 +661,35 @@
|
|
|
652
661
|
if (transaction?.state === 'rolled_back' && status.installedAligned
|
|
653
662
|
&& status.activeVersion === transaction.sourceVersion
|
|
654
663
|
&& (!state.transactionId || transaction.id === state.transactionId)) {
|
|
655
|
-
const
|
|
664
|
+
const userRestore = transaction.reasonCode === USER_ROLLBACK_REASON;
|
|
665
|
+
if ((userRestore || state.code === 'update_rollback_unconfirmed')
|
|
666
|
+
&& !root.CodexOverleafUpdateRuntimeIdentity?.verifiedRollback(status)) {
|
|
667
|
+
throw codedError('update_rollback_unconfirmed', 'Rollback returned incomplete evidence.');
|
|
668
|
+
}
|
|
669
|
+
if (userRestore) {
|
|
670
|
+
const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
|
|
671
|
+
const tabs = await chrome.tabs.query({ url: OVERLEAF_MATCHES });
|
|
672
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
673
|
+
await chrome.storage.local.set({ [UPDATE_RELOAD_TABS_KEY]: [],
|
|
674
|
+
[MANUAL_RELOAD_TABS_KEY]: pending?.targetVersion === transaction.sourceVersion ? pending : {
|
|
675
|
+
targetVersion: transaction.sourceVersion, tabIds: tabs.filter(isUsableEditorTab).map(tab => tab.id)
|
|
676
|
+
} });
|
|
677
|
+
}
|
|
678
|
+
const value = { state: 'rolled_back', currentVersion: transaction.sourceVersion,
|
|
656
679
|
transactionId: transaction.id, recoveryPending: false, cancelRequested: false,
|
|
657
680
|
code: transaction.reasonCode || 'update_rolled_back', message: 'The previous version has been restored.'
|
|
658
|
-
}
|
|
681
|
+
};
|
|
682
|
+
const restored = userRestore
|
|
683
|
+
? (await setUpdateAndConsentState({ ...value, latestVersion: transaction.targetVersion,
|
|
684
|
+
postponeUntil: LEGACY_GUARD, etag: '', cooldownUntil: 0
|
|
685
|
+
}, policy.normalizeConsentState({ snoozedVersion: transaction.targetVersion,
|
|
686
|
+
snoozedUntil: Date.now() + SNOOZE_MS }), recoveryOptions)).updateState
|
|
687
|
+
: await setUpdateState(value, recoveryOptions);
|
|
688
|
+
runtimeStatus = null;
|
|
689
|
+
if (userRestore) {
|
|
690
|
+
try { await requireReloadSafePoint(); } catch (_error) { return restored; }
|
|
691
|
+
}
|
|
692
|
+
if (recoveryOwner.generation !== operationGeneration) return getUpdateState();
|
|
659
693
|
setTimeout(() => chrome.runtime.reload(), 0);
|
|
660
694
|
return restored;
|
|
661
695
|
}
|
|
@@ -765,9 +799,15 @@
|
|
|
765
799
|
now: Date.now()
|
|
766
800
|
});
|
|
767
801
|
view.runtime = runtime;
|
|
802
|
+
// Native decides availability: a committed pair with its source still on
|
|
803
|
+
// disk, or any unsettled installation that recovery must finish anyway.
|
|
804
|
+
view.restorePrevious = !state.cancelRequested && !activeOperation
|
|
805
|
+
&& (runtime.rollbackAvailable?.sourceVersion || (state.recoveryPending && state.transactionId))
|
|
806
|
+
? { version: runtime.rollbackAvailable?.sourceVersion || '' } : null;
|
|
807
|
+
view.actions = { ...view.actions, restorePrevious: Boolean(view.restorePrevious) };
|
|
768
808
|
const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
|
|
769
809
|
const pagesPending = runtime.state === 'aligned' && pending?.targetVersion === runtime.installedVersion && pending.tabIds?.length;
|
|
770
|
-
if (runtime.state === 'reload_required' || pagesPending) {
|
|
810
|
+
if (!state.recoveryPending && (runtime.state === 'reload_required' || pagesPending)) {
|
|
771
811
|
view.showPanel = true;
|
|
772
812
|
view.state = { ...state, state: pagesPending ? 'reload_tabs_required' : 'reload_required',
|
|
773
813
|
currentVersion: runtime.extensionVersion, latestVersion: runtime.installedVersion,
|
|
@@ -784,9 +824,9 @@
|
|
|
784
824
|
runtimeStatusRead = withTimeout(requestNative('update.status'), 4000,
|
|
785
825
|
codedError('update_status_timeout', 'Installed version inspection timed out.')).then(status => {
|
|
786
826
|
runtimeStatusAt = Date.now();
|
|
787
|
-
runtimeStatus = root.CodexOverleafUpdateRuntimeIdentity.inspectInstalledRuntime(status, {
|
|
827
|
+
runtimeStatus = { ...root.CodexOverleafUpdateRuntimeIdentity.inspectInstalledRuntime(status, {
|
|
788
828
|
extensionVersion: chrome.runtime.getManifest().version, runtimeVersion: currentVersion()
|
|
789
|
-
});
|
|
829
|
+
}), rollbackAvailable: status.rollbackAvailable || null };
|
|
790
830
|
return runtimeStatus;
|
|
791
831
|
}).catch(() => ({ state: 'unknown' })).finally(() => { runtimeStatusRead = null; });
|
|
792
832
|
return runtimeStatusRead;
|
|
@@ -808,6 +848,9 @@
|
|
|
808
848
|
}
|
|
809
849
|
|
|
810
850
|
async function reloadInstalledRuntime() {
|
|
851
|
+
if ((await getUpdateState()).recoveryPending) {
|
|
852
|
+
throw codedError('update_rollback_unconfirmed', 'Confirm update recovery before reloading.');
|
|
853
|
+
}
|
|
811
854
|
const runtime = await readRuntimeStatus(true);
|
|
812
855
|
if (runtime.state === 'aligned') { await finishManualRuntimeReload(); return getView(); }
|
|
813
856
|
if (runtime.state !== 'reload_required') throw codedError('update_reload_unavailable', 'No verified installed update is awaiting reload.');
|
|
@@ -819,7 +862,80 @@
|
|
|
819
862
|
return getView();
|
|
820
863
|
}
|
|
821
864
|
|
|
865
|
+
// Returns a committed update, or an update whose installation or recovery is
|
|
866
|
+
// unsettled, to the source pair Native still holds. Native owns the decision;
|
|
867
|
+
// the browser only supplies the transaction it observed and reloads after an
|
|
868
|
+
// exact readback.
|
|
869
|
+
async function restorePreviousVersion() {
|
|
870
|
+
if (activeOperation || cancellationPromise) {
|
|
871
|
+
throw codedError('update_transaction_in_progress', 'Another update operation is in progress.');
|
|
872
|
+
}
|
|
873
|
+
const operation = beginOperation();
|
|
874
|
+
operation.phase = 'rolling_back';
|
|
875
|
+
try {
|
|
876
|
+
const status = await requestNative('update.status');
|
|
877
|
+
assertOperation(operation);
|
|
878
|
+
const transaction = status.transaction;
|
|
879
|
+
const committed = transaction?.state === 'committed' && status.rollbackAvailable?.transactionId === transaction.id;
|
|
880
|
+
if (!transaction?.id || (!committed && !RECOVERABLE_TRANSACTION_STATES.has(transaction.state))) {
|
|
881
|
+
throw codedError('update_rollback_unavailable', 'No installed update can be restored to its previous version.');
|
|
882
|
+
}
|
|
883
|
+
// Page refresh happens after restoration, so it waits for the same saved
|
|
884
|
+
// and idle point as a reload of an installed update.
|
|
885
|
+
const tabs = await requireReloadSafePoint();
|
|
886
|
+
assertOperation(operation);
|
|
887
|
+
const generation = operation.generation;
|
|
888
|
+
clearIdleRetryTimer();
|
|
889
|
+
await setUpdateState({ state: 'rolling_back', transactionId: transaction.id,
|
|
890
|
+
currentVersion: transaction.targetVersion, latestVersion: transaction.targetVersion,
|
|
891
|
+
recoveryPending: false, cancelRequested: false, operationId: '', code: '', message: ''
|
|
892
|
+
}, { observed: true, generation });
|
|
893
|
+
try {
|
|
894
|
+
// Old source runtimes consume this key through their saved/idle check.
|
|
895
|
+
// The fixed Bootstrap's legacy list would refresh tabs unconditionally.
|
|
896
|
+
await chrome.storage.local.set({ [UPDATE_RELOAD_TABS_KEY]: [],
|
|
897
|
+
[MANUAL_RELOAD_TABS_KEY]: { targetVersion: transaction.sourceVersion, tabIds: tabs.map(tab => tab.id) } });
|
|
898
|
+
assertOperation(operation);
|
|
899
|
+
const rolledBack = await requestNative('update.rollback', { transactionId: transaction.id,
|
|
900
|
+
expectedState: transaction.state, reasonCode: USER_ROLLBACK_REASON });
|
|
901
|
+
assertOperation(operation);
|
|
902
|
+
const readback = await requestNative('update.status');
|
|
903
|
+
assertOperation(operation);
|
|
904
|
+
if (rolledBack.transactionId !== transaction.id || rolledBack.state !== 'rolled_back'
|
|
905
|
+
|| rolledBack.version !== transaction.sourceVersion || readback.transaction?.id !== transaction.id
|
|
906
|
+
|| readback.transaction.sourceVersion !== transaction.sourceVersion
|
|
907
|
+
|| readback.transaction.targetVersion !== transaction.targetVersion
|
|
908
|
+
|| !root.CodexOverleafUpdateRuntimeIdentity?.verifiedRollback({ ...readback,
|
|
909
|
+
transaction: { ...readback.transaction, bootstrapRevision: transaction.bootstrapRevision } })
|
|
910
|
+
|| !['extensionVersion', 'nativeVersion', 'extensionSha256', 'nativeSha256', 'bootstrapSha256']
|
|
911
|
+
.every(key => readback.restoredPair[key] === rolledBack.restoredPair?.[key])) {
|
|
912
|
+
throw codedError('update_rollback_unconfirmed', 'Rollback returned incomplete evidence.');
|
|
913
|
+
}
|
|
914
|
+
} catch (error) {
|
|
915
|
+
await setUpdateState({ state: 'failed', transactionId: transaction.id, recoveryPending: true,
|
|
916
|
+
cancelRequested: false, code: safeCode(error), message: safeMessage(error)
|
|
917
|
+
}, { observed: true, generation }).catch(() => {});
|
|
918
|
+
throw error;
|
|
919
|
+
}
|
|
920
|
+
await setUpdateAndConsentState({ state: 'rolled_back', transactionId: transaction.id,
|
|
921
|
+
currentVersion: transaction.sourceVersion, latestVersion: transaction.targetVersion,
|
|
922
|
+
recoveryPending: false, cancelRequested: false, blocker: '', blockers: [],
|
|
923
|
+
postponeUntil: LEGACY_GUARD, etag: '', cooldownUntil: 0,
|
|
924
|
+
code: USER_ROLLBACK_REASON, message: 'You restored the previous version.'
|
|
925
|
+
}, policy.normalizeConsentState({ snoozedVersion: transaction.targetVersion,
|
|
926
|
+
snoozedUntil: Date.now() + SNOOZE_MS }), { observed: true, generation });
|
|
927
|
+
runtimeStatus = null;
|
|
928
|
+
// The disk restore is complete even if new work now prevents a reload.
|
|
929
|
+
// Keep the existing Reload action available without attempting rollback again.
|
|
930
|
+
try { await requireReloadSafePoint(); } catch (_error) { return getView(); }
|
|
931
|
+
assertOperation(operation);
|
|
932
|
+
setTimeout(() => chrome.runtime.reload(), 0);
|
|
933
|
+
return getView();
|
|
934
|
+
} finally { finishOperation(operation); }
|
|
935
|
+
}
|
|
936
|
+
|
|
822
937
|
async function finishManualRuntimeReload() {
|
|
938
|
+
if ((await getUpdateState()).recoveryPending) return;
|
|
823
939
|
const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
|
|
824
940
|
if (!pending || !Array.isArray(pending.tabIds)) return;
|
|
825
941
|
const runtime = await readRuntimeStatus(true);
|