codex-overleaf-link 1.9.2 → 1.9.4

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.
@@ -45,6 +45,7 @@
45
45
  });
46
46
 
47
47
  initPopupState();
48
+ scheduleConsentUpdateUi();
48
49
 
49
50
  async function initPopupState() {
50
51
  await checkNativeHost();
@@ -198,11 +199,11 @@
198
199
  function getStatusIconText(classification) {
199
200
  switch (classification) {
200
201
  case 'compatible':
201
- return 'OK';
202
+ return '';
202
203
  case 'update-available':
203
- return '!';
204
+ return '';
204
205
  default:
205
- return 'X';
206
+ return '';
206
207
  }
207
208
  }
208
209
 
@@ -244,4 +245,268 @@
244
245
  return 'Native host is not compatible. Update the native host, then reload the extension.';
245
246
  }
246
247
  }
248
+
249
+ function scheduleConsentUpdateUi() {
250
+ const start = () => {
251
+ if (typeof document.createElement !== 'function') return;
252
+ injectConsentUpdateStyles();
253
+ const section = ensureUpdateSection();
254
+ let actionInFlight = false;
255
+ let currentView = null;
256
+
257
+ section.addEventListener('click', async event => {
258
+ const action = event.target?.closest?.('[data-update-action]')?.dataset?.updateAction;
259
+ if (!action || actionInFlight) return;
260
+ actionInFlight = true;
261
+ renderUpdateSection(section, currentView, { busyAction: action });
262
+ try {
263
+ currentView = await sendConsentAction(action);
264
+ } catch (error) {
265
+ currentView = failedUpdateView(currentView, error);
266
+ } finally {
267
+ actionInFlight = false;
268
+ renderUpdateSection(section, currentView);
269
+ }
270
+ });
271
+
272
+ chrome.runtime.onMessage.addListener(message => {
273
+ if (message?.type !== 'codex-overleaf/consent-update-state') return;
274
+ currentView = message.view;
275
+ if (!actionInFlight) renderUpdateSection(section, currentView);
276
+ });
277
+
278
+ sendConsentAction('get')
279
+ .then(view => {
280
+ currentView = view;
281
+ renderUpdateSection(section, view);
282
+ })
283
+ .catch(error => renderUpdateSection(section, failedUpdateView(null, error)));
284
+ };
285
+
286
+ if (document.readyState === 'loading') {
287
+ document.addEventListener('DOMContentLoaded', start, { once: true });
288
+ } else {
289
+ Promise.resolve().then(start);
290
+ }
291
+ }
292
+
293
+ function ensureUpdateSection() {
294
+ let section = document.querySelector('.updates');
295
+ if (!section) {
296
+ section = document.createElement('section');
297
+ section.className = 'updates';
298
+ section.setAttribute('aria-label', 'Stable updates');
299
+ (nativeInstall || status).insertAdjacentElement('afterend', section);
300
+ }
301
+ section.classList.add('consent-updates');
302
+ section.replaceChildren();
303
+ return section;
304
+ }
305
+
306
+ async function sendConsentAction(action) {
307
+ const type = {
308
+ get: 'codex-overleaf/consent-update-get-state',
309
+ check: 'codex-overleaf/consent-update-check',
310
+ install: 'codex-overleaf/consent-update-install',
311
+ later: 'codex-overleaf/consent-update-later',
312
+ retry: 'codex-overleaf/consent-update-check'
313
+ }[action];
314
+ const response = await chrome.runtime.sendMessage({ type });
315
+ if (!response?.ok) {
316
+ const error = new Error(response?.error?.message || 'Update action failed.');
317
+ error.code = response?.error?.code || 'update_failed';
318
+ throw error;
319
+ }
320
+ return response.result;
321
+ }
322
+
323
+ function failedUpdateView(view, error) {
324
+ return {
325
+ ...(view || {}),
326
+ state: {
327
+ ...(view?.state || {}),
328
+ state: 'failed',
329
+ code: error?.code || 'update_failed',
330
+ message: error?.message || 'Update failed.'
331
+ },
332
+ progress: { value: 0, determinate: true, phase: 'failed' },
333
+ actions: { retry: true }
334
+ };
335
+ }
336
+
337
+ function renderUpdateSection(section, view = {}, options = {}) {
338
+ const focusedAction = document.activeElement?.dataset?.updateAction || '';
339
+ const state = view?.state || { state: 'idle' };
340
+ const progress = view?.progress || { value: 0, determinate: true, phase: 'idle' };
341
+ const copy = getUpdateCopy(state, view);
342
+ const card = document.createElement('div');
343
+ card.className = 'consent-update-card state-' + (state.state || 'idle');
344
+
345
+ const head = document.createElement('div');
346
+ head.className = 'consent-update-head';
347
+ const title = document.createElement('strong');
348
+ title.textContent = copy.title;
349
+ const version = document.createElement('span');
350
+ version.textContent = copy.version;
351
+ head.append(title, version);
352
+
353
+ const detail = document.createElement('p');
354
+ detail.className = 'consent-update-detail';
355
+ detail.textContent = copy.detail;
356
+
357
+ const progressEl = document.createElement('div');
358
+ progressEl.className = 'consent-update-progress' + (progress.determinate ? '' : ' is-indeterminate');
359
+ progressEl.setAttribute('role', 'progressbar');
360
+ progressEl.setAttribute('aria-label', copy.phase);
361
+ progressEl.setAttribute('aria-valuemin', '0');
362
+ progressEl.setAttribute('aria-valuemax', '100');
363
+ progressEl.setAttribute('aria-valuetext', copy.phase);
364
+ if (progress.determinate) progressEl.setAttribute('aria-valuenow', String(progress.value || 0));
365
+ const progressFill = document.createElement('span');
366
+ progressFill.style.width = Math.max(0, Math.min(100, Number(progress.value || 0))) + '%';
367
+ progressEl.append(progressFill);
368
+
369
+ const phase = document.createElement('div');
370
+ phase.className = 'consent-update-phase';
371
+ phase.textContent = copy.phase;
372
+
373
+ const actions = document.createElement('div');
374
+ actions.className = 'consent-update-actions';
375
+ for (const action of getVisibleActions(view, options.busyAction)) {
376
+ const actionButton = document.createElement('button');
377
+ actionButton.type = 'button';
378
+ actionButton.dataset.updateAction = action.id;
379
+ actionButton.className = action.primary ? 'update-primary' : 'update-secondary';
380
+ actionButton.textContent = action.label;
381
+ actionButton.disabled = Boolean(options.busyAction) || action.disabled;
382
+ actions.append(actionButton);
383
+ }
384
+
385
+ const live = document.createElement('div');
386
+ live.className = 'consent-update-live';
387
+ live.setAttribute('aria-live', 'polite');
388
+ live.textContent = ['committed', 'rolled_back', 'failed'].includes(state.state) ? copy.phase : '';
389
+
390
+ card.append(head, detail);
391
+ if (!['idle', 'update_available'].includes(state.state) || options.busyAction === 'check') {
392
+ card.append(progressEl, phase);
393
+ }
394
+ card.append(actions, live);
395
+ if (state.state === 'failed' && (state.code || state.message)) {
396
+ const details = document.createElement('details');
397
+ const summary = document.createElement('summary');
398
+ summary.textContent = utx('Technical details', '技术详情');
399
+ const code = document.createElement('code');
400
+ code.textContent = [state.code, state.message].filter(Boolean).join(': ');
401
+ details.append(summary, code);
402
+ card.append(details);
403
+ }
404
+ section.replaceChildren(card);
405
+ if (focusedAction) {
406
+ section.querySelector('[data-update-action="' + focusedAction + '"]:not(:disabled)')?.focus();
407
+ }
408
+ }
409
+
410
+ function getVisibleActions(view = {}, busyAction = '') {
411
+ const state = view.state?.state || 'idle';
412
+ if (busyAction) {
413
+ return [{ id: busyAction, label: getBusyLabel(busyAction), primary: true, disabled: true }];
414
+ }
415
+ if (state === 'update_available') {
416
+ return [
417
+ { id: 'later', label: utx('Later', '稍后提醒'), primary: false },
418
+ { id: 'install', label: utx('Update now', '立即更新'), primary: true }
419
+ ];
420
+ }
421
+ if (['staged', 'waiting_for_idle'].includes(state)) {
422
+ return [{ id: 'later', label: utx('Later', '稍后提醒'), primary: false }];
423
+ }
424
+ if (['downloading', 'applying', 'awaiting_health', 'checking'].includes(state)) return [];
425
+ if (['failed', 'rolled_back'].includes(state)) {
426
+ return [{ id: 'retry', label: utx('Retry', '重试'), primary: true }];
427
+ }
428
+ return [{ id: 'check', label: utx('Check latest', '检查更新'), primary: false }];
429
+ }
430
+
431
+ function getBusyLabel(action) {
432
+ return {
433
+ check: utx('Checking...', '正在检查...'),
434
+ retry: utx('Checking...', '正在检查...'),
435
+ install: utx('Starting update...', '正在启动更新...'),
436
+ later: utx('Saving...', '正在保存...')
437
+ }[action] || utx('Working...', '处理中...');
438
+ }
439
+
440
+ function getUpdateCopy(state, view) {
441
+ const current = state.currentVersion ? 'v' + state.currentVersion : '';
442
+ const latest = state.latestVersion ? 'v' + state.latestVersion : '';
443
+ const blocker = formatBlocker(state.blocker);
444
+ const map = {
445
+ idle: [utx('Stable updates', '稳定版更新'), utx('Up to date', '已是最新版'), utx('Automatically checks signed stable releases.', '自动检查经过签名验证的稳定版本。')],
446
+ checking: [utx('Checking for updates', '正在检查更新'), '', utx('Checking the latest signed stable release', '正在检查最新签名稳定版本')],
447
+ update_available: [utx('Update available', '发现新版本'), latest, view.snoozed ? utx('Reminder paused. Update remains ready here.', '提醒已暂停,仍可随时从这里更新。') : utx('Signed stable update for the extension and Native Host.', '扩展与 Native Host 的签名稳定更新。')],
448
+ downloading: [utx('Downloading update', '正在下载更新'), latest, utx('Downloading and verifying the coordinated update', '正在下载并验证协调更新')],
449
+ staged: [utx('Update ready', '更新已就绪'), latest, utx('Verified and ready to install at a safe point.', '已完成验证,将在安全时机安装。')],
450
+ waiting_for_idle: [utx('Waiting for a safe point', '正在等待安全时机'), latest, blocker],
451
+ applying: [utx('Installing update', '正在安装更新'), latest, utx('Installing extension and Native Host', '正在安装扩展与 Native Host')],
452
+ awaiting_health: [utx('Checking updated components', '正在检查更新后的组件'), latest, utx('Restarting and checking both components', '正在重启并检查两个组件')],
453
+ committed: [utx('Update complete', '更新完成'), latest || current, utx('Extension and Native Host were updated together.', '扩展与 Native Host 已同步更新。')],
454
+ rolled_back: [utx('Previous version restored', '已恢复上一版本'), current, utx('Health check failed, so the previous coordinated pair was restored.', '健康检查失败,已恢复上一组协调版本。')],
455
+ failed: [utx('Update could not continue', '更新无法继续'), latest, state.message || utx('Check the details and retry.', '请查看详情后重试。')]
456
+ };
457
+ const value = map[state.state] || map.idle;
458
+ return { title: value[0], version: value[1], detail: value[2], phase: value[2] };
459
+ }
460
+
461
+ function formatBlocker(blocker) {
462
+ const labels = {
463
+ unsaved: utx('Overleaf has not confirmed that this document is saved.', 'Overleaf 尚未确认当前文档已保存。'),
464
+ active_run: utx('A Codex task is still running.', 'Codex 任务仍在运行。'),
465
+ cancelling: utx('Cancellation is still settling.', '取消操作仍在收尾。'),
466
+ storage_write: utx('Extension state is still being saved.', '扩展状态仍在保存。'),
467
+ review_action: utx('Accept or Undo is still running.', '接受或撤销操作仍在进行。'),
468
+ dialog_open: utx('A confirmation dialog is open.', '确认对话框仍处于打开状态。'),
469
+ native_project_locked: utx('Native Host still holds a project lock.', 'Native Host 仍持有项目锁。'),
470
+ native_run_active: utx('Native Host is still processing a task.', 'Native Host 仍在处理任务。')
471
+ };
472
+ return labels[blocker] || utx('Waiting until every Overleaf tab is saved and idle.', '正在等待所有 Overleaf 标签页完成保存并进入空闲状态。');
473
+ }
474
+
475
+ function utx(english, chinese) {
476
+ const locale = document.documentElement?.lang || 'en';
477
+ return /^zh\b/i.test(locale) ? chinese : english;
478
+ }
479
+
480
+ function injectConsentUpdateStyles() {
481
+ if (document.getElementById('codex-overleaf-consent-update-styles')) return;
482
+ const style = document.createElement('style');
483
+ style.id = 'codex-overleaf-consent-update-styles';
484
+ style.textContent = [
485
+ 'body { width:344px; padding:16px; background:#171918; color:#eceeec; font-family:"Avenir Next","Segoe UI",sans-serif; }',
486
+ 'h1 { font-size:16px; letter-spacing:-.01em; }',
487
+ '.version-status { color:#a8ada9; }',
488
+ '.status-icon { min-width:10px; min-height:10px; width:10px; height:10px; box-shadow:0 0 0 3px rgba(255,255,255,.04); }',
489
+ '.status-icon.compatible { background:#7fd58a; } .status-icon.update-available { background:#d7b96e; } .status-icon.incompatible { background:#d87972; }',
490
+ '#open-panel { border-color:#39403c; background:#242825; color:#dfe4df; font-weight:600; }',
491
+ '#status { color:#9ba19c; font-size:12px; }',
492
+ '.consent-updates { margin-top:14px; padding-top:13px; border-top:1px solid #303431; }',
493
+ '.consent-update-card { padding-top:1px; }',
494
+ '.consent-update-head { display:flex; align-items:baseline; justify-content:space-between; gap:12px; }',
495
+ '.consent-update-head strong { font-size:13px; color:#f0f2f0; } .consent-update-head span { color:#85d99a; font-size:11px; font-weight:700; }',
496
+ '.consent-update-detail,.consent-update-phase { color:#a9aea9; font-size:12px; line-height:1.45; }',
497
+ '.consent-update-progress { position:relative; height:4px; margin:12px 0 8px; overflow:hidden; border-radius:999px; background:#2c312e; }',
498
+ '.consent-update-progress span { display:block; height:100%; border-radius:inherit; background:#4f9ee8; transition:width .25s ease; }',
499
+ '.consent-update-progress.is-indeterminate span { width:42% !important; animation:consent-progress 1.25s ease-in-out infinite; }',
500
+ '.consent-update-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:12px; }',
501
+ '.consent-update-actions button { width:auto; min-width:92px; margin:0; padding:7px 12px; font-weight:650; }',
502
+ '.consent-update-actions .update-primary { border-color:#4a91d7; background:#3578bd; }',
503
+ '.consent-update-actions .update-secondary { border-color:#3c433f; background:transparent; color:#d5dad6; }',
504
+ '.consent-update-actions button:focus-visible { outline:2px solid #79b8f2; outline-offset:2px; }',
505
+ '.consent-update-live { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }',
506
+ '.consent-update-card details { margin-top:10px; color:#a9aea9; font-size:11px; } .consent-update-card details code { background:#202320; color:#c9ceca; }',
507
+ '@keyframes consent-progress { 0% { transform:translateX(-110%); } 100% { transform:translateX(240%); } }',
508
+ '@media (prefers-reduced-motion:reduce) { .consent-update-progress.is-indeterminate span { animation:none; width:65% !important; } }'
509
+ ].join('\n');
510
+ document.head.append(style);
511
+ }
247
512
  })();
@@ -12,7 +12,7 @@
12
12
  const MIN_NATIVE_VERSION = '1.0.0';
13
13
  const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
14
14
  const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
15
- const BUILD_TARGET_VERSION = '1.9.2';
15
+ const BUILD_TARGET_VERSION = '1.9.4';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -0,0 +1,201 @@
1
+ (function initCodexOverleafUpdateConsent(root, factory) {
2
+ 'use strict';
3
+
4
+ const api = factory();
5
+ root.CodexOverleafUpdateConsent = api;
6
+ if (typeof module === 'object' && module?.exports) {
7
+ module.exports = api;
8
+ }
9
+ })(typeof globalThis === 'object' ? globalThis : this, function createUpdateConsentApi() {
10
+ 'use strict';
11
+
12
+ const UPDATE_STATES = new Set([
13
+ 'idle',
14
+ 'checking',
15
+ 'update_available',
16
+ 'downloading',
17
+ 'staged',
18
+ 'waiting_for_idle',
19
+ 'applying',
20
+ 'awaiting_health',
21
+ 'committed',
22
+ 'rolled_back',
23
+ 'failed'
24
+ ]);
25
+ const EXECUTION_STATES = new Set([
26
+ 'downloading',
27
+ 'staged',
28
+ 'waiting_for_idle',
29
+ 'applying',
30
+ 'awaiting_health'
31
+ ]);
32
+ const TERMINAL_STATES = new Set(['committed', 'rolled_back', 'failed']);
33
+ const QUIET_FAILURE_CODES = new Set([
34
+ 'update_network_failed',
35
+ 'update_network_unavailable',
36
+ 'update_github_http_error'
37
+ ]);
38
+ const PROGRESS = Object.freeze({
39
+ idle: { value: 0, determinate: true, phase: 'idle' },
40
+ checking: { value: 0, determinate: false, phase: 'checking' },
41
+ update_available: { value: 0, determinate: true, phase: 'available' },
42
+ downloading: { value: 35, determinate: false, phase: 'downloading' },
43
+ staged: { value: 65, determinate: true, phase: 'staged' },
44
+ waiting_for_idle: { value: 65, determinate: true, phase: 'waiting' },
45
+ applying: { value: 82, determinate: false, phase: 'applying' },
46
+ awaiting_health: { value: 94, determinate: false, phase: 'health' },
47
+ committed: { value: 100, determinate: true, phase: 'committed' },
48
+ rolled_back: { value: 94, determinate: true, phase: 'rolled_back' },
49
+ failed: { value: 0, determinate: true, phase: 'failed' }
50
+ });
51
+
52
+ function normalizeUpdateState(value = {}, currentVersion = '') {
53
+ const blockers = Array.isArray(value.blockers)
54
+ ? value.blockers.map(safeToken).filter(Boolean).slice(0, 12)
55
+ : [safeToken(value.blocker)].filter(Boolean);
56
+ const state = UPDATE_STATES.has(value.state) ? value.state : 'idle';
57
+ return {
58
+ state,
59
+ managed: value.managed !== false,
60
+ currentVersion: safeVersion(value.currentVersion) || safeVersion(currentVersion),
61
+ latestVersion: safeVersion(value.latestVersion) || safeVersion(value.currentVersion) || safeVersion(currentVersion),
62
+ etag: String(value.etag || '').slice(0, 300),
63
+ blocker: blockers[0] || '',
64
+ blockers,
65
+ transactionId: safeId(value.transactionId),
66
+ lastCheckedAt: finiteNumber(value.lastCheckedAt),
67
+ stagedAt: finiteNumber(value.stagedAt),
68
+ postponeUntil: finiteNumber(value.postponeUntil),
69
+ code: safeToken(value.code),
70
+ message: String(value.message || '').replace(/\s+/g, ' ').trim().slice(0, 300)
71
+ };
72
+ }
73
+
74
+ function normalizeConsentState(value = {}) {
75
+ return {
76
+ schemaVersion: 1,
77
+ mode: 'consent',
78
+ snoozedVersion: safeVersion(value.snoozedVersion),
79
+ snoozedUntil: finiteNumber(value.snoozedUntil),
80
+ authorizedVersion: safeVersion(value.authorizedVersion),
81
+ authorizationId: safeId(value.authorizationId),
82
+ authorizedAt: finiteNumber(value.authorizedAt),
83
+ lastPromptedVersion: safeVersion(value.lastPromptedVersion),
84
+ lastPromptedAt: finiteNumber(value.lastPromptedAt)
85
+ };
86
+ }
87
+
88
+ function deriveViewModel(updateValue = {}, consentValue = {}, options = {}) {
89
+ const now = finiteNumber(options.now) || Date.now();
90
+ const state = normalizeUpdateState(updateValue, options.currentVersion);
91
+ const consent = normalizeConsentState(consentValue);
92
+ const newer = compareStableVersions(state.latestVersion, state.currentVersion) > 0;
93
+ const snoozed = newer &&
94
+ consent.snoozedVersion === state.latestVersion &&
95
+ consent.snoozedUntil > now;
96
+ const execution = EXECUTION_STATES.has(state.state);
97
+ const available = state.state === 'update_available' && newer;
98
+ const progress = getProgressModel(state);
99
+ const quietFailure = state.state === 'failed' && QUIET_FAILURE_CODES.has(state.code);
100
+
101
+ return {
102
+ state,
103
+ consent: {
104
+ mode: consent.mode,
105
+ snoozedVersion: consent.snoozedVersion,
106
+ snoozedUntil: consent.snoozedUntil,
107
+ authorizedVersion: consent.authorizedVersion,
108
+ authorizedAt: consent.authorizedAt
109
+ },
110
+ progress,
111
+ available,
112
+ snoozed,
113
+ execution,
114
+ showPanel: (available && !snoozed) || execution || state.state === 'rolled_back' ||
115
+ (state.state === 'failed' && !quietFailure && newer),
116
+ badge: getBadge(state, { available, snoozed }),
117
+ actions: {
118
+ check: ['idle', 'committed', 'rolled_back', 'failed'].includes(state.state),
119
+ install: available,
120
+ later: available || ['staged', 'waiting_for_idle'].includes(state.state),
121
+ retry: ['failed', 'rolled_back'].includes(state.state)
122
+ }
123
+ };
124
+ }
125
+
126
+ function getProgressModel(stateValue = {}) {
127
+ const state = normalizeUpdateState(stateValue);
128
+ const base = PROGRESS[state.state] || PROGRESS.idle;
129
+ return {
130
+ ...base,
131
+ blocker: state.blocker,
132
+ code: state.code,
133
+ message: state.message
134
+ };
135
+ }
136
+
137
+ function getBadge(state, flags) {
138
+ if (state.state === 'applying' || state.state === 'awaiting_health') {
139
+ return { text: '...', color: '#3578bd' };
140
+ }
141
+ if (state.state === 'failed' || state.state === 'rolled_back') {
142
+ return { text: '!', color: '#b85450' };
143
+ }
144
+ if (flags.available) {
145
+ return { text: 'UP', color: flags.snoozed ? '#6f6755' : '#3578bd' };
146
+ }
147
+ return { text: '', color: '#3578bd' };
148
+ }
149
+
150
+ function isExecutionState(value) {
151
+ return EXECUTION_STATES.has(String(value || ''));
152
+ }
153
+
154
+ function isTerminalState(value) {
155
+ return TERMINAL_STATES.has(String(value || ''));
156
+ }
157
+
158
+ function compareStableVersions(left, right) {
159
+ const a = parseVersion(left);
160
+ const b = parseVersion(right);
161
+ if (!a || !b) return 0;
162
+ for (let index = 0; index < 3; index += 1) {
163
+ if (a[index] !== b[index]) return a[index] > b[index] ? 1 : -1;
164
+ }
165
+ return 0;
166
+ }
167
+
168
+ function parseVersion(value) {
169
+ const match = String(value || '').match(/^(\d+)\.(\d+)\.(\d+)$/);
170
+ return match ? match.slice(1).map(Number) : null;
171
+ }
172
+
173
+ function safeVersion(value) {
174
+ return parseVersion(value) ? String(value) : '';
175
+ }
176
+
177
+ function safeToken(value) {
178
+ const token = String(value || '');
179
+ return /^[a-z0-9_.-]{1,100}$/i.test(token) ? token : '';
180
+ }
181
+
182
+ function safeId(value) {
183
+ const id = String(value || '');
184
+ return /^[a-z0-9-]{1,100}$/i.test(id) ? id : '';
185
+ }
186
+
187
+ function finiteNumber(value) {
188
+ const number = Number(value || 0);
189
+ return Number.isFinite(number) && number >= 0 ? number : 0;
190
+ }
191
+
192
+ return Object.freeze({
193
+ compareStableVersions,
194
+ deriveViewModel,
195
+ getProgressModel,
196
+ isExecutionState,
197
+ isTerminalState,
198
+ normalizeConsentState,
199
+ normalizeUpdateState
200
+ });
201
+ });
@@ -4046,3 +4046,149 @@
4046
4046
  #codex-overleaf-panel .recent-projects-degraded [data-recent-projects-retry] {
4047
4047
  margin-top: 8px;
4048
4048
  }
4049
+
4050
+ /* v1.9.4: consent-driven coordinated update notice. */
4051
+ #codex-overleaf-panel .codex-update-notice {
4052
+ display: grid;
4053
+ grid-template-columns: minmax(0, 1fr) auto;
4054
+ gap: 14px;
4055
+ align-items: center;
4056
+ margin: 0;
4057
+ padding: 11px 13px;
4058
+ border-bottom: 1px solid var(--tl-border, #2a2f2c);
4059
+ background:
4060
+ linear-gradient(100deg, rgba(53, 120, 189, 0.13), transparent 58%),
4061
+ var(--tl-surface-1, #181a19);
4062
+ }
4063
+
4064
+ #codex-overleaf-panel .codex-update-notice[hidden] {
4065
+ display: none;
4066
+ }
4067
+
4068
+ #codex-overleaf-panel .codex-update-notice-body {
4069
+ display: grid;
4070
+ min-width: 0;
4071
+ gap: 2px;
4072
+ }
4073
+
4074
+ #codex-overleaf-panel .codex-update-notice-eyebrow {
4075
+ color: #7fb5e9;
4076
+ font-size: 10px;
4077
+ font-weight: 750;
4078
+ letter-spacing: 0.07em;
4079
+ text-transform: uppercase;
4080
+ }
4081
+
4082
+ #codex-overleaf-panel .codex-update-notice strong {
4083
+ color: var(--tl-fg-1, #eceeec);
4084
+ font-size: 12.5px;
4085
+ }
4086
+
4087
+ #codex-overleaf-panel .codex-update-notice-detail {
4088
+ overflow: hidden;
4089
+ color: var(--tl-fg-3, #9da39e);
4090
+ font-size: 11px;
4091
+ line-height: 1.35;
4092
+ text-overflow: ellipsis;
4093
+ }
4094
+
4095
+ #codex-overleaf-panel .codex-update-notice-actions {
4096
+ display: flex;
4097
+ gap: 6px;
4098
+ align-items: center;
4099
+ }
4100
+
4101
+ #codex-overleaf-panel .codex-update-notice-actions button {
4102
+ width: auto;
4103
+ min-width: 58px;
4104
+ margin: 0;
4105
+ padding: 5px 8px;
4106
+ border: 1px solid var(--tl-border-strong, #414844);
4107
+ border-radius: 5px;
4108
+ background: transparent;
4109
+ color: var(--tl-fg-2, #d5dad6);
4110
+ font-size: 10.5px;
4111
+ font-weight: 700;
4112
+ cursor: pointer;
4113
+ }
4114
+
4115
+ #codex-overleaf-panel .codex-update-notice-actions button.is-primary {
4116
+ border-color: #4a91d7;
4117
+ background: #3578bd;
4118
+ color: #fff;
4119
+ }
4120
+
4121
+ #codex-overleaf-panel .codex-update-notice-actions button:focus-visible {
4122
+ outline: 2px solid #79b8f2;
4123
+ outline-offset: 2px;
4124
+ }
4125
+
4126
+ #codex-overleaf-panel .codex-update-notice-progress {
4127
+ position: relative;
4128
+ height: 3px;
4129
+ margin-top: 6px;
4130
+ overflow: hidden;
4131
+ border-radius: 999px;
4132
+ background: #2c312e;
4133
+ }
4134
+
4135
+ #codex-overleaf-panel .codex-update-notice-progress > span {
4136
+ display: block;
4137
+ height: 100%;
4138
+ border-radius: inherit;
4139
+ background: #4f9ee8;
4140
+ transition: width 250ms ease;
4141
+ }
4142
+
4143
+ #codex-overleaf-panel .codex-update-notice-progress.is-indeterminate > span {
4144
+ width: 42% !important;
4145
+ animation: codex-update-notice-progress 1.25s ease-in-out infinite;
4146
+ }
4147
+
4148
+ #codex-overleaf-panel .codex-update-notice[data-state="waiting_for_idle"] {
4149
+ background:
4150
+ linear-gradient(100deg, rgba(199, 160, 83, 0.13), transparent 58%),
4151
+ var(--tl-surface-1, #181a19);
4152
+ }
4153
+
4154
+ #codex-overleaf-panel .codex-update-notice[data-state="waiting_for_idle"] .codex-update-notice-eyebrow {
4155
+ color: #d4b86f;
4156
+ }
4157
+
4158
+ #codex-overleaf-panel .codex-update-notice[data-state="committed"] .codex-update-notice-eyebrow {
4159
+ color: var(--tl-ok, #7fd58a);
4160
+ }
4161
+
4162
+ #codex-overleaf-panel .codex-update-notice[data-state="failed"],
4163
+ #codex-overleaf-panel .codex-update-notice[data-state="rolled_back"] {
4164
+ background:
4165
+ linear-gradient(100deg, rgba(184, 84, 80, 0.12), transparent 58%),
4166
+ var(--tl-surface-1, #181a19);
4167
+ }
4168
+
4169
+ #codex-overleaf-panel .codex-update-notice[data-state="failed"] .codex-update-notice-eyebrow,
4170
+ #codex-overleaf-panel .codex-update-notice[data-state="rolled_back"] .codex-update-notice-eyebrow {
4171
+ color: var(--tl-fail, #d87972);
4172
+ }
4173
+
4174
+ @keyframes codex-update-notice-progress {
4175
+ 0% { transform: translateX(-110%); }
4176
+ 100% { transform: translateX(240%); }
4177
+ }
4178
+
4179
+ @media (max-width: 720px) {
4180
+ #codex-overleaf-panel .codex-update-notice {
4181
+ grid-template-columns: 1fr;
4182
+ }
4183
+
4184
+ #codex-overleaf-panel .codex-update-notice-actions {
4185
+ justify-content: flex-end;
4186
+ }
4187
+ }
4188
+
4189
+ @media (prefers-reduced-motion: reduce) {
4190
+ #codex-overleaf-panel .codex-update-notice-progress.is-indeterminate > span {
4191
+ width: 65% !important;
4192
+ animation: none;
4193
+ }
4194
+ }