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.
- package/README.md +23 -23
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/content/assetTransferBroker.js +169 -0
- package/extension/src/content/contentRuntime.js +27 -1
- package/extension/src/content/generated/content.bundle.js +551 -65
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +3 -0
- package/extension/src/content/modelPicker.js +2 -2
- package/extension/src/content/modelPickerSupport.js +3 -3
- package/extension/src/content/pageBridgeClient.js +1 -0
- package/extension/src/content/runResultActions.js +114 -0
- package/extension/src/content/runTimelineView.js +17 -0
- package/extension/src/content/sessionForkController.js +120 -0
- package/extension/src/content/writebackController.js +3 -0
- package/extension/src/content/writebackOrchestrator.js +3 -1
- package/extension/src/page/binaryAssetUploader.js +343 -0
- package/extension/src/page/trackedChangesLifecycle.js +2 -0
- package/extension/src/pageBridge.js +30 -1
- package/extension/src/shared/compatibility.js +4 -2
- package/extension/src/shared/i18n.js +16 -0
- package/extension/src/shared/models.js +13 -2
- package/extension/src/shared/pageRpcContract.js +5 -1
- package/extension/src/shared/sessionState.js +12 -4
- package/extension/src/shared/storageDb.js +1 -1
- package/extension/src/shared/storageRunActions.js +11 -1
- package/extension/styles/panel.css +61 -0
- package/native-host/src/codexModels.js +6 -2
- package/native-host/src/codexSessionRunner.js +5 -1
- package/native-host/src/codexThreadFork.js +103 -0
- package/native-host/src/mirrorWorkspace.js +5 -19
- package/native-host/src/nativeAssetTransfer.js +183 -0
- package/native-host/src/taskRunnerRuntime.js +22 -0
- 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
|
-
|
|
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.
|
|
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:
|
|
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-
|
|
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
|
-
|
|
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) {
|