codex-overleaf-link 2.3.6 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -34
- package/README.zh-CN.md +546 -0
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +3 -0
- package/extension/src/content/activeTurnControl.js +1 -0
- package/extension/src/content/assetTransferBroker.js +2 -0
- package/extension/src/content/contentRuntime.js +122 -118
- package/extension/src/content/contextTray.js +4 -0
- package/extension/src/content/diagnosticsController.js +4 -0
- package/extension/src/content/diagnosticsPanel.js +128 -4
- package/extension/src/content/generated/content.bundle.js +1233 -550
- package/extension/src/content/generated/content.bundle.meta.json +4 -3
- package/extension/src/content/legacyGlobalRegistry.js +1 -0
- package/extension/src/content/modelPicker.js +21 -9
- package/extension/src/content/modelPickerSupport.js +38 -1
- package/extension/src/content/nativeCompatibilityController.js +4 -1
- package/extension/src/content/otWarmMirror.js +34 -30
- package/extension/src/content/otWarmMirrorController.js +20 -0
- package/extension/src/content/pageBridgeClient.js +2 -0
- package/extension/src/content/panelMaintenance.js +8 -4
- package/extension/src/content/recentProjects.js +354 -355
- package/extension/src/content/runResultActions.js +16 -1
- package/extension/src/content/runTimelineView.js +9 -11
- package/extension/src/content/scopedPersistenceCoordinator.js +2 -1
- package/extension/src/content/scopedPersistencePanelState.js +45 -2
- package/extension/src/content/sessionForkController.js +20 -11
- package/extension/src/content/sessionPersistence.js +4 -1
- package/extension/src/content/settingsPanel.js +16 -2
- package/extension/src/content/trackedChangeCaptureController.js +153 -0
- package/extension/src/content/writebackOrchestrator.js +31 -0
- package/extension/src/page/binaryAssetUploader.js +101 -29
- package/extension/src/page/overleafRealtimeObserver.js +4 -1
- package/extension/src/page/saveState.js +44 -1
- package/extension/src/page/textFileCreator.js +493 -0
- package/extension/src/page/trackedChangeCapture.js +342 -0
- package/extension/src/page/trackedChangesLifecycle.js +149 -150
- package/extension/src/page/treeOperations.js +40 -29
- package/extension/src/page/writebackRouter.js +95 -49
- package/extension/src/pageBridge.js +65 -25
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/i18n.js +94 -40
- package/extension/src/shared/pageRpcContract.js +2 -1
- package/extension/src/shared/sessionState.js +6 -2
- package/extension/src/shared/storageDb.js +11 -12
- package/extension/src/shared/storageRunActions.js +73 -1
- package/extension/src/shared/writebackSettlement.js +19 -5
- package/extension/styles/panel.css +254 -405
- package/native-host/src/codexSessionRunner.js +41 -38
- package/native-host/src/codexSessionTiming.js +68 -0
- package/native-host/src/readProgressGuard.js +84 -17
- package/native-host/src/taskRunnerRuntime.js +25 -0
- package/package.json +5 -1
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
size,
|
|
37
37
|
sha256: String(params.sha256).toLowerCase(),
|
|
38
38
|
mimeType: String(params.mimeType || 'application/octet-stream'),
|
|
39
|
+
projectId: treeOperations.getProjectId(),
|
|
39
40
|
overwrite: params.overwrite === true,
|
|
40
41
|
chunks: [],
|
|
41
42
|
received: 0
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
const transfer = transfers.get(transferId);
|
|
67
68
|
if (!transfer) return failure('binary_upload_transfer_missing', 'Asset upload transfer was not found.');
|
|
68
69
|
try {
|
|
70
|
+
assertTransferProject(transfer);
|
|
69
71
|
if (transfer.received !== transfer.size) return failure('binary_upload_incomplete', 'Asset upload is incomplete.');
|
|
70
72
|
const bytes = joinChunks(transfer.chunks, transfer.size);
|
|
71
73
|
const digest = await sha256Hex(bytes, windowRef.crypto);
|
|
@@ -84,9 +86,12 @@
|
|
|
84
86
|
const errors = [];
|
|
85
87
|
for (const attempt of attempts) {
|
|
86
88
|
try {
|
|
89
|
+
assertTransferProject(transfer);
|
|
87
90
|
const result = await attempt();
|
|
88
|
-
const
|
|
89
|
-
const
|
|
91
|
+
const evidence = result?.ok ? await waitForPath(transfer, result, 12000) : null;
|
|
92
|
+
const observed = evidence?.observed;
|
|
93
|
+
const hashVerified = evidence?.hashVerified === true;
|
|
94
|
+
assertTransferProject(transfer);
|
|
90
95
|
const overwriteVerified = !transfer.overwrite || hashVerified || result?.confirmedByTransport === true;
|
|
91
96
|
if (result?.ok && observed && overwriteVerified) {
|
|
92
97
|
return { ok: true, written: true, path: transfer.path, method: result.method, changedDocument: true };
|
|
@@ -97,7 +102,8 @@
|
|
|
97
102
|
errors.push(error.message);
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
|
-
return failure('binary_upload_unavailable', errors.filter(Boolean).join('; ') || 'No supported Overleaf asset upload path succeeded.')
|
|
105
|
+
return { ...failure('binary_upload_unavailable', errors.filter(Boolean).join('; ') || 'No supported Overleaf asset upload path succeeded.'),
|
|
106
|
+
changedDocument: transfer.mutationAttempted === true };
|
|
101
107
|
} finally {
|
|
102
108
|
transfers.delete(transferId);
|
|
103
109
|
}
|
|
@@ -107,6 +113,13 @@
|
|
|
107
113
|
return { ok: true, aborted: transfers.delete(String(params.transferId || '')) };
|
|
108
114
|
}
|
|
109
115
|
|
|
116
|
+
function assertTransferProject(transfer) {
|
|
117
|
+
if (transfers.get(transfer.id) !== transfer) throw new Error('Asset upload was cancelled or replaced.');
|
|
118
|
+
if (!transfer.projectId || treeOperations.getProjectId() !== transfer.projectId) {
|
|
119
|
+
throw new Error('Project changed during asset upload.');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
async function uploadWithMultipart(file, transfer) {
|
|
111
124
|
const projectId = treeOperations.getProjectId();
|
|
112
125
|
if (!projectId || typeof windowRef.fetch !== 'function') return { ok: false, reason: 'Project upload endpoint is unavailable.' };
|
|
@@ -120,6 +133,7 @@
|
|
|
120
133
|
const csrf = findCsrfToken();
|
|
121
134
|
const headers = csrf ? { 'X-CSRF-Token': csrf } : {};
|
|
122
135
|
const query = folderId ? `?folder_id=${encodeURIComponent(folderId)}` : '';
|
|
136
|
+
assertTransferProject(transfer);
|
|
123
137
|
const response = await windowRef.fetch(`/project/${encodeURIComponent(projectId)}/upload${query}`, {
|
|
124
138
|
method: 'POST', body: form, credentials: 'same-origin', headers
|
|
125
139
|
});
|
|
@@ -143,11 +157,16 @@
|
|
|
143
157
|
async function uploadWithDom(file, transfer) {
|
|
144
158
|
const parentPath = transfer.path.split('/').slice(0, -1).join('/');
|
|
145
159
|
if (parentPath) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
await
|
|
160
|
+
if (typeof deps.prepareUploadParent !== 'function') {
|
|
161
|
+
return { ok: false, reason: 'Verified folder selection is unavailable for nested asset upload.' };
|
|
162
|
+
}
|
|
163
|
+
await deps.prepareUploadParent(parentPath, {
|
|
164
|
+
createMissing: true,
|
|
165
|
+
onMutation: () => { transfer.mutationAttempted = true; },
|
|
166
|
+
isCurrent: () => treeOperations.getProjectId() === transfer.projectId && transfers.get(transfer.id) === transfer
|
|
167
|
+
});
|
|
150
168
|
}
|
|
169
|
+
assertTransferProject(transfer);
|
|
151
170
|
let input = findFileInput();
|
|
152
171
|
if (!input) {
|
|
153
172
|
const button = findUploadButton();
|
|
@@ -158,10 +177,12 @@
|
|
|
158
177
|
if (!input) return { ok: false, reason: 'Overleaf upload file input was not found.' };
|
|
159
178
|
const data = new windowRef.DataTransfer();
|
|
160
179
|
data.items.add(file);
|
|
180
|
+
assertTransferProject(transfer);
|
|
161
181
|
assignFilesToInput(input, data.files, windowRef);
|
|
182
|
+
transfer.mutationAttempted = true;
|
|
162
183
|
input.dispatchEvent(new windowRef.Event('input', { bubbles: true }));
|
|
163
184
|
input.dispatchEvent(new windowRef.Event('change', { bubbles: true }));
|
|
164
|
-
await clickReplaceIfShown();
|
|
185
|
+
await clickReplaceIfShown(transfer);
|
|
165
186
|
return { ok: true, method: 'overleaf.file-input' };
|
|
166
187
|
}
|
|
167
188
|
|
|
@@ -179,9 +200,9 @@
|
|
|
179
200
|
}
|
|
180
201
|
|
|
181
202
|
async function findFolderId(parentPath) {
|
|
182
|
-
const node = parentPath ?
|
|
183
|
-
|
|
184
|
-
const domId =
|
|
203
|
+
const node = parentPath ? (deps.findFolderNode?.(parentPath)
|
|
204
|
+
|| findNativeTreeNode(parentPath, 'folder') || treeOperations.findFileTreeNode(parentPath)) : null;
|
|
205
|
+
const domId = readTreeEntityId(node);
|
|
185
206
|
if (domId) return domId;
|
|
186
207
|
for (const root of [windowRef._ide, windowRef.Overleaf, windowRef.overleaf, windowRef.OL]) {
|
|
187
208
|
const folder = root?.project?.rootFolder?.[0] || root?.project?.rootFolder || root?.rootFolder?.[0] || root?.rootFolder;
|
|
@@ -223,31 +244,82 @@
|
|
|
223
244
|
}
|
|
224
245
|
return null;
|
|
225
246
|
}
|
|
226
|
-
async function clickReplaceIfShown() {
|
|
247
|
+
async function clickReplaceIfShown(transfer) {
|
|
227
248
|
await delay(180);
|
|
228
|
-
|
|
249
|
+
assertTransferProject(transfer);
|
|
250
|
+
if (!transfer.overwrite) return;
|
|
251
|
+
const button = Array.from(documentRef.querySelectorAll('[role="dialog"] button')).find(node => !isCodexOwnedNode(node)
|
|
252
|
+
&& /replace|overwrite|替换|覆盖/i.test(node.textContent || ''));
|
|
229
253
|
if (button) button.click();
|
|
230
254
|
}
|
|
231
|
-
|
|
255
|
+
function readTreeEntityId(node) {
|
|
256
|
+
const owner = node?.closest?.('[data-entity-id], [data-file-id], [data-folder-id], [data-id]');
|
|
257
|
+
const row = node?.closest?.('[role="treeitem"]') || node;
|
|
258
|
+
const entity = row?.querySelector?.(':scope > .entity[data-file-id]');
|
|
259
|
+
for (const candidate of [node, entity, owner]) {
|
|
260
|
+
const id = candidate?.dataset?.entityId || candidate?.dataset?.fileId
|
|
261
|
+
|| candidate?.dataset?.folderId || candidate?.dataset?.id;
|
|
262
|
+
if (id) return id;
|
|
263
|
+
}
|
|
264
|
+
return '';
|
|
265
|
+
}
|
|
266
|
+
function findNativeTreeNode(projectPath, type) {
|
|
267
|
+
const root = documentRef.querySelector?.('[data-testid="file-tree-list-root"]');
|
|
268
|
+
for (const row of root?.querySelectorAll?.('[role="treeitem"]') || []) {
|
|
269
|
+
const entity = row.querySelector?.(':scope > .entity[data-file-id]');
|
|
270
|
+
if (entity?.dataset?.fileType === type && readNativeTreePath(row, root) === projectPath) return row;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
function readNativeTreePath(row, root) {
|
|
275
|
+
const parts = [row.getAttribute('aria-label')];
|
|
276
|
+
let group = row.closest('[role="tree"]');
|
|
277
|
+
const seen = new Set();
|
|
278
|
+
while (group && group !== root) {
|
|
279
|
+
if (seen.has(group) || seen.size >= 64) return '';
|
|
280
|
+
seen.add(group);
|
|
281
|
+
const folder = group.previousElementSibling;
|
|
282
|
+
const entity = folder?.querySelector?.(':scope > .entity[data-file-id]');
|
|
283
|
+
if (folder?.getAttribute?.('role') !== 'treeitem' || entity?.dataset?.fileType !== 'folder') return '';
|
|
284
|
+
parts.unshift(folder.getAttribute('aria-label'));
|
|
285
|
+
group = group.parentElement?.closest?.('[role="tree"]');
|
|
286
|
+
}
|
|
287
|
+
if (group !== root || parts.some(part => !part || /[/\\]/.test(part))) return '';
|
|
288
|
+
return parts.join('/');
|
|
289
|
+
}
|
|
290
|
+
async function waitForPath(transfer, result, timeoutMs) {
|
|
291
|
+
const projectPath = transfer.path;
|
|
232
292
|
const deadline = Date.now() + timeoutMs;
|
|
293
|
+
let lastObserved = null;
|
|
233
294
|
while (Date.now() < deadline) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
295
|
+
assertTransferProject(transfer);
|
|
296
|
+
const node = findNativeTreeNode(projectPath, 'file')
|
|
297
|
+
|| treeOperations.findFileTreeNode(projectPath, { invalidateCache: true });
|
|
298
|
+
let observed = node || treeOperations.projectPathExists(projectPath)
|
|
299
|
+
? { path: projectPath, id: readTreeEntityId(node) } : null;
|
|
300
|
+
if (!observed?.id) {
|
|
301
|
+
try {
|
|
302
|
+
const list = await snapshotRouter?.buildProjectFileList({
|
|
303
|
+
force: true, maxAgeMs: 0, preferLightweight: true, allowZipFallback: false
|
|
304
|
+
});
|
|
305
|
+
const file = (list?.files || []).find(item => item.path === projectPath);
|
|
306
|
+
if (file) observed = file;
|
|
307
|
+
} catch (_error) { /* continue until the deadline */ }
|
|
308
|
+
}
|
|
309
|
+
assertTransferProject(transfer);
|
|
310
|
+
if (observed) {
|
|
311
|
+
lastObserved = observed;
|
|
312
|
+
const hashVerified = await verifyRemoteHash(observed, transfer).catch(() => false);
|
|
313
|
+
assertTransferProject(transfer);
|
|
314
|
+
// An existing path is not replacement evidence. Re-read identity and
|
|
315
|
+
// bytes while the native uploader and file-tree events settle.
|
|
316
|
+
if (!transfer.overwrite || hashVerified || result.confirmedByTransport === true) {
|
|
317
|
+
return { observed, hashVerified };
|
|
318
|
+
}
|
|
240
319
|
}
|
|
241
320
|
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
321
|
}
|
|
250
|
-
return
|
|
322
|
+
return { observed: lastObserved, hashVerified: false };
|
|
251
323
|
}
|
|
252
324
|
async function verifyRemoteHash(observed, transfer) {
|
|
253
325
|
const fileId = observed?.id || observed?._id || observed?.entityId || observed?.fileId || '';
|
|
@@ -339,5 +411,5 @@
|
|
|
339
411
|
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
340
412
|
}
|
|
341
413
|
function failure(code, reason) { return { ok: false, code, reason, changedDocument: false }; }
|
|
342
|
-
return { chooseOverleafFileInput, create, formatUploadHttpError, joinChunks, normalizePath };
|
|
414
|
+
return { assignFilesToInput, chooseOverleafFileInput, create, formatUploadHttpError, joinChunks, normalizePath };
|
|
343
415
|
});
|
|
@@ -73,6 +73,9 @@
|
|
|
73
73
|
|
|
74
74
|
running = true;
|
|
75
75
|
refreshActiveBaseline();
|
|
76
|
+
// The editor may mount after the page bridge. Keep the read-only
|
|
77
|
+
// observer alive until its first file identity is available.
|
|
78
|
+
if (statusName === 'unavailable' && lastErrorCode === 'missing_active_path') statusName = 'starting';
|
|
76
79
|
if (running) attachDocumentListeners();
|
|
77
80
|
return getStatus();
|
|
78
81
|
}
|
|
@@ -103,7 +106,7 @@
|
|
|
103
106
|
lastErrorCode,
|
|
104
107
|
channelCandidates: cloneChannelCandidates(channelCandidates)
|
|
105
108
|
};
|
|
106
|
-
if (statusName === 'unavailable' && statusReason) {
|
|
109
|
+
if ((statusName === 'unavailable' || statusName === 'starting') && statusReason) {
|
|
107
110
|
status.reason = statusReason;
|
|
108
111
|
}
|
|
109
112
|
return status;
|
|
@@ -288,5 +288,48 @@
|
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
|
|
291
|
+
async function confirmReviewWriteback({ params, result, readSnapshot, getProjectId, delay,
|
|
292
|
+
now = Date.now, timeoutMs = 45000 }) {
|
|
293
|
+
if (result?.ok !== true || result.skipped?.length) return result;
|
|
294
|
+
const expected = new Map((params.expectedFiles || [])
|
|
295
|
+
.filter(file => file?.path && typeof file.content === 'string').map(file => [file.path, file.content]));
|
|
296
|
+
for (const entry of result.applied || []) {
|
|
297
|
+
const path = entry.trackedChange?.path || entry.operation?.path;
|
|
298
|
+
if (expected.has(path) && typeof entry.result?.verifiedContent === 'string') {
|
|
299
|
+
expected.set(path, entry.result.verifiedContent);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const deadline = now() + Math.max(0, timeoutMs);
|
|
303
|
+
let attempts = 0;
|
|
304
|
+
if (expected.size && typeof readSnapshot === 'function') {
|
|
305
|
+
do {
|
|
306
|
+
if (!params.runProjectId || getProjectId() !== params.runProjectId) break;
|
|
307
|
+
try {
|
|
308
|
+
const snapshot = await readSnapshot({ force: true, maxAgeMs: 0,
|
|
309
|
+
zipTimeoutMs: Math.max(1, Math.min(8000, deadline - now())) });
|
|
310
|
+
attempts++;
|
|
311
|
+
if (getProjectId() !== params.runProjectId) break;
|
|
312
|
+
if (snapshot?.ok === true && Array.from(expected).every(([path, content]) =>
|
|
313
|
+
snapshot.files?.some(file => file.path === path && file.source === 'overleaf-zip'
|
|
314
|
+
&& file.content === content))) {
|
|
315
|
+
return { ...result, saveVerification: { ok: true, state: 'verified_saved',
|
|
316
|
+
source: 'overleaf-zip', attempts } };
|
|
317
|
+
}
|
|
318
|
+
} catch (_error) { /* A transient download failure grants no save proof. */ }
|
|
319
|
+
if (now() >= deadline) break;
|
|
320
|
+
await delay(Math.min(300, deadline - now()));
|
|
321
|
+
} while (now() < deadline);
|
|
322
|
+
}
|
|
323
|
+
const reason = 'Undo changed the editor, but its saved server content could not be confirmed. Keep this page open and check Overleaf before retrying.';
|
|
324
|
+
return { ...result, ok: false, saveVerification: { ok: false, state: 'unknown_timeout', attempts },
|
|
325
|
+
skipped: [...(result.skipped || []), { trackedChange: { path: expected.keys().next().value || '' },
|
|
326
|
+
result: { ok: false, code: 'undo_not_verified', reason, failure: {
|
|
327
|
+
code: 'undo_not_verified', stage: 'verification', severity: 'needs_review',
|
|
328
|
+
terminalState: 'needs_review', changedDocument: true, retryable: true, userMessage: reason
|
|
329
|
+
} } }] };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const api = { create, confirmReviewWriteback };
|
|
333
|
+
root.CodexOverleafSaveState = api;
|
|
334
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
292
335
|
})(typeof window !== 'undefined' ? window : globalThis);
|