codex-overleaf-link 2.3.0 → 2.3.2

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 (50) hide show
  1. package/README.md +23 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/background.js +20 -0
  4. package/extension/src/content/assetTransferBroker.js +169 -0
  5. package/extension/src/content/composerPanel.js +0 -2
  6. package/extension/src/content/contentRuntime.js +135 -111
  7. package/extension/src/content/generated/content.bundle.js +1066 -345
  8. package/extension/src/content/generated/content.bundle.meta.json +7 -3
  9. package/extension/src/content/legacyGlobalRegistry.js +3 -0
  10. package/extension/src/content/markdownText.js +12 -10
  11. package/extension/src/content/mathText.js +155 -9
  12. package/extension/src/content/modelPicker.js +2 -2
  13. package/extension/src/content/modelPickerSupport.js +3 -3
  14. package/extension/src/content/nativeCompatibilityController.js +2 -2
  15. package/extension/src/content/otWarmMirror.js +1 -1
  16. package/extension/src/content/pageBridgeClient.js +1 -0
  17. package/extension/src/content/runResultActions.js +114 -0
  18. package/extension/src/content/runTimelineView.js +23 -0
  19. package/extension/src/content/sessionForkController.js +120 -0
  20. package/extension/src/content/writebackController.js +3 -0
  21. package/extension/src/content/writebackOrchestrator.js +5 -50
  22. package/extension/src/page/binaryAssetUploader.js +343 -0
  23. package/extension/src/page/trackedChangesLifecycle.js +12 -4
  24. package/extension/src/page/writebackRouter.js +6 -2
  25. package/extension/src/pageBridge.js +30 -1
  26. package/extension/src/shared/agentTranscript.js +10 -3
  27. package/extension/src/shared/compatibility.js +4 -2
  28. package/extension/src/shared/i18n.js +18 -4
  29. package/extension/src/shared/models.js +13 -2
  30. package/extension/src/shared/pageRpcContract.js +6 -2
  31. package/extension/src/shared/runExecutionSnapshot.js +14 -4
  32. package/extension/src/shared/runExecutionSnapshotCodec.js +5 -2
  33. package/extension/src/shared/scopedPersistenceQueuePolicy.js +20 -1
  34. package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
  35. package/extension/src/shared/scopedPersistenceTransaction.js +50 -47
  36. package/extension/src/shared/sessionState.js +58 -15
  37. package/extension/src/shared/storageDb.js +6 -5
  38. package/extension/src/shared/storageRunActions.js +11 -1
  39. package/extension/src/shared/writebackOperationEvidence.js +16 -6
  40. package/extension/src/shared/writebackSettlement.js +28 -6
  41. package/extension/styles/panel.css +79 -8
  42. package/native-host/src/codexModels.js +6 -2
  43. package/native-host/src/codexPromptAssembly.js +1 -1
  44. package/native-host/src/codexSessionRunner.js +5 -1
  45. package/native-host/src/codexThreadFork.js +103 -0
  46. package/native-host/src/mirrorWorkspace.js +5 -19
  47. package/native-host/src/nativeAssetTransfer.js +183 -0
  48. package/native-host/src/taskRunnerRuntime.js +33 -81
  49. package/native-host/src/updateManager.js +36 -1
  50. package/package.json +1 -1
@@ -24,7 +24,7 @@
24
24
  'use strict';
25
25
 
26
26
  const DEFAULT_PANEL_STATE = {
27
- mode: 'confirm',
27
+ mode: 'ask',
28
28
  providerId: 'builtin',
29
29
  model: 'gpt-5.4',
30
30
  reasoningEffort: 'high',
@@ -47,13 +47,13 @@
47
47
  customInstructionsByProject: {}
48
48
  };
49
49
 
50
- const VALID_MODES = new Set(['ask', 'confirm', 'auto']);
50
+ const VALID_MODES = new Set(['ask', 'auto']);
51
51
  // Keep the persisted/session-level contract aligned with the model picker
52
52
  // and native provider bridge. Custom providers can explicitly disable
53
53
  // reasoning (`none`) or expose the lightest Codex tier (`minimal`). Dropping
54
54
  // either value here silently restores the global `high` default after the
55
55
  // user has selected it in the composer.
56
- const VALID_REASONING = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']);
56
+ const VALID_REASONING = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
57
57
  const VALID_SPEED_TIERS = new Set(['standard', 'fast']);
58
58
  const VALID_LOCALES = new Set(['en', 'zh']);
59
59
  const VALID_EVENT_STATUSES = new Set(['info', 'running', 'completed', 'failed', 'warning', 'blocked', 'skipped', 'pending']);
@@ -127,12 +127,8 @@
127
127
  ...input
128
128
  };
129
129
 
130
- if (!VALID_MODES.has(state.mode)) {
131
- state.mode = DEFAULT_PANEL_STATE.mode;
132
- }
133
- if (!VALID_REASONING.has(state.reasoningEffort)) {
134
- state.reasoningEffort = DEFAULT_PANEL_STATE.reasoningEffort;
135
- }
130
+ state.mode = normalizeMode(state.mode);
131
+ state.reasoningEffort = normalizeReasoning(state.reasoningEffort);
136
132
  if (!VALID_SPEED_TIERS.has(state.speedTier)) {
137
133
  state.speedTier = DEFAULT_PANEL_STATE.speedTier;
138
134
  }
@@ -221,7 +217,7 @@
221
217
  history,
222
218
  runs,
223
219
  task: sanitizeAssistantVisibleText(session.task),
224
- mode: VALID_MODES.has(session.mode) ? session.mode : fallbackState.mode,
220
+ mode: normalizeMode(session.mode, fallbackState.mode),
225
221
  providerId: typeof session.providerId === 'string' && session.providerId.trim()
226
222
  ? session.providerId.trim()
227
223
  : (fallbackState.providerId || 'builtin'),
@@ -234,7 +230,10 @@
234
230
  : fallbackState.speedTier,
235
231
  requireReviewing: session.requireReviewing !== false,
236
232
  focusFiles: normalizeFocusFiles(session.focusFiles),
233
+ projectReferenceFiles: normalizeProjectReferenceFiles(session.projectReferenceFiles),
237
234
  codexThreadId: typeof session.codexThreadId === 'string' ? session.codexThreadId : '',
235
+ forkedFromSessionId: typeof session.forkedFromSessionId === 'string' ? session.forkedFromSessionId : '',
236
+ forkedFromRunId: typeof session.forkedFromRunId === 'string' ? session.forkedFromRunId : '',
238
237
  pendingInputs: normalizePendingInputs(
239
238
  session.pendingInputs,
240
239
  options.restoreRunningRuns === true,
@@ -311,13 +310,14 @@
311
310
  id: active.id,
312
311
  history: Array.isArray(active.history) ? active.history.slice(-10) : [],
313
312
  focusFiles: normalizeFocusFiles(active.focusFiles),
313
+ projectReferenceFiles: normalizeProjectReferenceFiles(active.projectReferenceFiles),
314
314
  codexThreadId: active.codexThreadId || '',
315
315
  providerId: state.providerId || 'builtin'
316
316
  };
317
317
  state.runs = Array.isArray(active.runs) ? active.runs : [];
318
318
  state.task = typeof active.task === 'string' ? active.task : '';
319
319
  state.focusFiles = normalizeFocusFiles(active.focusFiles);
320
- state.mode = VALID_MODES.has(active.mode) ? active.mode : DEFAULT_PANEL_STATE.mode;
320
+ state.mode = normalizeMode(active.mode);
321
321
  state.requireReviewing = active.requireReviewing !== false;
322
322
 
323
323
  return state;
@@ -339,7 +339,7 @@
339
339
  history: normalizeHistoryEntries(overrides.history),
340
340
  runs,
341
341
  task: sanitizeAssistantVisibleText(overrides.task),
342
- mode: VALID_MODES.has(overrides.mode) ? overrides.mode : DEFAULT_PANEL_STATE.mode,
342
+ mode: normalizeMode(overrides.mode),
343
343
  providerId: typeof overrides.providerId === 'string' && overrides.providerId.trim()
344
344
  ? overrides.providerId.trim()
345
345
  : 'builtin',
@@ -352,7 +352,10 @@
352
352
  : DEFAULT_PANEL_STATE.speedTier,
353
353
  requireReviewing: overrides.requireReviewing !== false,
354
354
  focusFiles: normalizeFocusFiles(overrides.focusFiles),
355
+ projectReferenceFiles: normalizeProjectReferenceFiles(overrides.projectReferenceFiles),
355
356
  codexThreadId: typeof overrides.codexThreadId === 'string' ? overrides.codexThreadId : '',
357
+ forkedFromSessionId: typeof overrides.forkedFromSessionId === 'string' ? overrides.forkedFromSessionId : '',
358
+ forkedFromRunId: typeof overrides.forkedFromRunId === 'string' ? overrides.forkedFromRunId : '',
356
359
  pendingInputs: normalizePendingInputs(overrides.pendingInputs)
357
360
  };
358
361
  }
@@ -638,6 +641,8 @@
638
641
  undoStatus: sanitizeAssistantVisibleText(run.undoStatus),
639
642
  nativeRequestId: sanitizeAssistantVisibleText(run.nativeRequestId),
640
643
  codexTurnId: sanitizeAssistantVisibleText(run.codexTurnId),
644
+ forkSnapshot: run.forkSnapshot === true,
645
+ forkSourceRunId: sanitizeAssistantVisibleText(run.forkSourceRunId),
641
646
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
642
647
  queueItemId: sanitizeAssistantVisibleText(run.queueItemId),
643
648
  interruptedDraft: run.interruptedDraft ? sanitizeAssistantVisibleValue(run.interruptedDraft) : undefined
@@ -904,6 +909,33 @@
904
909
  return files.slice(0, 5);
905
910
  }
906
911
 
912
+ function normalizeProjectReferenceFiles(value) {
913
+ const seen = new Set();
914
+ const files = [];
915
+ for (const item of Array.isArray(value) ? value : []) {
916
+ const rawPath = typeof item === 'string' ? item : item?.path;
917
+ if (typeof rawPath !== 'string' || !rawPath.trim() || rawPath.length > 512) {
918
+ continue;
919
+ }
920
+ if (/^(?:[A-Za-z]:|[\\/])/.test(rawPath) || rawPath.includes('\0')) {
921
+ continue;
922
+ }
923
+ const path = rawPath.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\//, '').trim();
924
+ if (!path || path.split('/').some(segment => !segment || segment === '.' || segment === '..') || seen.has(path)) {
925
+ continue;
926
+ }
927
+ seen.add(path);
928
+ files.push({
929
+ path,
930
+ kind: item?.kind === 'binary' ? 'binary' : 'text'
931
+ });
932
+ if (files.length >= 5000) {
933
+ break;
934
+ }
935
+ }
936
+ return files;
937
+ }
938
+
907
939
  function normalizeCustomInstructionsByProject(value) {
908
940
  const result = {};
909
941
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -994,7 +1026,7 @@
994
1026
  // Skills page was reopened.
995
1027
  codexOverleafSkills: compactCodexOverleafSkillsForStorage(source.codexOverleafSkills),
996
1028
  composerAttachments: normalizeComposerAttachmentsForPersistence(source.composerAttachments),
997
- mode: VALID_MODES.has(active?.mode) ? active.mode : normalizeMode(source.mode),
1029
+ mode: normalizeMode(active?.mode, source.mode),
998
1030
  providerId: typeof source.providerId === 'string' && source.providerId.trim()
999
1031
  ? source.providerId.trim()
1000
1032
  : 'builtin',
@@ -1092,7 +1124,10 @@
1092
1124
  speedTier: normalizeSpeedTier(session.speedTier || fallbackState.speedTier),
1093
1125
  requireReviewing: session.requireReviewing !== false,
1094
1126
  focusFiles: normalizeFocusFiles(session.focusFiles),
1127
+ projectReferenceFiles: normalizeProjectReferenceFiles(session.projectReferenceFiles),
1095
1128
  codexThreadId: typeof session.codexThreadId === 'string' ? session.codexThreadId : '',
1129
+ forkedFromSessionId: normalizeTextField(session.forkedFromSessionId, 160),
1130
+ forkedFromRunId: normalizeTextField(session.forkedFromRunId, 160),
1096
1131
  pendingInputs: normalizePendingInputs(session.pendingInputs)
1097
1132
  };
1098
1133
  // Welcome-panel + write-guard: preserve the four
@@ -1185,6 +1220,8 @@
1185
1220
  undoStatus: summarizeTextForStorage(run.undoStatus, 'undo status'),
1186
1221
  nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
1187
1222
  codexTurnId: normalizeTextField(run.codexTurnId, 160),
1223
+ forkSnapshot: run.forkSnapshot === true,
1224
+ forkSourceRunId: normalizeTextField(run.forkSourceRunId, 160),
1188
1225
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
1189
1226
  queueItemId: normalizeTextField(run.queueItemId, 160)
1190
1227
  };
@@ -1339,8 +1376,14 @@
1339
1376
  };
1340
1377
  }
1341
1378
 
1342
- function normalizeMode(mode) {
1343
- return VALID_MODES.has(mode) ? mode : DEFAULT_PANEL_STATE.mode;
1379
+ function normalizeMode(mode, fallback = DEFAULT_PANEL_STATE.mode) {
1380
+ if (mode === 'confirm') {
1381
+ return 'ask';
1382
+ }
1383
+ if (VALID_MODES.has(mode)) {
1384
+ return mode;
1385
+ }
1386
+ return VALID_MODES.has(fallback) ? fallback : DEFAULT_PANEL_STATE.mode;
1344
1387
  }
1345
1388
 
1346
1389
  function normalizeReasoning(reasoningEffort) {
@@ -287,7 +287,7 @@
287
287
  history: compactHistoryForStorage(input.history),
288
288
  runs: compactRunsForStorage(input.runs, options),
289
289
  task: normalizeDisplayTextForStorage(input.task, SESSION_STORAGE_LIMITS.taskChars),
290
- mode: typeof input.mode === 'string' ? input.mode : '', providerId: typeof input.providerId === 'string' && input.providerId ? input.providerId : 'builtin',
290
+ mode: normalizeCurrentMode(input.mode), providerId: typeof input.providerId === 'string' && input.providerId ? input.providerId : 'builtin',
291
291
  model: typeof input.model === 'string' ? input.model : '',
292
292
  reasoningEffort: typeof input.reasoningEffort === 'string' ? input.reasoningEffort : '',
293
293
  speedTier: typeof input.speedTier === 'string' ? input.speedTier : '',
@@ -435,6 +435,9 @@
435
435
  }
436
436
 
437
437
  // --- Helpers ---
438
+ function normalizeCurrentMode(mode) {
439
+ return mode === 'auto' ? 'auto' : 'ask';
440
+ }
438
441
 
439
442
  function extractLightweightPrefs(state, projectId) {
440
443
  var prefs = {
@@ -442,8 +445,9 @@
442
445
  model: typeof state.model === 'string' ? state.model : '',
443
446
  reasoningEffort: typeof state.reasoningEffort === 'string' ? state.reasoningEffort : '',
444
447
  speedTier: typeof state.speedTier === 'string' ? state.speedTier : '',
445
- mode: typeof state.mode === 'string' ? state.mode : '',
448
+ mode: normalizeCurrentMode(state.mode),
446
449
  locale: typeof state.locale === 'string' ? state.locale : '',
450
+ theme: state.theme === 'light' || state.theme === 'auto' ? state.theme : 'dark',
447
451
  requireReviewing: state.requireReviewing !== false,
448
452
  autoRecompile: state.autoRecompile !== false,
449
453
  loadCodexLocalSkills: state.loadCodexLocalSkills !== false,
@@ -1314,11 +1318,9 @@
1314
1318
  }
1315
1319
 
1316
1320
  // --- Utilities ---
1317
-
1318
1321
  function generateId(prefix) {
1319
1322
  return prefix + '_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
1320
1323
  }
1321
-
1322
1324
  function safeJsonStringify(value) {
1323
1325
  try {
1324
1326
  return JSON.stringify(value) || '';
@@ -1326,7 +1328,6 @@
1326
1328
  return '[unserializable]';
1327
1329
  }
1328
1330
  }
1329
-
1330
1331
  var hashString = StorageRunActions.hashString;
1331
1332
 
1332
1333
  return {
@@ -38,8 +38,18 @@
38
38
  && Array.isArray(run.undoTrackedChanges) && run.undoTrackedChanges.length > 0
39
39
  && Array.isArray(run.undoExpectedFiles) && run.undoExpectedFiles.length > 0
40
40
  && Array.isArray(run.appliedOperations) && run.appliedOperations.length > 0;
41
+ // Overleaf can expose a working native editor-undo checkpoint before its
42
+ // Reviewing DOM markers are discoverable. Preserve that checkpoint too so
43
+ // a reload does not erase the only safe rollback path while the document
44
+ // still contains an unresolved tracked change.
45
+ var reviewingEditorUndo = trackedStatus !== 'accepted'
46
+ && trackedStatus !== 'rejected'
47
+ && run && run.undoStatus !== 'applied'
48
+ && run.executionSnapshot && run.executionSnapshot.requireReviewing === true
49
+ && Array.isArray(run.undoExpectedFiles) && run.undoExpectedFiles.length > 0
50
+ && Array.isArray(run.appliedOperations) && run.appliedOperations.length > 0;
41
51
  var legacyUndo = Array.isArray(run && run.undoOperations) && run.undoOperations.length > 0;
42
- return trackedLifecycle || legacyUndo;
52
+ return trackedLifecycle || reviewingEditorUndo || legacyUndo;
43
53
  }
44
54
 
45
55
  function compactRunActionPayload(run, keepActionPayload) {
@@ -114,12 +114,22 @@
114
114
  }
115
115
 
116
116
  function normalizeTrackedChanges(value) {
117
- return (Array.isArray(value) ? value : []).map(item => ({
118
- key: normalizeText(item?.key),
119
- id: normalizeText(item?.id),
120
- path: normalizeText(item?.path),
121
- label: normalizeText(item?.label)
122
- })).filter(item => item.key || item.id || item.path);
117
+ const normalized = [];
118
+ const seen = new Set();
119
+ for (const item of Array.isArray(value) ? value : []) {
120
+ const change = {
121
+ key: normalizeText(item?.key),
122
+ id: normalizeText(item?.id),
123
+ path: normalizeText(item?.path),
124
+ label: normalizeText(item?.label)
125
+ };
126
+ if (!change.key && !change.id && !change.path) continue;
127
+ const identity = `${change.path}\u0000${change.key || change.id || 'path-only'}`;
128
+ if (seen.has(identity)) continue;
129
+ seen.add(identity);
130
+ normalized.push(change);
131
+ }
132
+ return normalized;
123
133
  }
124
134
 
125
135
  function normalizeStringArray(value) {
@@ -270,9 +270,17 @@
270
270
  }
271
271
  const successful = isSuccessfulTrackedChangeSettlement(input.result);
272
272
  const reviewCodes = kind === 'accept' ? ACCEPT_NEEDS_REVIEW_CODES : REJECT_NEEDS_REVIEW_CODES;
273
- const needsReview = !successful && failures.some(failure =>
273
+ // Reject/Undo is an all-target recovery transaction. A successful entry
274
+ // only proves that one tracked target was restored; any failed/skipped
275
+ // sibling can still leave this run's content in another file. Keep that
276
+ // recovery actionable instead of collapsing a partial multi-file reject
277
+ // into the terminal `rejected` state. Accept retains its established
278
+ // best-effort terminal policy.
279
+ const incompleteReject = kind === 'reject'
280
+ && collectRunResultSkipped(input.result).length > 0;
281
+ const needsReview = incompleteReject || (!successful && failures.some(failure =>
274
282
  failure.terminalState === 'needs_review' || reviewCodes.has(failure.code)
275
- );
283
+ ));
276
284
  const status = needsReview ? 'needs_review' : kind === 'accept' ? 'accepted' : 'rejected';
277
285
  return {
278
286
  decision: status,
@@ -595,7 +603,12 @@
595
603
  : operation;
596
604
  }
597
605
 
598
- function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = []) {
606
+ function selectExpectedFilesForTrackedUndo(
607
+ project,
608
+ operations = [],
609
+ trackedChanges = [],
610
+ previousExpectedFiles = []
611
+ ) {
599
612
  const paths = new Set();
600
613
  for (const change of trackedChanges || []) {
601
614
  if (change?.path) paths.add(change.path);
@@ -604,9 +617,18 @@
604
617
  if (operation?.path) paths.add(operation.path);
605
618
  if (operation?.to) paths.add(operation.to);
606
619
  }
607
- return (project?.files || [])
608
- .filter(file => paths.has(file.path) && typeof file.content === 'string')
609
- .map(file => ({ path: file.path, content: file.content }));
620
+ const expectedByPath = new Map();
621
+ for (const file of previousExpectedFiles || []) {
622
+ if (paths.has(file?.path) && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
623
+ expectedByPath.set(file.path, { path: file.path, content: file.content });
624
+ }
625
+ }
626
+ for (const file of project?.files || []) {
627
+ if (paths.has(file?.path) && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
628
+ expectedByPath.set(file.path, { path: file.path, content: file.content });
629
+ }
630
+ }
631
+ return Array.from(expectedByPath.values());
610
632
  }
611
633
 
612
634
  function acceptDiagnosticStatus(step, info) {
@@ -2359,6 +2359,67 @@
2359
2359
  margin-left: auto;
2360
2360
  }
2361
2361
 
2362
+ #codex-overleaf-panel .run-result-actions {
2363
+ display: flex;
2364
+ justify-content: flex-start;
2365
+ align-items: center;
2366
+ gap: 2px;
2367
+ min-height: 30px;
2368
+ margin: 10px 0 0;
2369
+ }
2370
+
2371
+ #codex-overleaf-panel .run-result-actions[hidden] { display: none; }
2372
+
2373
+ #codex-overleaf-panel .run-copy-action,
2374
+ #codex-overleaf-panel .run-fork-action {
2375
+ display: inline-grid;
2376
+ place-items: center;
2377
+ width: 30px;
2378
+ height: 30px;
2379
+ border: 0;
2380
+ border-radius: 999px;
2381
+ background: transparent;
2382
+ color: var(--tl-fg-4);
2383
+ opacity: 1;
2384
+ transition: color 120ms ease, background 120ms ease, opacity 120ms ease;
2385
+ }
2386
+
2387
+ #codex-overleaf-panel .run-copy-action:hover:not(:disabled),
2388
+ #codex-overleaf-panel .run-fork-action:hover:not(:disabled) {
2389
+ background: var(--tl-hover);
2390
+ color: var(--tl-accent);
2391
+ }
2392
+
2393
+ #codex-overleaf-panel .run-copy-action:disabled,
2394
+ #codex-overleaf-panel .run-fork-action:disabled {
2395
+ cursor: not-allowed;
2396
+ opacity: 0.38;
2397
+ }
2398
+
2399
+ #codex-overleaf-panel .run-copy-action svg,
2400
+ #codex-overleaf-panel .run-fork-action svg {
2401
+ width: 16px;
2402
+ height: 16px;
2403
+ fill: none;
2404
+ stroke: currentColor;
2405
+ stroke-width: 1.7;
2406
+ stroke-linecap: round;
2407
+ stroke-linejoin: round;
2408
+ }
2409
+
2410
+ #codex-overleaf-panel .run-copy-action[hidden],
2411
+ #codex-overleaf-panel .run-fork-action[hidden] { display: none; }
2412
+
2413
+ #codex-overleaf-panel .run-completed-time {
2414
+ margin-left: 5px;
2415
+ color: var(--tl-fg-4);
2416
+ font-size: 12px;
2417
+ font-variant-numeric: tabular-nums;
2418
+ line-height: 30px;
2419
+ }
2420
+
2421
+ #codex-overleaf-panel .run-completed-time[hidden] { display: none; }
2422
+
2362
2423
  /* When Accept All is hidden, the Undo button takes the right-aligned slot. */
2363
2424
  #codex-overleaf-panel .run-turn-meta [data-run-accept][hidden] + [data-run-undo] {
2364
2425
  margin-left: auto;
@@ -2686,13 +2747,23 @@
2686
2747
  }
2687
2748
 
2688
2749
  #codex-overleaf-panel .run-math--fallback {
2689
- border-radius: 4px;
2690
- background: var(--tl-surface-2);
2691
- color: var(--tl-review);
2692
- padding: 1px 4px;
2750
+ border-bottom: 1px dotted var(--tl-fg-4);
2751
+ border-radius: 0;
2752
+ background: transparent;
2753
+ color: inherit;
2754
+ padding: 0 2px;
2693
2755
  white-space: pre-wrap;
2694
2756
  overflow-wrap: anywhere;
2695
- font: 0.96em/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
2757
+ font: inherit;
2758
+ }
2759
+
2760
+ #codex-overleaf-panel .run-math--display.run-math--fallback {
2761
+ border: 1px solid var(--tl-border);
2762
+ border-left: 3px solid var(--tl-fg-4);
2763
+ border-radius: 6px;
2764
+ background: var(--tl-surface-2);
2765
+ padding: 8px 10px;
2766
+ text-align: left;
2696
2767
  }
2697
2768
 
2698
2769
  #codex-overleaf-panel .run-stream[data-stream-role="reasoning"] .run-stream-text {
@@ -3173,12 +3244,12 @@
3173
3244
  white-space: nowrap;
3174
3245
  }
3175
3246
 
3176
- /* Mode switch (Ask/Suggest/Auto) — a real segmented control: a bordered track
3177
- holding three choices. Inactive choices are muted; hover is a light fill
3247
+ /* Mode switch (Ask/Auto) — a real segmented control: a bordered track
3248
+ holding two choices. Inactive choices are muted; hover is a light fill
3178
3249
  (distinct from the active accent-wash fill) so hover never reads as active. */
3179
3250
  #codex-overleaf-panel .codex-mode-switch {
3180
3251
  display: grid;
3181
- grid-template-columns: repeat(3, minmax(0, 1fr));
3252
+ grid-template-columns: repeat(2, minmax(0, 1fr));
3182
3253
  gap: 2px;
3183
3254
  min-width: 0;
3184
3255
  padding: 2px;
@@ -181,7 +181,7 @@ function buildModelEntry({
181
181
  defaultSpeedTier = 'standard'
182
182
  }) {
183
183
  const normalizedEfforts = normalizeReasoningEfforts(reasoningEfforts);
184
- const normalizedDefault = getString(defaultReasoningEffort) || 'medium';
184
+ const normalizedDefault = normalizeReasoningEffort(defaultReasoningEffort) || 'medium';
185
185
  const normalizedSpeedTiers = normalizeSpeedTiers(speedTiers);
186
186
  const normalizedDefaultSpeedTier = getString(defaultSpeedTier) || 'standard';
187
187
  return {
@@ -202,7 +202,7 @@ function normalizeReasoningEfforts(rawEfforts) {
202
202
  const seen = new Set();
203
203
  const result = [];
204
204
  for (const rawEffort of rawEfforts) {
205
- const effort = getString(typeof rawEffort === 'string' ? rawEffort : rawEffort?.effort);
205
+ const effort = normalizeReasoningEffort(typeof rawEffort === 'string' ? rawEffort : rawEffort?.effort);
206
206
  if (!effort || seen.has(effort)) {
207
207
  continue;
208
208
  }
@@ -212,6 +212,10 @@ function normalizeReasoningEfforts(rawEfforts) {
212
212
  return result;
213
213
  }
214
214
 
215
+ function normalizeReasoningEffort(rawEffort) {
216
+ return getString(rawEffort);
217
+ }
218
+
215
219
  function normalizeSpeedTiers(rawTiers) {
216
220
  const tiers = ['standard'];
217
221
  if (!Array.isArray(rawTiers)) {
@@ -64,7 +64,7 @@ function buildCodexTurnPrompt(options = {}) {
64
64
  'Mode for this turn:',
65
65
  `- ${context.mode}`,
66
66
  '- ask: inspect and explain only; do not edit files.',
67
- '- confirm/auto: edit the local workspace directly when the request calls for changes. The browser bridge handles review, confirmation, deletion approval, and syncing back to Overleaf.',
67
+ '- auto: edit the local workspace directly when the request calls for changes. The browser bridge handles authorization, deletion approval, and syncing back to Overleaf.',
68
68
  '',
69
69
  'Write expectation for this turn:',
70
70
  formatWriteExpectation({
@@ -22,6 +22,7 @@ const {
22
22
  loadSelectedProjectSkills
23
23
  } = require('./localSkills');
24
24
  const { createSubagentBroker } = require('./subagentBroker');
25
+ const { prepareBinaryAssetChanges } = require('./nativeAssetTransfer');
25
26
  const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
26
27
  const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch } = require('./codexProviderLaunch');
27
28
  const { buildReadProgressRules, createReadProgressController } = require('./readProgressGuard');
@@ -259,6 +260,9 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
259
260
  });
260
261
  }
261
262
 
263
+ const preparedAssets = prepareBinaryAssetChanges({ projectId, rootDir, changes: rawSyncChanges });
264
+ rawSyncChanges = preparedAssets.changes;
265
+ unsupportedChanges.push(...preparedAssets.unsupportedChanges);
262
266
  const syncChanges = rawSyncChanges.map(change => {
263
267
  if (change.type === 'write' && typeof change.previousContent === 'string') {
264
268
  return {
@@ -1461,7 +1465,7 @@ function extractCodexAppServerErrorMessage(params = {}) {
1461
1465
  }
1462
1466
 
1463
1467
  function normalizeReasoningEffort(value) {
1464
- return ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(value) ? value : null;
1468
+ return ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(value) ? value : null;
1465
1469
  }
1466
1470
 
1467
1471
  function normalizeSpeedTier(value) {
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('node:child_process');
4
+ const { version: PACKAGE_VERSION } = require('../../package.json');
5
+ const { buildCodexHomeEnv } = require('./codexHome');
6
+ const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
7
+ const { buildCodexAppServerArgs } = require('./codexSessionRunner');
8
+
9
+ function forkCodexThread(params = {}, env = process.env) {
10
+ const threadId = cleanId(params.threadId, 'threadId');
11
+ const lastTurnId = cleanId(params.lastTurnId, 'lastTurnId');
12
+ const childEnv = buildCodexHomeEnv(env, {
13
+ loadCodexLocalSkills: params.loadCodexLocalSkills !== false,
14
+ loadCodexOverleafSkills: params.loadCodexOverleafSkills !== false
15
+ });
16
+ const command = resolveCodexCommand(childEnv);
17
+ if (!command) return Promise.reject(codedError('codex_not_found', 'Codex CLI was not found.'));
18
+
19
+ return new Promise((resolve, reject) => {
20
+ const child = spawn(command, buildCodexAppServerArgs({
21
+ speedTier: params.speedTier || 'standard',
22
+ loadCodexLocalSkills: params.loadCodexLocalSkills !== false
23
+ }), {
24
+ env: childEnv,
25
+ shell: shouldUseShellForCommand(command, childEnv),
26
+ stdio: ['pipe', 'pipe', 'pipe']
27
+ });
28
+ const pending = new Map();
29
+ let nextId = 1;
30
+ let buffer = '';
31
+ let stderr = '';
32
+ let settled = false;
33
+ const timeout = setTimeout(() => finish(codedError('codex_thread_fork_timeout', 'Codex thread fork timed out.')), 30000);
34
+
35
+ child.stdout.setEncoding('utf8');
36
+ child.stderr.setEncoding('utf8');
37
+ child.stdout.on('data', chunk => {
38
+ buffer += chunk;
39
+ const lines = buffer.split(/\r?\n/);
40
+ buffer = lines.pop() || '';
41
+ for (const line of lines) if (line.trim()) handleMessage(line);
42
+ });
43
+ child.stderr.on('data', chunk => { stderr += chunk; });
44
+ child.on('error', finish);
45
+ child.on('close', code => {
46
+ if (!settled) finish(codedError('codex_thread_fork_failed', stderr || `Codex app-server exited with code ${code}.`));
47
+ });
48
+
49
+ (async () => {
50
+ await request('initialize', {
51
+ clientInfo: { name: 'codex-overleaf-link', version: PACKAGE_VERSION },
52
+ capabilities: null
53
+ });
54
+ notify('initialized');
55
+ const result = await request('thread/fork', { threadId, lastTurnId });
56
+ const forkedThreadId = result?.thread?.id || result?.threadId || '';
57
+ if (!forkedThreadId || forkedThreadId === threadId) {
58
+ throw codedError('codex_thread_fork_invalid_response', 'Codex app-server did not return a new thread id.');
59
+ }
60
+ finish(null, { threadId: forkedThreadId, sourceThreadId: threadId, lastTurnId });
61
+ })().catch(finish);
62
+
63
+ function request(method, requestParams) {
64
+ const id = nextId++;
65
+ child.stdin.write(`${JSON.stringify({ id, method, params: requestParams })}\n`);
66
+ return new Promise((resolveRequest, rejectRequest) => pending.set(id, { resolve: resolveRequest, reject: rejectRequest }));
67
+ }
68
+ function notify(method, requestParams) {
69
+ child.stdin.write(`${JSON.stringify({ method, params: requestParams })}\n`);
70
+ }
71
+ function handleMessage(line) {
72
+ let message;
73
+ try { message = JSON.parse(line); } catch (_error) { return; }
74
+ if (Object.prototype.hasOwnProperty.call(message, 'id') && (message.result !== undefined || message.error)) {
75
+ const waiter = pending.get(message.id);
76
+ if (!waiter) return;
77
+ pending.delete(message.id);
78
+ if (message.error) waiter.reject(codedError('codex_thread_fork_failed', message.error.message || JSON.stringify(message.error)));
79
+ else waiter.resolve(message.result);
80
+ } else if (Object.prototype.hasOwnProperty.call(message, 'id') && message.method) {
81
+ child.stdin.write(`${JSON.stringify({ id: message.id, error: { code: -32601, message: 'Unsupported during thread fork' } })}\n`);
82
+ }
83
+ }
84
+ function finish(error, result) {
85
+ if (settled) return;
86
+ settled = true;
87
+ clearTimeout(timeout);
88
+ for (const waiter of pending.values()) waiter.reject(error || codedError('codex_thread_fork_closed', 'Fork process closed.'));
89
+ pending.clear();
90
+ try { child.stdin.end(); } catch (_error) { /* best effort */ }
91
+ try { child.kill(); } catch (_error) { /* best effort */ }
92
+ if (error) reject(error); else resolve(result);
93
+ }
94
+ });
95
+ }
96
+
97
+ function cleanId(value, field) {
98
+ const id = String(value || '').trim();
99
+ if (!id || id.length > 160) throw codedError('codex_thread_fork_invalid', `${field} is required.`);
100
+ return id;
101
+ }
102
+ function codedError(code, message) { const error = new Error(message); error.code = code; return error; }
103
+ module.exports = { forkCodexThread };
@@ -152,34 +152,19 @@ async function collectMirrorChangesDetailed({ projectId, rootDir }) {
152
152
  if (hashBytes(bytes) === previous.hash) {
153
153
  continue;
154
154
  }
155
- if (stat.size > SAFE_INLINE_BINARY_CHANGE_BYTES) {
156
- unsupportedChanges.push(buildOversizedBinaryPayloadChange({
157
- filePath,
158
- size: stat.size,
159
- previous
160
- }));
161
- continue;
162
- }
163
155
  currentByPath.set(filePath, {
164
156
  binary: true,
165
- bytes,
157
+ sha256: hashBytes(bytes),
166
158
  size: stat.size,
167
159
  previous
168
160
  });
169
161
  continue;
170
162
  }
171
163
  if (!previous) {
172
- if (stat.size > SAFE_INLINE_BINARY_CHANGE_BYTES) {
173
- unsupportedChanges.push(buildOversizedBinaryPayloadChange({
174
- filePath,
175
- size: stat.size,
176
- previous
177
- }));
178
- continue;
179
- }
164
+ const bytes = fs.readFileSync(target);
180
165
  currentByPath.set(filePath, {
181
166
  binary: true,
182
- bytes: fs.readFileSync(target),
167
+ sha256: hashBytes(bytes),
183
168
  size: stat.size,
184
169
  previous
185
170
  });
@@ -218,7 +203,8 @@ async function collectMirrorChangesDetailed({ projectId, rootDir }) {
218
203
  changes.push({
219
204
  type: previous ? 'overwrite-binary' : 'binary-create',
220
205
  path: filePath,
221
- contentBase64: contentOrBinary.bytes.toString('base64'),
206
+ assetSourcePath: filePath,
207
+ sha256: contentOrBinary.sha256,
222
208
  previousExists: Boolean(previous),
223
209
  previousKind: previous?.kind || '',
224
210
  previousSize: previous?.size,