codex-overleaf-link 2.1.5 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -23
- package/extension/bootstrap/background.js +14 -1
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/runtime-manifest.json +10 -0
- package/extension/src/background.js +507 -8
- package/extension/src/backgroundUpdateCoordinator.js +284 -19
- package/extension/src/content/activeTurnControl.js +179 -0
- package/extension/src/content/composerPanel.js +14 -5
- package/extension/src/content/contentRuntime.js +330 -309
- package/extension/src/content/diagnosticsPanel.js +132 -1
- package/extension/src/content/markdownDomRenderer.js +451 -0
- package/extension/src/content/markdownText.js +86 -7
- package/extension/src/content/mathText.js +49 -4
- package/extension/src/content/modelPicker.js +14 -5
- package/extension/src/content/nativeChannel.js +7 -1
- package/extension/src/content/panelRenderer.js +112 -12
- package/extension/src/content/pendingInputView.js +106 -0
- package/extension/src/content/projectProviderSelection.js +68 -0
- package/extension/src/content/projectSessionCleanup.js +4 -0
- package/extension/src/content/providerSettingsCoordinator.js +170 -22
- package/extension/src/content/providerSettingsDialog.js +2 -2
- package/extension/src/content/recentProjects.js +7 -2
- package/extension/src/content/runGuidanceController.js +70 -0
- package/extension/src/content/runGuidanceView.js +48 -0
- package/extension/src/content/runQueueScheduler.js +260 -0
- package/extension/src/content/runTimelineView.js +33 -80
- package/extension/src/content/sessionManager.js +2 -2
- package/extension/src/content/sessionPersistence.js +62 -0
- package/extension/src/content/settingsPanel.js +1 -1
- package/extension/src/content/updateNotice.js +80 -1
- package/extension/src/page/writebackRouter.js +3 -9
- package/extension/src/shared/compatibility.js +2 -1
- package/extension/src/shared/i18n.js +38 -0
- package/extension/src/shared/providerProfiles.js +3 -2
- package/extension/src/shared/runInputQueue.js +253 -0
- package/extension/src/shared/sessionState.js +45 -22
- package/extension/src/shared/storageDb.js +39 -44
- package/extension/src/shared/storageMigration.js +123 -3
- package/extension/src/shared/storageRunActions.js +34 -1
- package/extension/src/shared/updateConsent.js +15 -4
- package/extension/styles/panel.css +319 -20
- package/extension/vendor/katex/katex.min.css +1 -1
- package/extension/vendor/markdown-it/LICENSE +22 -0
- package/extension/vendor/markdown-it/SOURCE.json +7 -0
- package/extension/vendor/markdown-it/VERSION +1 -0
- package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
- package/native-host/src/anthropicBridgeRequest.js +3 -3
- package/native-host/src/anthropicBridgeResponse.js +58 -17
- package/native-host/src/anthropicMessagesBridge.js +156 -23
- package/native-host/src/chatBridgeRequest.js +1 -1
- package/native-host/src/chatBridgeResponse.js +12 -8
- package/native-host/src/chatCompletionsBridge.js +22 -40
- package/native-host/src/codexPromptAssembly.js +3 -1
- package/native-host/src/codexProviderLaunch.js +6 -3
- package/native-host/src/codexSessionRunner.js +65 -65
- package/native-host/src/codexSessionTiming.js +45 -0
- package/native-host/src/index.js +58 -3
- package/native-host/src/managedInstall.js +45 -7
- package/native-host/src/mirrorWorkspace.js +15 -8
- package/native-host/src/providerProfile.js +2 -2
- package/native-host/src/providerStreamLifecycle.js +100 -0
- package/native-host/src/responsesPassthroughBridge.js +636 -0
- package/native-host/src/taskRunnerRuntime.js +81 -2
- package/native-host/src/updateArchive.js +4 -3
- package/native-host/src/updateManager.js +306 -19
- package/native-host/src/updateTrust.js +1 -1
- package/package.json +3 -1
|
@@ -3,13 +3,29 @@
|
|
|
3
3
|
|
|
4
4
|
const UPDATE_STATE_KEY = 'codex-overleaf-managed-update-state-v1';
|
|
5
5
|
const CONSENT_STATE_KEY = 'codex-overleaf-update-consent-v1';
|
|
6
|
+
const UPDATE_RELOAD_TABS_KEY = 'codex-overleaf-managed-update-tabs-v1';
|
|
6
7
|
const CHECK_ALARM = 'codex-overleaf-consent-update-check';
|
|
8
|
+
const IDLE_ALARM = 'codex-overleaf-consent-update-idle';
|
|
9
|
+
const WATCHDOG_ALARM = 'codex-overleaf-consent-update-watchdog';
|
|
7
10
|
const STARTUP_CHECK_SESSION_KEY = 'codex-overleaf-startup-update-check-v1';
|
|
8
11
|
const STARTUP_CHECK_CLAIMED_FLAG = '__codexOverleafStartupUpdateCheckClaimed';
|
|
9
12
|
const CHECK_INTERVAL_MINUTES = 24 * 60;
|
|
10
13
|
const SNOOZE_MS = 24 * 60 * 60 * 1000;
|
|
11
14
|
const CANDIDATE_MAX_AGE_MS = 5 * 60 * 1000;
|
|
12
15
|
const CHECK_REQUEST_TIMEOUT_MS = 15 * 1000;
|
|
16
|
+
const ACTIVATION_TIMEOUT_MS = 20 * 1000;
|
|
17
|
+
const ACTIVATION_POLL_MS = 250;
|
|
18
|
+
const FAST_IDLE_RETRY_MS = 4000;
|
|
19
|
+
const SLOW_IDLE_RETRY_MS = 15000;
|
|
20
|
+
const FAST_IDLE_RETRY_BLOCKERS = new Set(['recent_user_activity', 'save_state_not_stable']);
|
|
21
|
+
const WATCHDOG_INTERVAL_MINUTES = 1;
|
|
22
|
+
const PHASE_TIMEOUT_MS = Object.freeze({
|
|
23
|
+
checking: 30 * 1000,
|
|
24
|
+
downloading: 2 * 60 * 1000,
|
|
25
|
+
applying: 90 * 1000,
|
|
26
|
+
awaiting_health: 2 * 60 * 1000,
|
|
27
|
+
rolling_back: 60 * 1000
|
|
28
|
+
});
|
|
13
29
|
const LEGACY_GUARD = Number.MAX_SAFE_INTEGER;
|
|
14
30
|
const OVERLEAF_MATCHES = [
|
|
15
31
|
'https://www.overleaf.com/project',
|
|
@@ -29,6 +45,8 @@
|
|
|
29
45
|
let nativeBridge = null;
|
|
30
46
|
let initialized = false;
|
|
31
47
|
let policyTail = Promise.resolve();
|
|
48
|
+
let idleRetryTimer = null;
|
|
49
|
+
let activationPromise = null;
|
|
32
50
|
|
|
33
51
|
function init(options = {}) {
|
|
34
52
|
if (initialized || !policy) return;
|
|
@@ -51,6 +69,12 @@
|
|
|
51
69
|
if (alarm?.name === CHECK_ALARM) {
|
|
52
70
|
void enqueuePolicyAction(() => checkOnly({ manual: false })).catch(() => {});
|
|
53
71
|
}
|
|
72
|
+
if (alarm?.name === WATCHDOG_ALARM) {
|
|
73
|
+
void enqueuePolicyAction(() => reconcileExpiredPhase()).catch(() => {});
|
|
74
|
+
}
|
|
75
|
+
if (alarm?.name === IDLE_ALARM) {
|
|
76
|
+
void enqueuePolicyAction(() => tryActivateStagedUpdate()).catch(() => {});
|
|
77
|
+
}
|
|
54
78
|
});
|
|
55
79
|
|
|
56
80
|
chrome.storage?.onChanged?.addListener((changes, area) => {
|
|
@@ -66,10 +90,19 @@
|
|
|
66
90
|
delayInMinutes: 0.5,
|
|
67
91
|
periodInMinutes: CHECK_INTERVAL_MINUTES
|
|
68
92
|
});
|
|
93
|
+
chrome.alarms?.create?.(WATCHDOG_ALARM, {
|
|
94
|
+
delayInMinutes: WATCHDOG_INTERVAL_MINUTES,
|
|
95
|
+
periodInMinutes: WATCHDOG_INTERVAL_MINUTES
|
|
96
|
+
});
|
|
69
97
|
await recoverInterruptedCheck();
|
|
98
|
+
await reconcileExpiredPhase();
|
|
70
99
|
await settleTerminalConsent();
|
|
71
100
|
await armLegacyGuard();
|
|
72
101
|
await publishView();
|
|
102
|
+
const updateState = await getUpdateState();
|
|
103
|
+
if (['staged', 'waiting_for_idle'].includes(updateState.state)) {
|
|
104
|
+
scheduleIdleRetry(updateState.blockers);
|
|
105
|
+
}
|
|
73
106
|
if (await claimStartupCheck()) {
|
|
74
107
|
void enqueuePolicyAction(() => checkOnly({ manual: false })).catch(() => {});
|
|
75
108
|
}
|
|
@@ -115,6 +148,7 @@
|
|
|
115
148
|
await setUpdateState({
|
|
116
149
|
...currentState,
|
|
117
150
|
state: 'checking',
|
|
151
|
+
initiatedBy: manual ? 'manual' : 'automatic',
|
|
118
152
|
code: '',
|
|
119
153
|
message: '',
|
|
120
154
|
blocker: '',
|
|
@@ -181,6 +215,7 @@
|
|
|
181
215
|
await setUpdateState({
|
|
182
216
|
...currentState,
|
|
183
217
|
state: 'failed',
|
|
218
|
+
initiatedBy: manual ? 'manual' : 'automatic',
|
|
184
219
|
postponeUntil: LEGACY_GUARD,
|
|
185
220
|
code: safeCode(error),
|
|
186
221
|
message: safeMessage(error)
|
|
@@ -239,25 +274,27 @@
|
|
|
239
274
|
}
|
|
240
275
|
|
|
241
276
|
const authorizationId = crypto.randomUUID();
|
|
242
|
-
await requestNative('update.authorize', {
|
|
243
|
-
authorizationId,
|
|
244
|
-
targetVersion: state.latestVersion,
|
|
245
|
-
currentVersion: currentVersion()
|
|
246
|
-
});
|
|
247
|
-
await setConsentState({
|
|
248
|
-
...consent,
|
|
249
|
-
snoozedVersion: '',
|
|
250
|
-
snoozedUntil: 0,
|
|
251
|
-
authorizedVersion: state.latestVersion,
|
|
252
|
-
authorizationId,
|
|
253
|
-
authorizedAt: Date.now()
|
|
254
|
-
});
|
|
255
|
-
await setUpdateState({ ...state, postponeUntil: 0, code: '', message: '' });
|
|
256
|
-
|
|
257
277
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
278
|
+
await requestNative('update.authorize', {
|
|
279
|
+
authorizationId,
|
|
280
|
+
targetVersion: state.latestVersion,
|
|
281
|
+
currentVersion: currentVersion()
|
|
282
|
+
});
|
|
283
|
+
await setConsentState({
|
|
284
|
+
...consent,
|
|
285
|
+
snoozedVersion: '',
|
|
286
|
+
snoozedUntil: 0,
|
|
287
|
+
authorizedVersion: state.latestVersion,
|
|
288
|
+
authorizationId,
|
|
289
|
+
authorizedAt: Date.now()
|
|
290
|
+
});
|
|
291
|
+
await setUpdateState({ ...state, postponeUntil: 0, code: '', message: '' });
|
|
292
|
+
const executor = globalThis.CodexOverleafManagedUpdateExecutor;
|
|
293
|
+
if (!executor || typeof executor.installAuthorizedUpdate !== 'function') {
|
|
294
|
+
throw codedError(
|
|
295
|
+
'update_executor_unavailable',
|
|
296
|
+
'The managed update executor is unavailable.'
|
|
297
|
+
);
|
|
261
298
|
}
|
|
262
299
|
await executor.installAuthorizedUpdate();
|
|
263
300
|
return getView();
|
|
@@ -273,11 +310,177 @@
|
|
|
273
310
|
authorizationId: '',
|
|
274
311
|
authorizedAt: 0
|
|
275
312
|
});
|
|
313
|
+
const observed = await getUpdateState().catch(() => state);
|
|
314
|
+
if (!['awaiting_health', 'committed', 'rolled_back'].includes(observed.state)) {
|
|
315
|
+
await setUpdateState({
|
|
316
|
+
...observed,
|
|
317
|
+
state: 'failed',
|
|
318
|
+
initiatedBy: 'manual',
|
|
319
|
+
blocker: '',
|
|
320
|
+
blockers: [],
|
|
321
|
+
code: safeCode(error),
|
|
322
|
+
message: safeMessage(error)
|
|
323
|
+
});
|
|
324
|
+
}
|
|
276
325
|
await armLegacyGuard();
|
|
277
326
|
throw error;
|
|
278
327
|
}
|
|
279
328
|
}
|
|
280
329
|
|
|
330
|
+
async function stageAuthorizedUpdate() {
|
|
331
|
+
const current = await getUpdateState();
|
|
332
|
+
await setUpdateState({
|
|
333
|
+
...current,
|
|
334
|
+
state: 'downloading',
|
|
335
|
+
initiatedBy: 'manual',
|
|
336
|
+
blocker: '',
|
|
337
|
+
blockers: [],
|
|
338
|
+
code: '',
|
|
339
|
+
message: ''
|
|
340
|
+
});
|
|
341
|
+
const staged = await requestNative('update.stage');
|
|
342
|
+
await setUpdateState({
|
|
343
|
+
...current,
|
|
344
|
+
state: 'staged',
|
|
345
|
+
initiatedBy: 'manual',
|
|
346
|
+
latestVersion: staged.targetVersion,
|
|
347
|
+
transactionId: staged.transactionId,
|
|
348
|
+
stagedAt: Date.now(),
|
|
349
|
+
blocker: '',
|
|
350
|
+
blockers: [],
|
|
351
|
+
code: '',
|
|
352
|
+
message: ''
|
|
353
|
+
});
|
|
354
|
+
return tryActivateStagedUpdate();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function tryActivateStagedUpdate() {
|
|
358
|
+
const executor = root.CodexOverleafManagedUpdateExecutor;
|
|
359
|
+
if (typeof executor?.installAuthorizedUpdate === 'function') {
|
|
360
|
+
if (activationPromise) return activationPromise;
|
|
361
|
+
activationPromise = Promise.resolve(executor.installAuthorizedUpdate()).finally(() => {
|
|
362
|
+
activationPromise = null;
|
|
363
|
+
});
|
|
364
|
+
return activationPromise;
|
|
365
|
+
}
|
|
366
|
+
if (activationPromise) return activationPromise;
|
|
367
|
+
activationPromise = tryActivateStagedUpdateCore().finally(() => {
|
|
368
|
+
activationPromise = null;
|
|
369
|
+
});
|
|
370
|
+
return activationPromise;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function tryActivateStagedUpdateCore() {
|
|
374
|
+
clearIdleRetryTimer();
|
|
375
|
+
const state = await getUpdateState();
|
|
376
|
+
if (!['staged', 'waiting_for_idle'].includes(state.state)) {
|
|
377
|
+
await chrome.alarms?.clear?.(IDLE_ALARM).catch(() => {});
|
|
378
|
+
return state;
|
|
379
|
+
}
|
|
380
|
+
const surfaceTabs = await chrome.tabs.query({ url: OVERLEAF_MATCHES }).catch(() => []);
|
|
381
|
+
const editorTabs = surfaceTabs.filter(isUsableEditorTab);
|
|
382
|
+
const reloadTabs = surfaceTabs
|
|
383
|
+
.filter(tab => Number.isInteger(tab?.id) && !tab.discarded && tab.status !== 'unloaded')
|
|
384
|
+
.map(tab => tab.id);
|
|
385
|
+
const probes = await Promise.all(editorTabs.map(tab => probeTabIdle(tab.id)));
|
|
386
|
+
const nativeGate = await requestNative('update.canApply')
|
|
387
|
+
.then(result => ({ ok: true, result }))
|
|
388
|
+
.catch(error => ({ ok: false, error: safeError(error) }));
|
|
389
|
+
const blockers = root.CodexOverleafUpdateStatus?.collectBlockers(probes, nativeGate) || ['busy'];
|
|
390
|
+
if (blockers.length) {
|
|
391
|
+
const waiting = await setUpdateState({
|
|
392
|
+
...state,
|
|
393
|
+
state: 'waiting_for_idle',
|
|
394
|
+
blocker: blockers[0],
|
|
395
|
+
blockers
|
|
396
|
+
});
|
|
397
|
+
scheduleIdleRetry(blockers);
|
|
398
|
+
return waiting;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
await chrome.alarms?.clear?.(IDLE_ALARM).catch(() => {});
|
|
402
|
+
await chrome.storage.local.set({ [UPDATE_RELOAD_TABS_KEY]: reloadTabs });
|
|
403
|
+
await setUpdateState({
|
|
404
|
+
...state,
|
|
405
|
+
state: 'applying',
|
|
406
|
+
blocker: '',
|
|
407
|
+
blockers: [],
|
|
408
|
+
code: '',
|
|
409
|
+
message: ''
|
|
410
|
+
});
|
|
411
|
+
const activation = await requestNative('update.activate', {
|
|
412
|
+
transactionId: state.transactionId
|
|
413
|
+
});
|
|
414
|
+
const transaction = await waitForActivatedTransaction(activation.transactionId || state.transactionId);
|
|
415
|
+
if (transaction.state === 'rolled_back') {
|
|
416
|
+
throw codedError(transaction.reasonCode || 'update_apply_failed', 'The managed update was rolled back during activation.');
|
|
417
|
+
}
|
|
418
|
+
await setUpdateState({
|
|
419
|
+
...state,
|
|
420
|
+
state: 'awaiting_health',
|
|
421
|
+
latestVersion: transaction.targetVersion,
|
|
422
|
+
transactionId: transaction.id,
|
|
423
|
+
code: '',
|
|
424
|
+
message: ''
|
|
425
|
+
});
|
|
426
|
+
chrome.runtime.reload();
|
|
427
|
+
return { state: 'awaiting_health' };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function waitForActivatedTransaction(transactionId) {
|
|
431
|
+
const deadline = Date.now() + ACTIVATION_TIMEOUT_MS;
|
|
432
|
+
while (Date.now() <= deadline) {
|
|
433
|
+
const status = await requestNative('update.status').catch(() => null);
|
|
434
|
+
const transaction = status?.transaction;
|
|
435
|
+
if (transaction?.id === transactionId && ['awaiting_health', 'rolled_back'].includes(transaction.state)) {
|
|
436
|
+
return transaction;
|
|
437
|
+
}
|
|
438
|
+
await new Promise(resolve => setTimeout(resolve, ACTIVATION_POLL_MS));
|
|
439
|
+
}
|
|
440
|
+
throw codedError('update_activation_timeout', 'The managed update did not reach health verification after activation.');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function scheduleIdleRetry(blockers = []) {
|
|
444
|
+
clearIdleRetryTimer();
|
|
445
|
+
const values = Array.isArray(blockers) ? blockers.filter(Boolean) : [];
|
|
446
|
+
const fast = values.length > 0 && values.every(value => FAST_IDLE_RETRY_BLOCKERS.has(value));
|
|
447
|
+
idleRetryTimer = setTimeout(() => {
|
|
448
|
+
idleRetryTimer = null;
|
|
449
|
+
void enqueuePolicyAction(() => tryActivateStagedUpdate()).catch(() => {});
|
|
450
|
+
}, fast ? FAST_IDLE_RETRY_MS : SLOW_IDLE_RETRY_MS);
|
|
451
|
+
chrome.alarms?.create?.(IDLE_ALARM, { delayInMinutes: 0.5 });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function clearIdleRetryTimer() {
|
|
455
|
+
if (idleRetryTimer === null) return;
|
|
456
|
+
clearTimeout(idleRetryTimer);
|
|
457
|
+
idleRetryTimer = null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function isUsableEditorTab(tab) {
|
|
461
|
+
if (!Number.isInteger(tab?.id) || tab.discarded || tab.status === 'unloaded') return false;
|
|
462
|
+
try {
|
|
463
|
+
const url = new URL(tab.url || '');
|
|
464
|
+
return url.protocol === 'https:' &&
|
|
465
|
+
(url.hostname === 'www.overleaf.com' || url.hostname === 'overleaf.com') &&
|
|
466
|
+
/^\/project\/[^/]+(?:\/|$)/.test(url.pathname);
|
|
467
|
+
} catch (_error) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function probeTabIdle(tabId) {
|
|
473
|
+
try {
|
|
474
|
+
return await withTimeout(
|
|
475
|
+
chrome.tabs.sendMessage(tabId, { type: 'codex-overleaf/update-idle-probe' }),
|
|
476
|
+
3500,
|
|
477
|
+
codedError('tab_probe_timeout', 'An Overleaf tab did not answer the idle check in time.')
|
|
478
|
+
);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
return { idle: false, blockers: [safeCode(error) === 'tab_probe_timeout' ? 'tab_probe_timeout' : 'tab_probe_unavailable'] };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
281
484
|
async function postponeUpdate() {
|
|
282
485
|
const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
|
|
283
486
|
if (['applying', 'awaiting_health'].includes(state.state)) {
|
|
@@ -301,6 +504,8 @@
|
|
|
301
504
|
}
|
|
302
505
|
|
|
303
506
|
const snoozedUntil = Date.now() + SNOOZE_MS;
|
|
507
|
+
clearIdleRetryTimer();
|
|
508
|
+
await chrome.alarms?.clear?.(IDLE_ALARM).catch(() => {});
|
|
304
509
|
await setUpdateState({
|
|
305
510
|
...state,
|
|
306
511
|
state: 'update_available',
|
|
@@ -334,6 +539,55 @@
|
|
|
334
539
|
});
|
|
335
540
|
}
|
|
336
541
|
|
|
542
|
+
async function reconcileExpiredPhase() {
|
|
543
|
+
const state = await getUpdateState();
|
|
544
|
+
if (!state.deadlineAt || Date.now() <= state.deadlineAt || policy.isTerminalState(state.state)) {
|
|
545
|
+
return state;
|
|
546
|
+
}
|
|
547
|
+
if (['applying', 'awaiting_health', 'rolling_back'].includes(state.state)) {
|
|
548
|
+
const status = await requestNative('update.status').catch(() => null);
|
|
549
|
+
const transaction = status?.transaction;
|
|
550
|
+
if (transaction?.state === 'awaiting_health') {
|
|
551
|
+
await requestNative('update.rollback', {
|
|
552
|
+
transactionId: transaction.id,
|
|
553
|
+
reasonCode: 'update_health_timeout'
|
|
554
|
+
}).catch(() => null);
|
|
555
|
+
return setUpdateState({
|
|
556
|
+
...state,
|
|
557
|
+
state: 'failed',
|
|
558
|
+
currentVersion: transaction.sourceVersion || state.currentVersion,
|
|
559
|
+
code: 'update_health_timeout',
|
|
560
|
+
message: 'The updated runtime did not complete its health check and was rolled back. Retry, or use the manual update command.'
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
if (transaction?.state === 'committed') {
|
|
564
|
+
return setUpdateState({
|
|
565
|
+
...state,
|
|
566
|
+
state: 'committed',
|
|
567
|
+
currentVersion: transaction.targetVersion,
|
|
568
|
+
latestVersion: transaction.targetVersion,
|
|
569
|
+
code: '',
|
|
570
|
+
message: ''
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (transaction?.state === 'rolled_back') {
|
|
574
|
+
return setUpdateState({
|
|
575
|
+
...state,
|
|
576
|
+
state: 'rolled_back',
|
|
577
|
+
currentVersion: transaction.sourceVersion || state.currentVersion,
|
|
578
|
+
code: transaction.reasonCode || 'update_rolled_back',
|
|
579
|
+
message: 'The managed update was rolled back after activation did not complete.'
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return setUpdateState({
|
|
584
|
+
...state,
|
|
585
|
+
state: 'failed',
|
|
586
|
+
code: 'update_phase_timeout',
|
|
587
|
+
message: 'The managed update phase did not complete in time. Retry, or use the manual update command.'
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
337
591
|
async function handleObservedStateChange() {
|
|
338
592
|
await settleTerminalConsent();
|
|
339
593
|
const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
|
|
@@ -388,7 +642,18 @@
|
|
|
388
642
|
}
|
|
389
643
|
|
|
390
644
|
async function setUpdateState(value) {
|
|
391
|
-
const
|
|
645
|
+
const stored = await chrome.storage.local.get(UPDATE_STATE_KEY);
|
|
646
|
+
const previous = policy.normalizeUpdateState(stored?.[UPDATE_STATE_KEY], currentVersion());
|
|
647
|
+
const now = Date.now();
|
|
648
|
+
const changedPhase = value.state && value.state !== previous.state;
|
|
649
|
+
const timeoutMs = PHASE_TIMEOUT_MS[value.state] || 0;
|
|
650
|
+
const next = policy.normalizeUpdateState({
|
|
651
|
+
...value,
|
|
652
|
+
initiatedBy: value.initiatedBy || previous.initiatedBy,
|
|
653
|
+
phaseStartedAt: changedPhase ? now : (value.phaseStartedAt || previous.phaseStartedAt),
|
|
654
|
+
deadlineAt: changedPhase ? (timeoutMs ? now + timeoutMs : 0) : (value.deadlineAt || previous.deadlineAt),
|
|
655
|
+
heartbeatAt: now
|
|
656
|
+
}, currentVersion());
|
|
392
657
|
await chrome.storage.local.set({ [UPDATE_STATE_KEY]: next });
|
|
393
658
|
return next;
|
|
394
659
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
(function initCodexOverleafActiveTurnControl(root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) {
|
|
3
|
+
module.exports = factory();
|
|
4
|
+
} else {
|
|
5
|
+
root.CodexOverleafActiveTurnControl = factory();
|
|
6
|
+
}
|
|
7
|
+
})(typeof window !== 'undefined' ? window : globalThis, function activeTurnControlFactory() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
function create(options = {}) {
|
|
11
|
+
const runtime = options.chrome?.runtime;
|
|
12
|
+
let port = null;
|
|
13
|
+
let binding = null;
|
|
14
|
+
let reconnectTimer = null;
|
|
15
|
+
let destroyed = false;
|
|
16
|
+
|
|
17
|
+
function connect() {
|
|
18
|
+
if (destroyed || port || typeof runtime?.connect !== 'function') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const nextPort = runtime.connect({ name: 'codex-overleaf/run-owner' });
|
|
23
|
+
port = nextPort;
|
|
24
|
+
nextPort.onDisconnect?.addListener?.(() => {
|
|
25
|
+
if (port === nextPort) {
|
|
26
|
+
port = null;
|
|
27
|
+
}
|
|
28
|
+
if (!destroyed) {
|
|
29
|
+
reconnectTimer = setTimeout(connect, 50);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (binding) {
|
|
33
|
+
postBinding('bind', binding);
|
|
34
|
+
}
|
|
35
|
+
} catch (_error) {
|
|
36
|
+
port = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function postBinding(type, value) {
|
|
41
|
+
try {
|
|
42
|
+
port?.postMessage?.({ type, ...value });
|
|
43
|
+
} catch (_error) {
|
|
44
|
+
port = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bind(value = {}) {
|
|
49
|
+
binding = {
|
|
50
|
+
requestId: String(value.requestId || ''),
|
|
51
|
+
projectKey: String(value.projectKey || ''),
|
|
52
|
+
clientRunId: String(value.clientRunId || ''),
|
|
53
|
+
sessionId: String(value.sessionId || '')
|
|
54
|
+
};
|
|
55
|
+
connect();
|
|
56
|
+
postBinding('bind', binding);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function release(requestId) {
|
|
60
|
+
if (!binding || (requestId && binding.requestId !== requestId)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
postBinding('release', binding);
|
|
64
|
+
binding = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function listJournals(projectKey) {
|
|
68
|
+
if (typeof runtime?.sendMessage !== 'function') {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const response = await runtime.sendMessage({
|
|
72
|
+
type: 'codex-overleaf/run-journal/list',
|
|
73
|
+
projectKey: String(projectKey || '')
|
|
74
|
+
});
|
|
75
|
+
return response?.ok && Array.isArray(response.journals) ? response.journals : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function acknowledge(requestId) {
|
|
79
|
+
if (!requestId || typeof runtime?.sendMessage !== 'function') {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const response = await runtime.sendMessage({
|
|
83
|
+
type: 'codex-overleaf/run-journal/ack',
|
|
84
|
+
requestId
|
|
85
|
+
});
|
|
86
|
+
return response?.ok === true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function recoverJournals(options = {}) {
|
|
90
|
+
const projectKey = String(options.projectKey || '');
|
|
91
|
+
if (!projectKey) {
|
|
92
|
+
return { changed: false, acknowledgeIds: [] };
|
|
93
|
+
}
|
|
94
|
+
const journals = await listJournals(projectKey).catch(() => []);
|
|
95
|
+
let changed = false;
|
|
96
|
+
const acknowledgeIds = [];
|
|
97
|
+
for (const journal of journals) {
|
|
98
|
+
if (!journal?.terminal && !journal?.ownerLost) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const session = options.findSession?.(journal.sessionId);
|
|
102
|
+
if (!session) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let record = (session.runs || []).find(run => run.id === journal.clientRunId);
|
|
106
|
+
if (!record && journal.clientRunId) {
|
|
107
|
+
record = options.createInterruptedRun?.(journal, projectKey);
|
|
108
|
+
if (record) {
|
|
109
|
+
session.runs = [...(session.runs || []), record].slice(-20);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!record) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const reloadReports = (record.events || []).filter(event => event?.failure?.source === 'panel_reload');
|
|
116
|
+
record.events = (record.events || []).filter(event => event?.failure?.source !== 'panel_reload');
|
|
117
|
+
const lastSequence = Number(record.nativeEventSeq || 0);
|
|
118
|
+
for (const entry of Array.isArray(journal.events) ? journal.events : []) {
|
|
119
|
+
const sequence = Number(entry?.sequence || 0);
|
|
120
|
+
if (sequence <= lastSequence) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const raw = entry?.event || {};
|
|
124
|
+
if (raw.type === 'codex.turn.bound') {
|
|
125
|
+
session.codexThreadId = String(raw.detail?.threadId || session.codexThreadId || '');
|
|
126
|
+
record.codexTurnId = String(raw.detail?.turnId || record.codexTurnId || '');
|
|
127
|
+
record.nativeEventSeq = sequence;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const recovered = options.mapEvent?.(raw, journal);
|
|
131
|
+
if (recovered) {
|
|
132
|
+
if (recovered.kind === 'stream') {
|
|
133
|
+
options.upsertStream?.(record, recovered);
|
|
134
|
+
} else {
|
|
135
|
+
record.events = [...(record.events || []), recovered].slice(-(options.maxEvents || 300));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
record.nativeEventSeq = sequence;
|
|
139
|
+
}
|
|
140
|
+
if (reloadReports.length) {
|
|
141
|
+
record.events = [...(record.events || []), ...reloadReports].slice(-(options.maxEvents || 300));
|
|
142
|
+
}
|
|
143
|
+
if (record.status === 'running' || record.status === 'interrupted') {
|
|
144
|
+
record.status = 'interrupted';
|
|
145
|
+
record.finishedAt = journal.updatedAt || new Date().toISOString();
|
|
146
|
+
record.interruptedDraft = {
|
|
147
|
+
requestId: journal.requestId,
|
|
148
|
+
threadId: session.codexThreadId || '',
|
|
149
|
+
turnId: record.codexTurnId || '',
|
|
150
|
+
lastEventSeq: record.nativeEventSeq || 0,
|
|
151
|
+
reason: journal.ownerLost ? 'page_owner_lost' : 'page_reload'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
options.pauseSessionQueue?.(session, 'page_reload');
|
|
155
|
+
changed = true;
|
|
156
|
+
acknowledgeIds.push(journal.requestId);
|
|
157
|
+
}
|
|
158
|
+
return { changed, acknowledgeIds };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function destroy() {
|
|
162
|
+
destroyed = true;
|
|
163
|
+
clearTimeout(reconnectTimer);
|
|
164
|
+
reconnectTimer = null;
|
|
165
|
+
try {
|
|
166
|
+
port?.disconnect?.();
|
|
167
|
+
} catch (_error) {
|
|
168
|
+
// Best effort.
|
|
169
|
+
}
|
|
170
|
+
port = null;
|
|
171
|
+
binding = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
connect();
|
|
175
|
+
return { acknowledge, bind, destroy, listJournals, recoverJournals, release };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { create };
|
|
179
|
+
});
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
container.innerHTML = `
|
|
18
|
+
<div class="codex-pending-inputs" data-pending-inputs hidden></div>
|
|
18
19
|
<form class="codex-composer" data-composer-form>
|
|
19
20
|
<div class="codex-attachment-strip codex-attachment-preview-list" data-attachment-strip hidden></div>
|
|
20
21
|
<div class="codex-composer-skill-context" data-composer-skill-context hidden>
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
</div>
|
|
86
87
|
</div>
|
|
87
88
|
</div>
|
|
88
|
-
<button type="submit" data-run title="Send" aria-label="Send">↑</button>
|
|
89
|
+
<button type="submit" data-run data-action="send" title="Send" aria-label="Send">↑</button>
|
|
89
90
|
</div>
|
|
90
91
|
<div class="codex-slash-menu" data-slash-menu hidden>
|
|
91
92
|
<button type="button" data-slash-command="install-skill" data-slash-command-kind="installer">
|
|
@@ -142,7 +143,7 @@
|
|
|
142
143
|
bind(instance, form, 'drop', event => instance.callbacks.onDrop?.(event));
|
|
143
144
|
bind(instance, instance.container.querySelector('[data-run]'), 'click', event => {
|
|
144
145
|
event.preventDefault();
|
|
145
|
-
if (instance.callbacks.isRunning?.()) {
|
|
146
|
+
if (instance.callbacks.isRunning?.() && !String(task?.value || '').trim()) {
|
|
146
147
|
instance.callbacks.onCancel?.();
|
|
147
148
|
return;
|
|
148
149
|
}
|
|
@@ -185,9 +186,17 @@
|
|
|
185
186
|
if (!runButton) {
|
|
186
187
|
return;
|
|
187
188
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
const hasInput = Boolean(String(instance?.container?.querySelector('[data-task]')?.value || '').trim());
|
|
190
|
+
const action = running && !hasInput ? 'cancel' : 'send';
|
|
191
|
+
const label = action === 'cancel'
|
|
192
|
+
? t(instance, 'cancelRun')
|
|
193
|
+
: running
|
|
194
|
+
? t(instance, 'queueNextInput')
|
|
195
|
+
: t(instance, 'send');
|
|
196
|
+
runButton.dataset.action = action;
|
|
197
|
+
runButton.disabled = action === 'send' && !hasInput;
|
|
198
|
+
runButton.title = label;
|
|
199
|
+
runButton.setAttribute('aria-label', label);
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
function setModels(target, models = []) {
|