codex-overleaf-link 2.3.1 → 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 (33) hide show
  1. package/README.md +23 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/content/assetTransferBroker.js +169 -0
  4. package/extension/src/content/contentRuntime.js +27 -1
  5. package/extension/src/content/generated/content.bundle.js +551 -65
  6. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  7. package/extension/src/content/legacyGlobalRegistry.js +3 -0
  8. package/extension/src/content/modelPicker.js +2 -2
  9. package/extension/src/content/modelPickerSupport.js +3 -3
  10. package/extension/src/content/pageBridgeClient.js +1 -0
  11. package/extension/src/content/runResultActions.js +114 -0
  12. package/extension/src/content/runTimelineView.js +17 -0
  13. package/extension/src/content/sessionForkController.js +120 -0
  14. package/extension/src/content/writebackController.js +3 -0
  15. package/extension/src/content/writebackOrchestrator.js +3 -1
  16. package/extension/src/page/binaryAssetUploader.js +343 -0
  17. package/extension/src/page/trackedChangesLifecycle.js +2 -0
  18. package/extension/src/pageBridge.js +30 -1
  19. package/extension/src/shared/compatibility.js +4 -2
  20. package/extension/src/shared/i18n.js +16 -0
  21. package/extension/src/shared/models.js +13 -2
  22. package/extension/src/shared/pageRpcContract.js +5 -1
  23. package/extension/src/shared/sessionState.js +12 -4
  24. package/extension/src/shared/storageDb.js +1 -1
  25. package/extension/src/shared/storageRunActions.js +11 -1
  26. package/extension/styles/panel.css +61 -0
  27. package/native-host/src/codexModels.js +6 -2
  28. package/native-host/src/codexSessionRunner.js +5 -1
  29. package/native-host/src/codexThreadFork.js +103 -0
  30. package/native-host/src/mirrorWorkspace.js +5 -19
  31. package/native-host/src/nativeAssetTransfer.js +183 -0
  32. package/native-host/src/taskRunnerRuntime.js +22 -0
  33. package/package.json +1 -1
@@ -0,0 +1,343 @@
1
+ (function initCodexOverleafBinaryAssetUploader(root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafBinaryAssetUploader = api;
5
+ })(typeof window !== 'undefined' ? window : globalThis, function binaryAssetUploaderFactory() {
6
+ 'use strict';
7
+
8
+ const MAX_ASSET_BYTES = 10 * 1024 * 1024;
9
+
10
+ function create(deps = {}) {
11
+ const windowRef = deps.window || globalThis.window;
12
+ const documentRef = deps.document || windowRef?.document;
13
+ const treeOperations = deps.treeOperations;
14
+ const snapshotRouter = deps.snapshotRouter;
15
+ const transfers = new Map();
16
+
17
+ function begin(params = {}) {
18
+ let transferId;
19
+ let projectPath;
20
+ try {
21
+ transferId = cleanTransferId(params.transferId);
22
+ projectPath = normalizePath(params.path);
23
+ } catch (error) {
24
+ return failure('binary_asset_path_invalid', error.message);
25
+ }
26
+ const size = Number(params.size);
27
+ if (!Number.isSafeInteger(size) || size < 0 || size > MAX_ASSET_BYTES) {
28
+ return failure('binary_asset_size_invalid', 'Asset size is invalid or exceeds 10 MiB.');
29
+ }
30
+ if (!/^[a-f0-9]{64}$/i.test(String(params.sha256 || ''))) {
31
+ return failure('binary_asset_hash_invalid', 'Asset SHA-256 is missing or invalid.');
32
+ }
33
+ transfers.set(transferId, {
34
+ id: transferId,
35
+ path: projectPath,
36
+ size,
37
+ sha256: String(params.sha256).toLowerCase(),
38
+ mimeType: String(params.mimeType || 'application/octet-stream'),
39
+ overwrite: params.overwrite === true,
40
+ chunks: [],
41
+ received: 0
42
+ });
43
+ return { ok: true, transferId };
44
+ }
45
+
46
+ function append(params = {}) {
47
+ const transfer = transfers.get(String(params.transferId || ''));
48
+ if (!transfer) return failure('binary_upload_transfer_missing', 'Asset upload transfer was not found.');
49
+ if (Number(params.offset) !== transfer.received) {
50
+ return failure('binary_upload_chunk_order_invalid', 'Asset chunks arrived out of order.');
51
+ }
52
+ let bytes;
53
+ try { bytes = decodeBase64(params.contentBase64, windowRef); } catch (_error) {
54
+ return failure('binary_upload_chunk_invalid', 'Asset chunk is not valid Base64.');
55
+ }
56
+ if (transfer.received + bytes.length > transfer.size) {
57
+ return failure('binary_upload_size_mismatch', 'Asset chunks exceed the declared size.');
58
+ }
59
+ transfer.chunks.push(bytes);
60
+ transfer.received += bytes.length;
61
+ return { ok: true, transferId: transfer.id, received: transfer.received };
62
+ }
63
+
64
+ async function commit(params = {}) {
65
+ const transferId = String(params.transferId || '');
66
+ const transfer = transfers.get(transferId);
67
+ if (!transfer) return failure('binary_upload_transfer_missing', 'Asset upload transfer was not found.');
68
+ try {
69
+ if (transfer.received !== transfer.size) return failure('binary_upload_incomplete', 'Asset upload is incomplete.');
70
+ const bytes = joinChunks(transfer.chunks, transfer.size);
71
+ const digest = await sha256Hex(bytes, windowRef.crypto);
72
+ if (digest !== transfer.sha256) {
73
+ return failure('binary_upload_checksum_mismatch', 'Asset checksum did not match the Native Host descriptor.');
74
+ }
75
+ const fileName = transfer.path.split('/').pop();
76
+ const file = new windowRef.File([bytes], fileName, { type: transfer.mimeType });
77
+ const attempts = [
78
+ // Prefer Overleaf's visible uploader. Its UI contract is more
79
+ // stable than the private multipart endpoint across deployments.
80
+ () => uploadWithDom(file, transfer),
81
+ () => uploadWithMultipart(file, transfer),
82
+ () => uploadWithLegacyManager(file, transfer)
83
+ ];
84
+ const errors = [];
85
+ for (const attempt of attempts) {
86
+ try {
87
+ const result = await attempt();
88
+ const observed = result?.ok ? await waitForPath(transfer.path, 12000) : null;
89
+ const hashVerified = observed ? await verifyRemoteHash(observed, transfer).catch(() => false) : false;
90
+ const overwriteVerified = !transfer.overwrite || hashVerified || result?.confirmedByTransport === true;
91
+ if (result?.ok && observed && overwriteVerified) {
92
+ return { ok: true, written: true, path: transfer.path, method: result.method, changedDocument: true };
93
+ }
94
+ if (result?.ok && observed && !overwriteVerified) errors.push('Overleaf did not expose enough evidence to verify the binary replacement.');
95
+ if (result?.reason) errors.push(result.reason);
96
+ } catch (error) {
97
+ errors.push(error.message);
98
+ }
99
+ }
100
+ return failure('binary_upload_unavailable', errors.filter(Boolean).join('; ') || 'No supported Overleaf asset upload path succeeded.');
101
+ } finally {
102
+ transfers.delete(transferId);
103
+ }
104
+ }
105
+
106
+ function abort(params = {}) {
107
+ return { ok: true, aborted: transfers.delete(String(params.transferId || '')) };
108
+ }
109
+
110
+ async function uploadWithMultipart(file, transfer) {
111
+ const projectId = treeOperations.getProjectId();
112
+ if (!projectId || typeof windowRef.fetch !== 'function') return { ok: false, reason: 'Project upload endpoint is unavailable.' };
113
+ const parentPath = transfer.path.split('/').slice(0, -1).join('/');
114
+ const folderId = await findFolderId(parentPath);
115
+ if (parentPath && !folderId) return { ok: false, reason: 'Target folder id is unavailable for direct upload.' };
116
+ const form = new windowRef.FormData();
117
+ form.append('qqfile', file, file.name);
118
+ form.append('name', file.name);
119
+ if (folderId) form.append('folder_id', folderId);
120
+ const csrf = findCsrfToken();
121
+ const headers = csrf ? { 'X-CSRF-Token': csrf } : {};
122
+ const query = folderId ? `?folder_id=${encodeURIComponent(folderId)}` : '';
123
+ const response = await windowRef.fetch(`/project/${encodeURIComponent(projectId)}/upload${query}`, {
124
+ method: 'POST', body: form, credentials: 'same-origin', headers
125
+ });
126
+ const responseText = await response.text();
127
+ if (!response.ok) throw new Error(formatUploadHttpError(response.status, responseText));
128
+ try {
129
+ const payload = responseText ? JSON.parse(responseText) : null;
130
+ if (payload && (payload.success === false || payload.ok === false || payload.error)) {
131
+ throw new Error(payload.error?.message || payload.error || payload.message || 'Overleaf rejected the asset upload.');
132
+ }
133
+ } catch (error) {
134
+ if (error instanceof SyntaxError) {
135
+ // Some Overleaf deployments return an empty or HTML success body.
136
+ } else {
137
+ throw error;
138
+ }
139
+ }
140
+ return { ok: true, method: 'overleaf.multipart', confirmedByTransport: true };
141
+ }
142
+
143
+ async function uploadWithDom(file, transfer) {
144
+ const parentPath = transfer.path.split('/').slice(0, -1).join('/');
145
+ if (parentPath) {
146
+ const folderNode = treeOperations.findFileTreeNode(parentPath);
147
+ if (!folderNode) return { ok: false, reason: `Target folder ${parentPath} is unavailable in the Overleaf file tree.` };
148
+ folderNode?.dispatchEvent(new windowRef.MouseEvent('click', { bubbles: true }));
149
+ await delay(120);
150
+ }
151
+ let input = findFileInput();
152
+ if (!input) {
153
+ const button = findUploadButton();
154
+ if (!button) return { ok: false, reason: 'Overleaf upload button was not found.' };
155
+ button.click();
156
+ input = await waitForFileInput(4000);
157
+ }
158
+ if (!input) return { ok: false, reason: 'Overleaf upload file input was not found.' };
159
+ const data = new windowRef.DataTransfer();
160
+ data.items.add(file);
161
+ assignFilesToInput(input, data.files, windowRef);
162
+ input.dispatchEvent(new windowRef.Event('input', { bubbles: true }));
163
+ input.dispatchEvent(new windowRef.Event('change', { bubbles: true }));
164
+ await clickReplaceIfShown();
165
+ return { ok: true, method: 'overleaf.file-input' };
166
+ }
167
+
168
+ async function uploadWithLegacyManager(file, transfer) {
169
+ const manager = treeOperations.findFileTreeManager();
170
+ if (!manager) return { ok: false, reason: 'Legacy file-tree manager is unavailable.' };
171
+ for (const name of ['uploadFile', 'uploadAsset', 'createBinaryFile', 'createFile', 'addFile']) {
172
+ if (typeof manager[name] !== 'function') continue;
173
+ try {
174
+ await manager[name](transfer.path, file);
175
+ return { ok: true, method: `fileTreeManager.${name}`, confirmedByTransport: true };
176
+ } catch (_error) { /* try the next adapter */ }
177
+ }
178
+ return { ok: false, reason: 'Legacy file-tree manager has no compatible asset method.' };
179
+ }
180
+
181
+ async function findFolderId(parentPath) {
182
+ const node = parentPath ? treeOperations.findFileTreeNode(parentPath) : null;
183
+ const element = node?.closest?.('[data-entity-id], [data-folder-id], [data-id]') || node;
184
+ const domId = element?.dataset?.entityId || element?.dataset?.folderId || element?.dataset?.id;
185
+ if (domId) return domId;
186
+ for (const root of [windowRef._ide, windowRef.Overleaf, windowRef.overleaf, windowRef.OL]) {
187
+ const folder = root?.project?.rootFolder?.[0] || root?.project?.rootFolder || root?.rootFolder?.[0] || root?.rootFolder;
188
+ if (!parentPath && (folder?._id || folder?.id)) return folder._id || folder.id;
189
+ }
190
+ try {
191
+ const list = await snapshotRouter?.buildProjectFileList({
192
+ force: true, maxAgeMs: 0, preferLightweight: true, allowZipFallback: false
193
+ });
194
+ const folder = (list?.files || []).find(item => item.path === parentPath && /folder/i.test(item.kind || item.type || ''));
195
+ if (folder) return folder.id || folder._id || folder.entityId || '';
196
+ } catch (_error) { /* DOM and legacy adapters remain available */ }
197
+ return '';
198
+ }
199
+
200
+ function findCsrfToken() {
201
+ return documentRef.querySelector('meta[name="ol-csrfToken"], meta[name="csrf-token"], meta[name="_csrf"]')?.content
202
+ || windowRef.csrfToken || windowRef._csrf || '';
203
+ }
204
+ function findFileInput() {
205
+ return chooseOverleafFileInput(Array.from(documentRef.querySelectorAll('input[type="file"]')));
206
+ }
207
+ function findUploadButton() {
208
+ return chooseOverleafUploadButton(Array.from(documentRef.querySelectorAll('button, [role="button"]')));
209
+ }
210
+ function chooseOverleafUploadButton(nodes) {
211
+ return nodes.filter(node => {
212
+ if (!node || node.disabled || isCodexOwnedNode(node)) return false;
213
+ const label = `${node.getAttribute('aria-label') || ''} ${node.getAttribute('title') || ''} ${node.textContent || ''}`;
214
+ return /upload|上传/i.test(label);
215
+ }).sort((left, right) => uploadButtonScore(right) - uploadButtonScore(left))[0] || null;
216
+ }
217
+ async function waitForFileInput(timeoutMs) {
218
+ const deadline = Date.now() + timeoutMs;
219
+ while (Date.now() < deadline) {
220
+ const input = findFileInput();
221
+ if (input) return input;
222
+ await delay(50);
223
+ }
224
+ return null;
225
+ }
226
+ async function clickReplaceIfShown() {
227
+ await delay(180);
228
+ const button = Array.from(documentRef.querySelectorAll('button')).find(node => /replace|overwrite|替换|覆盖/i.test(node.textContent || ''));
229
+ if (button) button.click();
230
+ }
231
+ async function waitForPath(projectPath, timeoutMs) {
232
+ const deadline = Date.now() + timeoutMs;
233
+ while (Date.now() < deadline) {
234
+ if (treeOperations.projectPathExists(projectPath)) {
235
+ const node = treeOperations.findFileTreeNode(projectPath);
236
+ return {
237
+ path: projectPath,
238
+ id: node?.dataset?.entityId || node?.dataset?.fileId || node?.dataset?.id || ''
239
+ };
240
+ }
241
+ await delay(250);
242
+ try {
243
+ const list = await snapshotRouter?.buildProjectFileList({
244
+ force: true, maxAgeMs: 0, preferLightweight: true, allowZipFallback: false
245
+ });
246
+ const file = (list?.files || []).find(item => item.path === projectPath);
247
+ if (file) return file;
248
+ } catch (_error) { /* continue until the deadline */ }
249
+ }
250
+ return null;
251
+ }
252
+ async function verifyRemoteHash(observed, transfer) {
253
+ const fileId = observed?.id || observed?._id || observed?.entityId || observed?.fileId || '';
254
+ const projectId = treeOperations.getProjectId();
255
+ if (!fileId || !projectId || typeof windowRef.fetch !== 'function') return false;
256
+ const response = await windowRef.fetch(`/project/${encodeURIComponent(projectId)}/file/${encodeURIComponent(fileId)}`, {
257
+ credentials: 'same-origin', cache: 'no-store'
258
+ });
259
+ if (!response.ok) return false;
260
+ const digest = await sha256Hex(new Uint8Array(await response.arrayBuffer()), windowRef.crypto);
261
+ return digest === transfer.sha256;
262
+ }
263
+ function delay(ms) { return new Promise(resolve => windowRef.setTimeout(resolve, ms)); }
264
+ return { abort, append, begin, commit };
265
+ }
266
+
267
+ function normalizePath(value) {
268
+ const path = String(value || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
269
+ if (!path || path.split('/').some(part => !part || part === '.' || part === '..')) throw new Error('Asset path is invalid.');
270
+ return path;
271
+ }
272
+ function cleanTransferId(value) {
273
+ const id = String(value || '');
274
+ if (!/^[a-zA-Z0-9_-]{8,120}$/.test(id)) throw new Error('Asset transfer id is invalid.');
275
+ return id;
276
+ }
277
+ function chooseOverleafFileInput(inputs) {
278
+ return Array.from(inputs || [])
279
+ .filter(input => input && !input.disabled && !isCodexOwnedNode(input))
280
+ .sort((left, right) => uploadInputScore(right) - uploadInputScore(left))[0] || null;
281
+ }
282
+ function isCodexOwnedNode(node) {
283
+ return Boolean(node?.closest?.('#codex-overleaf-panel, [data-codex-overleaf-root], .codex-overleaf-panel'));
284
+ }
285
+ function uploadInputScore(input) {
286
+ let score = 0;
287
+ if (input?.closest?.('.uppy-Dashboard, [data-uppy-drag-drop-supported]')) score += 100;
288
+ if (input?.closest?.('[role="dialog"], .modal')) score += 40;
289
+ if (/uppy|upload/i.test(`${input?.className || ''} ${input?.getAttribute?.('data-testid') || ''}`)) score += 30;
290
+ if (input?.multiple) score += 10;
291
+ if (input?.isConnected !== false) score += 1;
292
+ return score;
293
+ }
294
+ function uploadButtonScore(node) {
295
+ let score = 0;
296
+ const label = `${node?.getAttribute?.('aria-label') || ''} ${node?.getAttribute?.('title') || ''} ${node?.textContent || ''}`.trim();
297
+ if (/^(upload|上传)$/i.test(label)) score += 50;
298
+ if (node?.closest?.('[data-testid*="file-tree"], .file-tree, .ide-react-file-tree')) score += 30;
299
+ if (node?.isConnected !== false) score += 1;
300
+ return score;
301
+ }
302
+ function assignFilesToInput(input, files, windowRef) {
303
+ const prototype = windowRef?.HTMLInputElement?.prototype;
304
+ const setter = prototype && Object.getOwnPropertyDescriptor(prototype, 'files')?.set;
305
+ if (setter) {
306
+ setter.call(input, files);
307
+ return;
308
+ }
309
+ Object.defineProperty(input, 'files', { configurable: true, value: files });
310
+ }
311
+ function formatUploadHttpError(status, responseText) {
312
+ const detail = summarizeUploadResponse(responseText);
313
+ return `Overleaf upload endpoint returned HTTP ${status}${detail ? ` (${detail})` : ''}.`;
314
+ }
315
+ function summarizeUploadResponse(value) {
316
+ const text = String(value || '').trim();
317
+ if (!text || /^\s*</.test(text)) return '';
318
+ let message = text;
319
+ try {
320
+ const payload = JSON.parse(text);
321
+ message = payload?.error?.message || payload?.error || payload?.message || text;
322
+ } catch (_error) { /* use the bounded plain-text response */ }
323
+ return String(message || '').replace(/\s+/g, ' ').slice(0, 240);
324
+ }
325
+ function decodeBase64(value, windowRef) {
326
+ const binary = windowRef.atob(String(value || ''));
327
+ const bytes = new Uint8Array(binary.length);
328
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
329
+ return bytes;
330
+ }
331
+ function joinChunks(chunks, size) {
332
+ const output = new Uint8Array(size);
333
+ let offset = 0;
334
+ for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.length; }
335
+ return output;
336
+ }
337
+ async function sha256Hex(bytes, cryptoImpl) {
338
+ const digest = await cryptoImpl.subtle.digest('SHA-256', bytes);
339
+ return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
340
+ }
341
+ function failure(code, reason) { return { ok: false, code, reason, changedDocument: false }; }
342
+ return { chooseOverleafFileInput, create, formatUploadHttpError, joinChunks, normalizePath };
343
+ });
@@ -1498,6 +1498,8 @@
1498
1498
  '[data-track-change-id]',
1499
1499
  '[data-ol-change-id]',
1500
1500
  '[data-path][class*="change" i]',
1501
+ '.ol-cm-change',
1502
+ '.review-panel-entry-change',
1501
1503
  '[class*="track-change" i]',
1502
1504
  '[class*="review-change" i]',
1503
1505
  '[class*="suggest" i]',
@@ -22,6 +22,7 @@
22
22
  let projectSnapshotBridge = null;
23
23
  let writebackRouter = null;
24
24
  let writeGuard = null;
25
+ let binaryAssetUploader = null;
25
26
  const saveState = requirePageModule('CodexOverleafSaveState').create({
26
27
  delay,
27
28
  detectEditor,
@@ -66,6 +67,12 @@
66
67
  treeOperations,
67
68
  window
68
69
  });
70
+ binaryAssetUploader = requirePageModule('CodexOverleafBinaryAssetUploader').create({
71
+ document,
72
+ snapshotRouter,
73
+ treeOperations,
74
+ window
75
+ });
69
76
  writeGuard = requirePageModule('CodexOverleafWriteGuard').create({
70
77
  document,
71
78
  treeOperations,
@@ -207,6 +214,10 @@
207
214
  requireEditing: params.requireEditing === true,
208
215
  runProjectId: typeof params.runProjectId === 'string' ? params.runProjectId : ''
209
216
  }),
217
+ binaryUploadBegin: params => binaryAssetUploader.begin(params),
218
+ binaryUploadAppend: params => binaryAssetUploader.append(params),
219
+ binaryUploadCommit: params => binaryAssetUploader.commit(params),
220
+ binaryUploadAbort: params => binaryAssetUploader.abort(params),
210
221
  jumpToPosition,
211
222
  rejectTrackedChanges,
212
223
  acceptTrackedChanges,
@@ -613,7 +624,7 @@
613
624
  return range;
614
625
  }
615
626
 
616
- const focused = editorAdapter.focusActiveEditorRange(range.from, range.to);
627
+ let focused = editorAdapter.focusActiveEditorRange(range.from, range.to);
617
628
  if (!focused.ok) {
618
629
  return {
619
630
  ...focused,
@@ -621,6 +632,24 @@
621
632
  };
622
633
  }
623
634
 
635
+ if (params.line !== undefined) {
636
+ // Overleaf can publish the new file path and text before its unstable
637
+ // store swaps in the final CodeMirror view. Re-resolve the editor after
638
+ // two short settlement windows so a first dispatch to the retiring view
639
+ // cannot leave the visible cursor at its old line. Repeating an already
640
+ // successful selection is idempotent.
641
+ for (let attempt = 0; attempt < 2; attempt += 1) {
642
+ await delay(120);
643
+ if (getActiveFilePath() !== filePath) {
644
+ break;
645
+ }
646
+ const settled = editorAdapter.focusActiveEditorRange(range.from, range.to);
647
+ if (settled.ok) {
648
+ focused = settled;
649
+ }
650
+ }
651
+ }
652
+
624
653
  return {
625
654
  ok: true,
626
655
  method: focused.method,
@@ -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 = '2.3.1';
15
+ const BUILD_TARGET_VERSION = '2.3.2';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -26,7 +26,9 @@
26
26
  'historyClearPlugin',
27
27
  'localSkills',
28
28
  'mirrorSensitiveScan',
29
- 'providerProfiles'
29
+ 'providerProfiles',
30
+ 'assetTransfer',
31
+ 'threadFork'
30
32
  ]);
31
33
  const UPDATE_AVAILABLE_CAPABILITIES = Object.freeze([
32
34
  'bridgePing',
@@ -94,6 +94,14 @@
94
94
  deleteSessionHistoryFailedToast: 'Deleted this plugin session, but the matching plugin-local Codex thread history was not fully cleared: {message}',
95
95
  deleteSessionNoThreadToast: 'Deleted this plugin session. This old session had no identifiable local Codex thread history, so no local history was cleared.',
96
96
  deleteSessionDoneToast: 'Deleted this plugin session.',
97
+ forkRunTitle: 'Fork a new conversation from this turn',
98
+ forkRunUnavailable: 'This turn cannot be forked because its Codex thread position is unavailable.',
99
+ forkRunFailed: 'Could not fork this conversation.',
100
+ forkRunDone: 'Forked conversation created.',
101
+ forkSessionTitle: '{title} (fork)',
102
+ copyConclusionTitle: 'Copy conclusion',
103
+ copyConclusionDone: 'Conclusion copied',
104
+ copyConclusionFailed: 'Could not copy conclusion',
97
105
  modeSwitchFailedToast: 'Could not switch mode: {message}',
98
106
  diffAccepted: 'Accepted',
99
107
  diffRejected: 'Rejected',
@@ -606,6 +614,14 @@
606
614
  deleteSessionHistoryFailedToast: '已删除这个插件会话,但对应的插件本地 Codex 线程历史没有清理完成:{message}',
607
615
  deleteSessionNoThreadToast: '已删除这个插件会话;这个旧会话没有可识别的本地 Codex 线程历史,未清理本地历史。',
608
616
  deleteSessionDoneToast: '已删除这个插件会话。',
617
+ forkRunTitle: '从此轮分叉一个新对话',
618
+ forkRunUnavailable: '该轮缺少可定位的 Codex thread 节点,无法分叉。',
619
+ forkRunFailed: '无法分叉此对话。',
620
+ forkRunDone: '已创建分叉对话。',
621
+ forkSessionTitle: '{title}(分叉)',
622
+ copyConclusionTitle: '复制结论',
623
+ copyConclusionDone: '已复制结论',
624
+ copyConclusionFailed: '无法复制结论',
609
625
  modeSwitchFailedToast: '切换模式失败:{message}',
610
626
  diffAccepted: '已接受',
611
627
  diffRejected: '已拒绝',
@@ -62,13 +62,13 @@
62
62
  const normalized = {
63
63
  id,
64
64
  label,
65
- reasoningEfforts: Array.isArray(model?.reasoningEfforts) ? model.reasoningEfforts.slice() : [],
65
+ reasoningEfforts: normalizeReasoningEfforts(model?.reasoningEfforts),
66
66
  reasoningPresentation: typeof model?.reasoningPresentation === 'string' ? model.reasoningPresentation : '',
67
67
  speedTiers: normalizeSpeedTiers(model?.speedTiers)
68
68
  };
69
69
 
70
70
  if (Object.prototype.hasOwnProperty.call(Object(model), 'defaultReasoningEffort')) {
71
- normalized.defaultReasoningEffort = model.defaultReasoningEffort;
71
+ normalized.defaultReasoningEffort = normalizeReasoningEffort(model.defaultReasoningEffort);
72
72
  }
73
73
  if (Object.prototype.hasOwnProperty.call(Object(model), 'defaultSpeedTier')) {
74
74
  normalized.defaultSpeedTier = model.defaultSpeedTier;
@@ -77,6 +77,17 @@
77
77
  return normalized;
78
78
  }
79
79
 
80
+ function normalizeReasoningEfforts(reasoningEfforts) {
81
+ if (!Array.isArray(reasoningEfforts)) {
82
+ return [];
83
+ }
84
+ return Array.from(new Set(reasoningEfforts.map(normalizeReasoningEffort).filter(Boolean)));
85
+ }
86
+
87
+ function normalizeReasoningEffort(reasoningEffort) {
88
+ return normalizeModelId(reasoningEffort);
89
+ }
90
+
80
91
  function normalizeSpeedTiers(speedTiers) {
81
92
  if (!Array.isArray(speedTiers)) {
82
93
  return ['standard'];
@@ -5,7 +5,7 @@
5
5
  })(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this), function () {
6
6
  'use strict';
7
7
 
8
- var REVISION = '2026-07-31-page-rpc-contract-v14';
8
+ var REVISION = '2026-08-04-page-rpc-contract-v15';
9
9
  var METHODS = freezeCatalog({
10
10
  initializeCapability: entry('bridge.initialize', 'control', 'none', 'default', false, 'none'),
11
11
  probe: entry('bridge.probe', 'read', 'none', 'default', false, 'safe'),
@@ -17,6 +17,10 @@
17
17
  ensureReviewing: entry('reviewing.enable', 'page_state', 'optional', 'default', true, 'idempotent'),
18
18
  ensureEditing: entry('reviewing.disable', 'page_state', 'optional', 'default', true, 'idempotent'),
19
19
  applyOperations: entry('document.write', 'document', 'required', 'writeback', true, 'no_retry'),
20
+ binaryUploadBegin: entry('asset.stage', 'cache', 'required', 'default', false, 'no_retry'),
21
+ binaryUploadAppend: entry('asset.stage', 'cache', 'none', 'writeback', true, 'no_retry'),
22
+ binaryUploadCommit: entry('asset.write', 'document', 'required', 'writeback', true, 'no_retry'),
23
+ binaryUploadAbort: entry('asset.stage', 'cache', 'none', 'default', false, 'idempotent'),
20
24
  jumpToPosition: entry('editor.navigate', 'navigation', 'required', 'default', false, 'idempotent'),
21
25
  rejectTrackedChanges: entry('tracked_changes.reject', 'document', 'required', 'lifecycle', true, 'no_retry'),
22
26
  acceptTrackedChanges: entry('tracked_changes.accept', 'document', 'required', 'lifecycle', true, 'no_retry'),
@@ -53,7 +53,7 @@
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']);
@@ -128,9 +128,7 @@
128
128
  };
129
129
 
130
130
  state.mode = normalizeMode(state.mode);
131
- if (!VALID_REASONING.has(state.reasoningEffort)) {
132
- state.reasoningEffort = DEFAULT_PANEL_STATE.reasoningEffort;
133
- }
131
+ state.reasoningEffort = normalizeReasoning(state.reasoningEffort);
134
132
  if (!VALID_SPEED_TIERS.has(state.speedTier)) {
135
133
  state.speedTier = DEFAULT_PANEL_STATE.speedTier;
136
134
  }
@@ -234,6 +232,8 @@
234
232
  focusFiles: normalizeFocusFiles(session.focusFiles),
235
233
  projectReferenceFiles: normalizeProjectReferenceFiles(session.projectReferenceFiles),
236
234
  codexThreadId: typeof session.codexThreadId === 'string' ? session.codexThreadId : '',
235
+ forkedFromSessionId: typeof session.forkedFromSessionId === 'string' ? session.forkedFromSessionId : '',
236
+ forkedFromRunId: typeof session.forkedFromRunId === 'string' ? session.forkedFromRunId : '',
237
237
  pendingInputs: normalizePendingInputs(
238
238
  session.pendingInputs,
239
239
  options.restoreRunningRuns === true,
@@ -354,6 +354,8 @@
354
354
  focusFiles: normalizeFocusFiles(overrides.focusFiles),
355
355
  projectReferenceFiles: normalizeProjectReferenceFiles(overrides.projectReferenceFiles),
356
356
  codexThreadId: typeof overrides.codexThreadId === 'string' ? overrides.codexThreadId : '',
357
+ forkedFromSessionId: typeof overrides.forkedFromSessionId === 'string' ? overrides.forkedFromSessionId : '',
358
+ forkedFromRunId: typeof overrides.forkedFromRunId === 'string' ? overrides.forkedFromRunId : '',
357
359
  pendingInputs: normalizePendingInputs(overrides.pendingInputs)
358
360
  };
359
361
  }
@@ -639,6 +641,8 @@
639
641
  undoStatus: sanitizeAssistantVisibleText(run.undoStatus),
640
642
  nativeRequestId: sanitizeAssistantVisibleText(run.nativeRequestId),
641
643
  codexTurnId: sanitizeAssistantVisibleText(run.codexTurnId),
644
+ forkSnapshot: run.forkSnapshot === true,
645
+ forkSourceRunId: sanitizeAssistantVisibleText(run.forkSourceRunId),
642
646
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
643
647
  queueItemId: sanitizeAssistantVisibleText(run.queueItemId),
644
648
  interruptedDraft: run.interruptedDraft ? sanitizeAssistantVisibleValue(run.interruptedDraft) : undefined
@@ -1122,6 +1126,8 @@
1122
1126
  focusFiles: normalizeFocusFiles(session.focusFiles),
1123
1127
  projectReferenceFiles: normalizeProjectReferenceFiles(session.projectReferenceFiles),
1124
1128
  codexThreadId: typeof session.codexThreadId === 'string' ? session.codexThreadId : '',
1129
+ forkedFromSessionId: normalizeTextField(session.forkedFromSessionId, 160),
1130
+ forkedFromRunId: normalizeTextField(session.forkedFromRunId, 160),
1125
1131
  pendingInputs: normalizePendingInputs(session.pendingInputs)
1126
1132
  };
1127
1133
  // Welcome-panel + write-guard: preserve the four
@@ -1214,6 +1220,8 @@
1214
1220
  undoStatus: summarizeTextForStorage(run.undoStatus, 'undo status'),
1215
1221
  nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
1216
1222
  codexTurnId: normalizeTextField(run.codexTurnId, 160),
1223
+ forkSnapshot: run.forkSnapshot === true,
1224
+ forkSourceRunId: normalizeTextField(run.forkSourceRunId, 160),
1217
1225
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
1218
1226
  queueItemId: normalizeTextField(run.queueItemId, 160)
1219
1227
  };
@@ -435,7 +435,6 @@
435
435
  }
436
436
 
437
437
  // --- Helpers ---
438
-
439
438
  function normalizeCurrentMode(mode) {
440
439
  return mode === 'auto' ? 'auto' : 'ask';
441
440
  }
@@ -448,6 +447,7 @@
448
447
  speedTier: typeof state.speedTier === 'string' ? state.speedTier : '',
449
448
  mode: normalizeCurrentMode(state.mode),
450
449
  locale: typeof state.locale === 'string' ? state.locale : '',
450
+ theme: state.theme === 'light' || state.theme === 'auto' ? state.theme : 'dark',
451
451
  requireReviewing: state.requireReviewing !== false,
452
452
  autoRecompile: state.autoRecompile !== false,
453
453
  loadCodexLocalSkills: state.loadCodexLocalSkills !== false,
@@ -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) {