codex-overleaf-link 1.8.6 → 1.9.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.
Files changed (73) hide show
  1. package/README.md +45 -21
  2. package/bin/codex-overleaf-link.mjs +58 -0
  3. package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
  4. package/extension/assets/icons/codex-overleaf-icon.png +0 -0
  5. package/extension/assets/icons/icon128.png +0 -0
  6. package/extension/assets/icons/icon16.png +0 -0
  7. package/extension/assets/icons/icon32.png +0 -0
  8. package/extension/assets/icons/icon48.png +0 -0
  9. package/extension/bootstrap/background.js +404 -0
  10. package/extension/bootstrap/manifest.template.json +51 -0
  11. package/extension/bootstrap/popup.css +26 -0
  12. package/extension/bootstrap/popup.html +38 -0
  13. package/extension/bootstrap/popup.js +44 -0
  14. package/extension/bootstrap/runtimeContext.js +2 -0
  15. package/extension/runtime-manifest.json +60 -0
  16. package/extension/src/background.js +532 -0
  17. package/extension/src/content/applyResultFormatters.js +387 -0
  18. package/extension/src/content/composerAttachments.js +522 -0
  19. package/extension/src/content/composerPanel.js +254 -0
  20. package/extension/src/content/contentRuntime.js +8897 -0
  21. package/extension/src/content/contextTray.js +686 -0
  22. package/extension/src/content/diagnosticsController.js +659 -0
  23. package/extension/src/content/diagnosticsPanel.js +342 -0
  24. package/extension/src/content/diffReviewPanel.js +797 -0
  25. package/extension/src/content/localSkillsPanel.js +280 -0
  26. package/extension/src/content/markdownText.js +606 -0
  27. package/extension/src/content/mirrorHealth.js +92 -0
  28. package/extension/src/content/modelPicker.js +489 -0
  29. package/extension/src/content/nativeChannel.js +62 -0
  30. package/extension/src/content/otWarmMirror.js +964 -0
  31. package/extension/src/content/otWarmMirrorController.js +174 -0
  32. package/extension/src/content/panelMaintenance.js +367 -0
  33. package/extension/src/content/panelRenderer.js +253 -0
  34. package/extension/src/content/recentProjects.js +1063 -0
  35. package/extension/src/content/reviewHunks.js +247 -0
  36. package/extension/src/content/runController.js +242 -0
  37. package/extension/src/content/runTimelineView.js +1131 -0
  38. package/extension/src/content/sessionManager.js +487 -0
  39. package/extension/src/content/sessionPanel.js +263 -0
  40. package/extension/src/content/settingsPanel.js +527 -0
  41. package/extension/src/content/themeController.js +91 -0
  42. package/extension/src/content/updateIdle.js +67 -0
  43. package/extension/src/content/writebackController.js +300 -0
  44. package/extension/src/content/writebackOrchestrator.js +850 -0
  45. package/extension/src/contentScript.js +8 -0
  46. package/extension/src/page/compileBridge.js +460 -0
  47. package/extension/src/page/overleafCapabilities.js +100 -0
  48. package/extension/src/page/overleafEditor.js +282 -0
  49. package/extension/src/page/overleafProjectSnapshot.js +234 -0
  50. package/extension/src/page/overleafRealtimeObserver.js +491 -0
  51. package/extension/src/page/pageBridgeCapability.js +71 -0
  52. package/extension/src/page/snapshotRouter.js +948 -0
  53. package/extension/src/page/trackedChangesLifecycle.js +1506 -0
  54. package/extension/src/page/treeOperations.js +1584 -0
  55. package/extension/src/page/writeGuard.js +142 -0
  56. package/extension/src/page/writebackRouter.js +1934 -0
  57. package/extension/src/pageBridge.js +2224 -0
  58. package/extension/src/popup.js +247 -0
  59. package/extension/src/shared/agentTranscript.js +18 -4
  60. package/extension/src/shared/compatibility.js +1 -1
  61. package/extension/styles/panel.css +4048 -0
  62. package/native-host/src/index.js +9 -2
  63. package/native-host/src/managedInstall.js +393 -0
  64. package/native-host/src/managedLauncherRuntime.js +71 -0
  65. package/native-host/src/nativeHostPlatform.js +16 -0
  66. package/native-host/src/runtimeInstaller.js +6 -1
  67. package/native-host/src/taskRunnerRuntime.js +8 -0
  68. package/native-host/src/updateArchive.js +253 -0
  69. package/native-host/src/updateManager.js +548 -0
  70. package/native-host/src/updateTrust.js +156 -0
  71. package/package.json +11 -2
  72. package/scripts/install-managed.mjs +64 -0
  73. package/scripts/uninstall-managed.mjs +48 -0
@@ -0,0 +1,532 @@
1
+ if (!globalThis.CodexOverleafCompatibility) {
2
+ importScripts('shared/compatibility.js');
3
+ }
4
+
5
+ (function initBackground() {
6
+ 'use strict';
7
+
8
+ const HOST_NAME = 'com.codex.overleaf';
9
+ const COMPATIBILITY_REQUIRED_METHODS = new Set([
10
+ 'codex.run',
11
+ 'task.run',
12
+ 'task.confirm',
13
+ 'mirror.sync',
14
+ 'mirror.patchFiles',
15
+ 'mirror.confirmWriteback',
16
+ 'mirror.scanSensitive',
17
+ 'codex.history.clearPlugin',
18
+ 'skills.list',
19
+ 'skills.install',
20
+ 'skills.remove'
21
+ ]);
22
+ const RECOVERABLE_COMPATIBILITY_METHODS = new Set([
23
+ 'bridge.ping',
24
+ 'mirror.status',
25
+ 'codex.models',
26
+ 'codex.cancel'
27
+ ]);
28
+ const CodexOverleafCompatibility = globalThis.CodexOverleafCompatibility;
29
+ let port = null;
30
+ const pending = new Map();
31
+ globalThis.CodexOverleafNativeBridge = Object.freeze({
32
+ requestInternal: payload => sendNativeRequest(payload, null, { skipCompatibility: true }),
33
+ getPendingState: () => ({
34
+ executionRequests: Array.from(pending.values()).filter(item => item.retryClass === 'no_silent_retry').length,
35
+ totalRequests: pending.size
36
+ })
37
+ });
38
+
39
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
40
+ if (message?.type !== 'codex-overleaf/native-request') {
41
+ return undefined;
42
+ }
43
+
44
+ if (!isAllowedOverleafSender(sender)) {
45
+ sendResponse({
46
+ ok: false,
47
+ error: {
48
+ code: 'forbidden_sender',
49
+ message: 'Native requests are only accepted from Overleaf project pages.'
50
+ }
51
+ });
52
+ return undefined;
53
+ }
54
+
55
+ if (message.payload?.method === 'codex.cancel') {
56
+ const compatibilityBlock = getNativeCompatibilityBlock(message.payload);
57
+ if (compatibilityBlock) {
58
+ sendResponse({
59
+ ok: false,
60
+ error: compatibilityBlock
61
+ });
62
+ return undefined;
63
+ }
64
+
65
+ try {
66
+ const id = sendNativeCancel(message.payload);
67
+ sendResponse({
68
+ ok: true,
69
+ result: {
70
+ sent: true,
71
+ id
72
+ }
73
+ });
74
+ } catch (error) {
75
+ sendResponse({
76
+ ok: false,
77
+ error: {
78
+ code: 'native_connection_failed',
79
+ message: getErrorMessage(error, 'Native host request failed.')
80
+ }
81
+ });
82
+ }
83
+ return undefined;
84
+ }
85
+
86
+ sendNativeRequest(message.payload, sender)
87
+ .then(result => sendResponse(result))
88
+ .catch(error => {
89
+ const userFacingError = toUserFacingNativeError(error);
90
+ sendResponse({
91
+ ok: false,
92
+ error: userFacingError
93
+ });
94
+ });
95
+
96
+ return true;
97
+ });
98
+
99
+ function getNativeRetryClass(method) {
100
+ switch (method) {
101
+ case 'bridge.ping':
102
+ return 'safe_read_retry';
103
+ case 'mirror.status':
104
+ return 'safe_read_retry';
105
+ case 'mirror.scanSensitive':
106
+ return 'safe_read_retry';
107
+ case 'mirror.sync':
108
+ return 'safe_sync_retry';
109
+ case 'mirror.patchFiles':
110
+ return 'safe_sync_retry';
111
+ case 'mirror.confirmWriteback':
112
+ return 'safe_sync_retry';
113
+ case 'codex.cancel':
114
+ return 'best_effort';
115
+ case 'codex.run':
116
+ return 'no_silent_retry';
117
+ case 'task.run':
118
+ return 'no_silent_retry';
119
+ case 'task.confirm':
120
+ return 'no_silent_retry';
121
+ default:
122
+ return 'no_silent_retry';
123
+ }
124
+ }
125
+
126
+ function sendNativeRequest(payload, sender, options = {}) {
127
+ const id = payload.id || crypto.randomUUID();
128
+ const requestWithEvidence = { ...payload, id };
129
+ const compatibilityBlock = options.skipCompatibility ? null : getNativeCompatibilityBlock(requestWithEvidence);
130
+ if (compatibilityBlock) {
131
+ return Promise.resolve({
132
+ ok: false,
133
+ error: compatibilityBlock
134
+ });
135
+ }
136
+ const request = sanitizeNativeRequest(requestWithEvidence);
137
+
138
+ return new Promise((resolve, reject) => {
139
+ const pendingRequest = {
140
+ resolve,
141
+ reject,
142
+ tabId: sender?.tab?.id,
143
+ request,
144
+ method: request.method,
145
+ retryClass: getNativeRetryClass(request.method),
146
+ retryCount: 0,
147
+ nativePort: null,
148
+ finalResponseReceived: false,
149
+ eventForwarded: false
150
+ };
151
+ pending.set(id, pendingRequest);
152
+
153
+ try {
154
+ postNativeRequest(pendingRequest);
155
+ } catch (error) {
156
+ handleNativePostFailure(id, pendingRequest, error);
157
+ }
158
+ });
159
+ }
160
+
161
+ function getNativeCompatibilityBlock(request = {}) {
162
+ const evidence = getNativeCompatibilityEvidence(request);
163
+ if (!evidence) {
164
+ if (COMPATIBILITY_REQUIRED_METHODS.has(request.method)) {
165
+ return createNativeUpdateRequiredBlock(request.method, {
166
+ status: 'unknown_native',
167
+ classification: 'incompatible'
168
+ });
169
+ }
170
+ return null;
171
+ }
172
+
173
+ if (isNativeMethodAllowedByCompatibility(request.method, evidence)) {
174
+ return null;
175
+ }
176
+
177
+ const status = getNativeCompatibilityStatus(evidence);
178
+ return createNativeUpdateRequiredBlock(request.method, evidence, status);
179
+ }
180
+
181
+ function isNativeMethodAllowedByCompatibility(method, evidence) {
182
+ if (!CodexOverleafCompatibility || typeof CodexOverleafCompatibility.isNativeMethodAllowed !== 'function') {
183
+ return true;
184
+ }
185
+
186
+ if (!COMPATIBILITY_REQUIRED_METHODS.has(method) && !RECOVERABLE_COMPATIBILITY_METHODS.has(method)) {
187
+ return CodexOverleafCompatibility.isNativeMethodAllowed(method, evidence);
188
+ }
189
+
190
+ return CodexOverleafCompatibility.isNativeMethodAllowed(method, evidence);
191
+ }
192
+
193
+ function getNativeCompatibilityEvidence(request = {}) {
194
+ const params = request.params;
195
+ if (!params || typeof params !== 'object') {
196
+ return null;
197
+ }
198
+ return params.nativeCompatibility || params.compatibilityStatus || params.compatibility || null;
199
+ }
200
+
201
+ function getNativeCompatibilityStatus(evidence) {
202
+ if (typeof evidence === 'string') {
203
+ return evidence;
204
+ }
205
+ return evidence?.status || evidence?.classification || 'native_missing';
206
+ }
207
+
208
+ function getNativeCompatibilityClassification(evidence) {
209
+ if (typeof evidence === 'string') {
210
+ return isNativeCompatibilityClassification(evidence)
211
+ ? evidence
212
+ : evidence === 'ok'
213
+ ? 'compatible'
214
+ : 'incompatible';
215
+ }
216
+ if (isNativeCompatibilityClassification(evidence?.classification)) {
217
+ return evidence.classification;
218
+ }
219
+ if (isNativeCompatibilityClassification(evidence?.status)) {
220
+ return evidence.status;
221
+ }
222
+ return evidence?.status === 'ok' ? 'compatible' : 'incompatible';
223
+ }
224
+
225
+ function isNativeCompatibilityClassification(status) {
226
+ return status === 'compatible' || status === 'update-available' || status === 'incompatible';
227
+ }
228
+
229
+ function createNativeUpdateRequiredBlock(method, evidence = {}, status = getNativeCompatibilityStatus(evidence)) {
230
+ const classification = getNativeCompatibilityClassification(evidence);
231
+ const requiredVersion = getNativeRequiredVersion(evidence);
232
+ const recommendedVersion = getNativeRecommendedVersion(evidence);
233
+ const updateCommand = getNativeUpdateCommand(evidence);
234
+ const currentNativeVersion = getCurrentNativeVersion(evidence);
235
+ return {
236
+ code: 'native_update_required',
237
+ message: formatNativeCompatibilityBlockMessage(status, requiredVersion, recommendedVersion),
238
+ method,
239
+ status,
240
+ classification,
241
+ updateCommand,
242
+ installCommand: updateCommand,
243
+ currentNativeVersion,
244
+ requiredVersion,
245
+ recommendedVersion,
246
+ releaseUrl: evidence?.releaseUrl || CodexOverleafCompatibility?.buildReleaseUrl?.(recommendedVersion)
247
+ };
248
+ }
249
+
250
+ function getNativeRequiredVersion(evidence = {}) {
251
+ return evidence.requiredVersion ||
252
+ evidence.minimumNativeVersion ||
253
+ CodexOverleafCompatibility?.MIN_COMPATIBLE_NATIVE_VERSION ||
254
+ '1.0.0';
255
+ }
256
+
257
+ function getNativeRecommendedVersion(evidence = {}) {
258
+ return evidence.recommendedVersion ||
259
+ CodexOverleafCompatibility?.BUILD_TARGET_VERSION ||
260
+ getNativeRequiredVersion(evidence);
261
+ }
262
+
263
+ function getCurrentNativeVersion(evidence = {}) {
264
+ return evidence.currentNativeVersion ||
265
+ evidence.nativeVersion ||
266
+ evidence.native?.version;
267
+ }
268
+
269
+ function getNativeUpdateCommand(evidence = {}) {
270
+ const command = CodexOverleafCompatibility?.buildInstallCommand?.(
271
+ getNativeRecommendedVersion(evidence),
272
+ evidence.native?.platform || evidence.platform,
273
+ getCurrentExtensionId()
274
+ );
275
+ if (command) {
276
+ return command;
277
+ }
278
+ return evidence.updateCommand || evidence.installCommand;
279
+ }
280
+
281
+ function getCurrentExtensionId() {
282
+ return typeof chrome === 'object' && chrome?.runtime?.id
283
+ ? chrome.runtime.id
284
+ : '';
285
+ }
286
+
287
+ function sanitizeNativeRequest(request) {
288
+ if (!request?.params || typeof request.params !== 'object') {
289
+ return request;
290
+ }
291
+ const { nativeCompatibility, compatibilityStatus, compatibility, ...params } = request.params;
292
+ if (!nativeCompatibility && !compatibilityStatus && !compatibility) {
293
+ return request;
294
+ }
295
+ return {
296
+ ...request,
297
+ params
298
+ };
299
+ }
300
+
301
+ function formatNativeCompatibilityBlockMessage(status, requiredVersion, recommendedVersion = requiredVersion) {
302
+ switch (status) {
303
+ case 'extension_too_old':
304
+ return 'Extension update required before this request can run. Update the Chrome extension and try again.';
305
+ case 'protocol_unsupported':
306
+ return 'Native host protocol mismatch. Update the extension and native host together, then try again.';
307
+ case 'native_unhealthy':
308
+ return 'Native host responded, but the local Codex environment is not healthy enough to run this request.';
309
+ case 'native_missing':
310
+ return 'Native host is not connected. Install the local native host, reload the extension, and try again.';
311
+ default:
312
+ if (recommendedVersion && recommendedVersion !== requiredVersion) {
313
+ return `This operation requires native host v${requiredVersion} or later. The recommended update target is v${recommendedVersion}. Run the update command to upgrade.`;
314
+ }
315
+ return `This operation requires native host v${requiredVersion} or later. Run the update command to upgrade.`;
316
+ }
317
+ }
318
+
319
+ function sendNativeCancel(payload) {
320
+ const nativePort = ensurePort();
321
+ const id = payload.id || crypto.randomUUID();
322
+ const request = sanitizeNativeRequest({ ...payload, id });
323
+ try {
324
+ nativePort.postMessage(request);
325
+ } catch (error) {
326
+ handleNativeConnectionFailure(
327
+ nativePort,
328
+ getErrorMessage(error, 'Native host connection failed.')
329
+ );
330
+ throw error;
331
+ }
332
+ return id;
333
+ }
334
+
335
+ function postNativeRequest(pendingRequest) {
336
+ const nativePort = ensurePort();
337
+ pendingRequest.nativePort = nativePort;
338
+ nativePort.postMessage(pendingRequest.request);
339
+ }
340
+
341
+ function ensurePort() {
342
+ if (port) {
343
+ return port;
344
+ }
345
+
346
+ const nativePort = chrome.runtime.connectNative(HOST_NAME);
347
+ port = nativePort;
348
+ nativePort.onMessage.addListener(message => {
349
+ if (nativePort !== port) {
350
+ return;
351
+ }
352
+
353
+ const id = message?.id;
354
+ if (!pending.has(id)) {
355
+ if (message?.ok === false && isMissingNativeRequestId(id)) {
356
+ resolveUnmatchedNativeError(message);
357
+ }
358
+ return;
359
+ }
360
+ const pendingRequest = pending.get(id);
361
+ if (message?.event) {
362
+ pendingRequest.eventForwarded = (
363
+ forwardNativeEvent(pendingRequest.tabId, id, message.event) ||
364
+ pendingRequest.eventForwarded
365
+ );
366
+ return;
367
+ }
368
+ pendingRequest.finalResponseReceived = true;
369
+ pending.delete(id);
370
+ pendingRequest.resolve(message);
371
+ });
372
+ nativePort.onDisconnect.addListener(() => {
373
+ if (nativePort !== port) {
374
+ return;
375
+ }
376
+
377
+ const error = chrome.runtime.lastError?.message || 'Native host disconnected';
378
+ handlePortDisconnect(error);
379
+ });
380
+
381
+ return port;
382
+ }
383
+
384
+ function handlePortDisconnect(errorMessage) {
385
+ handleNativeConnectionFailure(port, errorMessage);
386
+ }
387
+
388
+ function handleNativeConnectionFailure(failedPort, errorMessage) {
389
+ if (!failedPort || port === failedPort) {
390
+ port = null;
391
+ }
392
+
393
+ const interruptedRequests = Array.from(pending.entries()).filter(([_pendingId, pendingRequest]) => {
394
+ return !failedPort || pendingRequest.nativePort === failedPort;
395
+ });
396
+ for (const [pendingId, pendingRequest] of interruptedRequests) {
397
+ if (!pending.has(pendingId)) {
398
+ continue;
399
+ }
400
+
401
+ if (canRetryNativeRequest(pendingRequest)) {
402
+ retryNativeRequest(pendingId, pendingRequest);
403
+ continue;
404
+ }
405
+
406
+ rejectInterruptedNativeRequest(pendingId, pendingRequest, errorMessage);
407
+ }
408
+ }
409
+
410
+ function retryNativeRequest(pendingId, pendingRequest) {
411
+ pendingRequest.retryCount += 1;
412
+ try {
413
+ postNativeRequest(pendingRequest);
414
+ } catch (error) {
415
+ handleNativePostFailure(pendingId, pendingRequest, error);
416
+ }
417
+ }
418
+
419
+ function handleNativePostFailure(pendingId, pendingRequest, error) {
420
+ handleNativeConnectionFailure(
421
+ pendingRequest.nativePort || port,
422
+ getErrorMessage(error, 'Native host connection failed.')
423
+ );
424
+ }
425
+
426
+ function rejectInterruptedNativeRequest(pendingId, pendingRequest, errorMessage) {
427
+ pending.delete(pendingId);
428
+ if (pendingRequest.retryClass === 'no_silent_retry') {
429
+ pendingRequest.reject(createNativeRequestError(
430
+ 'native_execution_interrupted',
431
+ 'Native host disconnected while an execution request was running. The request was not retried to avoid repeating side effects.'
432
+ ));
433
+ return;
434
+ }
435
+
436
+ pendingRequest.reject(createNativeRequestError(
437
+ 'native_connection_failed',
438
+ errorMessage || 'Native host disconnected'
439
+ ));
440
+ }
441
+
442
+ function canRetryNativeRequest(pendingRequest) {
443
+ if (pendingRequest.retryCount >= 1 || pendingRequest.finalResponseReceived) {
444
+ return false;
445
+ }
446
+
447
+ return (
448
+ pendingRequest.retryClass === 'safe_read_retry' ||
449
+ pendingRequest.retryClass === 'safe_sync_retry'
450
+ );
451
+ }
452
+
453
+ function resolveUnmatchedNativeError(message) {
454
+ if (pending.size === 1) {
455
+ const [pendingId, pendingRequest] = pending.entries().next().value;
456
+ pendingRequest.finalResponseReceived = true;
457
+ pendingRequest.resolve({
458
+ ...message,
459
+ id: pendingId
460
+ });
461
+ pending.delete(pendingId);
462
+ return;
463
+ }
464
+
465
+ const ambiguousRequests = Array.from(pending.entries());
466
+ if (ambiguousRequests.length > 0) {
467
+ handlePortDisconnect(getUnmatchedNativeErrorMessage(message));
468
+ }
469
+ }
470
+
471
+ function getUnmatchedNativeErrorMessage(message) {
472
+ return getErrorMessage(
473
+ message?.error || message,
474
+ 'Native host returned an error without a request id while multiple requests were pending.'
475
+ );
476
+ }
477
+
478
+ function isMissingNativeRequestId(id) {
479
+ return id === undefined || id === null || id === '';
480
+ }
481
+
482
+ function createNativeRequestError(code, message) {
483
+ const error = new Error(message);
484
+ error.code = code;
485
+ return error;
486
+ }
487
+
488
+ function toUserFacingNativeError(error) {
489
+ return {
490
+ code: error?.code || 'native_connection_failed',
491
+ message: getErrorMessage(error, 'Native host request failed.')
492
+ };
493
+ }
494
+
495
+ function getErrorMessage(error, fallback) {
496
+ const message = String(error?.message || fallback);
497
+ return message.split('\n')[0] || fallback;
498
+ }
499
+
500
+ function isAllowedOverleafSender(sender) {
501
+ if (!sender?.tab) {
502
+ const extensionUrl = chrome.runtime.getURL('');
503
+ const senderUrl = sender?.url || '';
504
+ return sender?.id === chrome.runtime.id && senderUrl.startsWith(extensionUrl);
505
+ }
506
+
507
+ const senderUrl = sender.tab?.url || '';
508
+ try {
509
+ const url = new URL(senderUrl);
510
+ return url.protocol === 'https:' && (
511
+ url.hostname === 'www.overleaf.com' || url.hostname === 'overleaf.com'
512
+ );
513
+ } catch (_error) {
514
+ return false;
515
+ }
516
+ }
517
+
518
+ function forwardNativeEvent(tabId, id, event) {
519
+ if (typeof tabId !== 'number') {
520
+ return false;
521
+ }
522
+
523
+ chrome.tabs.sendMessage(tabId, {
524
+ type: 'codex-overleaf/native-event',
525
+ id,
526
+ event
527
+ }, () => {
528
+ void chrome.runtime.lastError;
529
+ });
530
+ return true;
531
+ }
532
+ })();