codex-overleaf-link 2.6.0 → 2.6.5
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 +9 -9
- package/README.zh-CN.md +9 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +70 -0
- package/extension/src/content/composerAttachments.js +35 -8
- package/extension/src/content/contentRuntime.js +200 -51
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +129 -42
- package/extension/src/content/generated/content.bundle.js +1283 -331
- package/extension/src/content/generated/content.bundle.meta.json +3 -3
- package/extension/src/content/localSkillsPanel.js +4 -2
- package/extension/src/content/markdownText.js +11 -9
- package/extension/src/content/modelPicker.js +28 -19
- package/extension/src/content/nativeCompatibilityController.js +23 -9
- package/extension/src/content/pageBridgeClient.js +5 -5
- package/extension/src/content/panelMaintenance.js +4 -0
- package/extension/src/content/panelRenderer.js +4 -1
- package/extension/src/content/providerSettingsDialog.js +26 -18
- package/extension/src/content/recentProjects.js +26 -16
- package/extension/src/content/runQueueScheduler.js +19 -9
- package/extension/src/content/runResultActions.js +51 -4
- package/extension/src/content/runReviewController.js +9 -3
- package/extension/src/content/scopedPersistencePanelState.js +34 -25
- package/extension/src/content/selectionContextView.js +30 -3
- package/extension/src/content/sharedSessionViewSync.js +9 -1
- package/extension/src/content/writebackOrchestrator.js +266 -50
- package/extension/src/content/writingStyleSettings.js +70 -17
- package/extension/src/page/overleafEditor.js +12 -3
- package/extension/src/page/overleafProjectSnapshot.js +56 -3
- package/extension/src/page/snapshotRouter.js +75 -66
- package/extension/src/page/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +152 -40
- package/extension/src/pageBridge.js +17 -7
- package/extension/src/shared/agentTranscript.js +23 -2
- package/extension/src/shared/auditRecords.js +1 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/composerAttachmentDraft.js +22 -7
- package/extension/src/shared/failureReasons.js +15 -0
- package/extension/src/shared/i18n.js +16 -0
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/projectFiles.js +2 -1
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
- package/extension/src/shared/staleGuard.js +58 -0
- package/extension/src/shared/storageMigration.js +104 -1
- package/extension/styles/panel.css +191 -63
- package/native-host/src/codexSessionRunner.js +1 -0
- package/native-host/src/debugLog.js +27 -3
- package/native-host/src/index.js +42 -11
- package/native-host/src/localSkills.js +1 -1
- package/native-host/src/mirrorWorkspace.js +47 -3
- package/native-host/src/nativeEnvironment.js +27 -5
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
- package/native-host/src/taskRunnerRuntime.js +11 -3
- package/native-host/src/updateManager.js +24 -6
- package/native-host/src/writingStyleRuntime.js +1 -1
- package/package.json +1 -1
|
@@ -347,7 +347,20 @@
|
|
|
347
347
|
const input = node('input'); input.type = 'search'; input.value = query; input.dataset.styleFocus = 'project-search';
|
|
348
348
|
input.placeholder = tx('Search all Overleaf projects', '搜索全部 Overleaf 项目');
|
|
349
349
|
input.setAttribute('aria-label', input.placeholder);
|
|
350
|
-
|
|
350
|
+
let composing = false;
|
|
351
|
+
const updateResults = () => {
|
|
352
|
+
query = input.value;
|
|
353
|
+
const results = input.closest('.ws2-picker')?.querySelector('.ws2-projects');
|
|
354
|
+
if (results) renderProjectResults(results);
|
|
355
|
+
};
|
|
356
|
+
input.addEventListener('compositionstart', () => { composing = true; });
|
|
357
|
+
input.addEventListener('compositionend', () => {
|
|
358
|
+
composing = false;
|
|
359
|
+
updateResults();
|
|
360
|
+
});
|
|
361
|
+
input.addEventListener('input', event => {
|
|
362
|
+
if (!composing && !event.isComposing) updateResults();
|
|
363
|
+
});
|
|
351
364
|
search.append(input); panel.append(search);
|
|
352
365
|
const filters = node('div', 'ws2-filters');
|
|
353
366
|
for (const [value, label] of [['all', tx('All', '全部')], ['active', tx('Active', '常规')], ['archived', tx('Archived', '已归档')]]) {
|
|
@@ -358,6 +371,16 @@
|
|
|
358
371
|
panel.append(filters);
|
|
359
372
|
const list = node('div', 'ws2-projects');
|
|
360
373
|
list.setAttribute('role', 'group'); list.setAttribute('aria-label', tx('Overleaf projects', 'Overleaf 项目'));
|
|
374
|
+
renderProjectResults(list);
|
|
375
|
+
panel.append(list);
|
|
376
|
+
const footer = node('div', 'ws2-picker-footer');
|
|
377
|
+
footer.append(node('span', '', tx(sources.length + ' of 3 references selected', '已选 ' + sources.length + ' / 3 份资料')),
|
|
378
|
+
button(tx('Done', '完成'), () => { pickerOpen = false; render(); }, 'ws2-button ws2-button-primary', 'picker-done'));
|
|
379
|
+
panel.append(footer); return panel;
|
|
380
|
+
}
|
|
381
|
+
function renderProjectResults(list) {
|
|
382
|
+
const oldScroll = list.scrollTop;
|
|
383
|
+
list.replaceChildren();
|
|
361
384
|
if (projectLoading) {
|
|
362
385
|
const loading = node('div', 'ws2-picker-state');
|
|
363
386
|
loading.append(node('span', 'ws2-pulse'), node('span', '', tx('Loading project directory…', '正在加载项目目录…')));
|
|
@@ -395,11 +418,7 @@
|
|
|
395
418
|
if (!matches.length) list.append(node('div', 'ws2-picker-state',
|
|
396
419
|
query ? tx('No projects match this search.', '没有匹配的项目。') : tx('No projects in this group.', '此分组暂无项目。')));
|
|
397
420
|
}
|
|
398
|
-
|
|
399
|
-
const footer = node('div', 'ws2-picker-footer');
|
|
400
|
-
footer.append(node('span', '', tx(sources.length + ' of 3 references selected', '已选 ' + sources.length + ' / 3 份资料')),
|
|
401
|
-
button(tx('Done', '完成'), () => { pickerOpen = false; render(); }, 'ws2-button ws2-button-primary', 'picker-done'));
|
|
402
|
-
panel.append(footer); return panel;
|
|
421
|
+
list.scrollTop = oldScroll;
|
|
403
422
|
}
|
|
404
423
|
function renderSources() {
|
|
405
424
|
const section = node('section', 'ws2-library');
|
|
@@ -480,6 +499,38 @@
|
|
|
480
499
|
}
|
|
481
500
|
return section;
|
|
482
501
|
}
|
|
502
|
+
function replaceAround(parent, fresh, retained, replacement) {
|
|
503
|
+
for (const child of Array.from(parent.childNodes)) {
|
|
504
|
+
if (child !== retained) child.remove();
|
|
505
|
+
}
|
|
506
|
+
let before = true;
|
|
507
|
+
for (const child of Array.from(fresh.childNodes)) {
|
|
508
|
+
if (child === replacement) before = false;
|
|
509
|
+
else parent.insertBefore(child, before ? retained : null);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function renderKeepingProjectSearch(content) {
|
|
513
|
+
const library = host.querySelector('.ws2-library');
|
|
514
|
+
const picker = library?.querySelector('.ws2-picker');
|
|
515
|
+
const search = picker?.querySelector('.ws2-search');
|
|
516
|
+
const nextLibrary = content.querySelector('.ws2-library');
|
|
517
|
+
const nextPicker = nextLibrary?.querySelector('.ws2-picker');
|
|
518
|
+
if (!search || !nextPicker) {
|
|
519
|
+
host.replaceChildren(content);
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
// Keep every ancestor connected: detaching and refocusing an input
|
|
523
|
+
// cannot restore its IME composition when an async catalog read settles.
|
|
524
|
+
const input = search.querySelector('input');
|
|
525
|
+
const nextSearch = nextPicker.querySelector('.ws2-search');
|
|
526
|
+
input.placeholder = nextSearch.querySelector('input').placeholder;
|
|
527
|
+
input.setAttribute('aria-label', input.placeholder);
|
|
528
|
+
picker.setAttribute('aria-label', nextPicker.getAttribute('aria-label'));
|
|
529
|
+
replaceAround(picker, nextPicker, search, nextSearch);
|
|
530
|
+
replaceAround(library, nextLibrary, picker, nextPicker);
|
|
531
|
+
replaceAround(host, content, library, nextLibrary);
|
|
532
|
+
return document.activeElement === input;
|
|
533
|
+
}
|
|
483
534
|
function render() {
|
|
484
535
|
if (!host?.isConnected) return;
|
|
485
536
|
alignScope();
|
|
@@ -487,7 +538,8 @@
|
|
|
487
538
|
const oldScroll = host.querySelector('.ws2-projects')?.scrollTop || 0;
|
|
488
539
|
const heading = host.closest('[data-set-group]')?.querySelector('[data-writing-style-heading]');
|
|
489
540
|
if (heading) heading.textContent = tx('Writing style', '写作风格');
|
|
490
|
-
host.classList.add('ws2');
|
|
541
|
+
host.classList.add('ws2');
|
|
542
|
+
const content = document.createDocumentFragment();
|
|
491
543
|
const top = node('div', 'ws2-heading');
|
|
492
544
|
const copy = node('div', 'ws2-heading-copy');
|
|
493
545
|
const title = node('label', 'ws2-title', tx('Write in My Style', '沿用写作风格'));
|
|
@@ -501,7 +553,7 @@
|
|
|
501
553
|
toggleInput.dataset.styleFocus = 'enable-style'; toggleInput.checked = desiredEnabled;
|
|
502
554
|
toggleInput.disabled = !scope || !meta || toggleBusy || (working() && !meta?.ready);
|
|
503
555
|
toggleInput.addEventListener('change', () => { void toggle(toggleInput.checked); });
|
|
504
|
-
top.append(copy, toggleInput);
|
|
556
|
+
top.append(copy, toggleInput); content.append(top);
|
|
505
557
|
if (meta?.ready) {
|
|
506
558
|
const summary = node('div', 'ws2-profile');
|
|
507
559
|
const mark = node('span', 'ws2-profile-icon'); mark.append(icon('check'));
|
|
@@ -518,16 +570,16 @@
|
|
|
518
570
|
const view = button(preview ? tx('Hide preview', '收起预览') : tx('Preview style', '预览风格'),
|
|
519
571
|
() => { void loadPreview(); }, 'ws2-button ws2-button-quiet', 'preview-style');
|
|
520
572
|
view.disabled = previewBusy;
|
|
521
|
-
summary.append(mark, info, view);
|
|
573
|
+
summary.append(mark, info, view); content.append(summary);
|
|
522
574
|
}
|
|
523
575
|
const editing = expanded || working();
|
|
524
576
|
const manage = button(editing ? tx('Hide references', '收起参考资料')
|
|
525
577
|
: meta?.ready ? tx('Manage references', '管理参考资料') : tx('Choose references', '选择参考资料'),
|
|
526
578
|
() => { expanded = !expanded; pickerOpen = false; render(); }, 'ws2-manage', 'manage-references');
|
|
527
579
|
manage.setAttribute('aria-expanded', String(editing));
|
|
528
|
-
manage.disabled = working(); manage.append(icon('arrow'));
|
|
580
|
+
manage.disabled = working(); manage.append(icon('arrow')); content.append(manage);
|
|
529
581
|
if (editing) {
|
|
530
|
-
|
|
582
|
+
content.append(renderSources());
|
|
531
583
|
if (working()) {
|
|
532
584
|
const progress = node('div', 'ws2-progress'); progress.setAttribute('role', 'status');
|
|
533
585
|
const phase = activeGeneration?.phase || 'learning';
|
|
@@ -545,7 +597,7 @@
|
|
|
545
597
|
}
|
|
546
598
|
progress.append(steps);
|
|
547
599
|
const stop = button(tx('Cancel', '取消'), () => { void cancel(); }, 'ws2-button', 'cancel-generation');
|
|
548
|
-
stop.disabled = cancelling; progress.append(stop);
|
|
600
|
+
stop.disabled = cancelling; progress.append(stop); content.append(progress);
|
|
549
601
|
} else {
|
|
550
602
|
const footer = node('div', 'ws2-actions');
|
|
551
603
|
const help = node('div', 'ws2-action-copy');
|
|
@@ -559,8 +611,8 @@
|
|
|
559
611
|
const generateButton = button(meta?.ready ? tx('Update style', '更新风格') : tx('Generate style', '生成风格'),
|
|
560
612
|
() => { void generate(); }, 'ws2-button ws2-button-primary', 'generate-style');
|
|
561
613
|
generateButton.disabled = !scope || !meta || !sources.length || deps.isRunning();
|
|
562
|
-
footer.append(generateButton);
|
|
563
|
-
if (deps.isRunning())
|
|
614
|
+
footer.append(generateButton); content.append(footer);
|
|
615
|
+
if (deps.isRunning()) content.append(node('p', 'ws2-caption', tx('Available after the current task finishes.', '当前任务结束后可生成。')));
|
|
564
616
|
}
|
|
565
617
|
}
|
|
566
618
|
if (message || !meta) {
|
|
@@ -570,12 +622,13 @@
|
|
|
570
622
|
if (!meta && message) status.append(button(tx('Retry', '重试'), () => {
|
|
571
623
|
message = ''; messageTone = ''; render(); void ensureLoaded(true).catch(() => {});
|
|
572
624
|
}, 'ws2-button', 'retry-settings'));
|
|
573
|
-
|
|
625
|
+
content.append(status);
|
|
574
626
|
}
|
|
575
|
-
if (preview)
|
|
627
|
+
if (preview) content.append(renderPreview());
|
|
628
|
+
const searchKeptFocus = renderKeepingProjectSearch(content);
|
|
576
629
|
const scroll = host.querySelector('.ws2-projects');
|
|
577
630
|
if (scroll) scroll.scrollTop = oldScroll;
|
|
578
|
-
restoreFocus(focus);
|
|
631
|
+
if (!searchKeptFocus) restoreFocus(focus);
|
|
579
632
|
}
|
|
580
633
|
function mount(panel) {
|
|
581
634
|
const next = panel?.querySelector('[data-writing-style-settings]');
|
|
@@ -68,8 +68,15 @@
|
|
|
68
68
|
return '';
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function
|
|
71
|
+
function captureMatches(view, capture) {
|
|
72
|
+
return !capture || (view === capture.view && view?.state === capture.state && view.state.doc === capture.doc
|
|
73
|
+
&& view.state.doc.toString() === capture.text);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function replaceActiveEditorText(text, options = {}) {
|
|
72
77
|
const editorView = deps.getCodeMirrorEditorView?.();
|
|
78
|
+
if (!captureMatches(editorView, options.expectedCapture))
|
|
79
|
+
return { ok: false, code: 'document_identity_mismatch', changedDocument: false };
|
|
73
80
|
if (editorView) {
|
|
74
81
|
const from = 0;
|
|
75
82
|
const to = deps.getCodeMirrorDocLength(editorView);
|
|
@@ -120,13 +127,15 @@
|
|
|
120
127
|
};
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
function replaceActiveEditorPatches(patches, nextContent) {
|
|
130
|
+
function replaceActiveEditorPatches(patches, nextContent, options = {}) {
|
|
124
131
|
const normalized = deps.normalizeTextPatches(patches, readActiveEditorText().length);
|
|
125
132
|
if (!normalized.ok) {
|
|
126
133
|
return normalized;
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
const editorView = deps.getCodeMirrorEditorView?.();
|
|
137
|
+
if (!captureMatches(editorView, options.expectedCapture))
|
|
138
|
+
return { ok: false, code: 'document_identity_mismatch', changedDocument: false };
|
|
130
139
|
if (editorView) {
|
|
131
140
|
editorView.dispatch({
|
|
132
141
|
changes: normalized.patches.map(patch => ({
|
|
@@ -159,7 +168,7 @@
|
|
|
159
168
|
};
|
|
160
169
|
}
|
|
161
170
|
|
|
162
|
-
const result = replaceActiveEditorText(nextContent);
|
|
171
|
+
const result = replaceActiveEditorText(nextContent, options);
|
|
163
172
|
return result.ok
|
|
164
173
|
? {
|
|
165
174
|
...result,
|
|
@@ -16,27 +16,70 @@
|
|
|
16
16
|
const snapshotCache = createKeyedCache();
|
|
17
17
|
const fileListCache = createKeyedCache();
|
|
18
18
|
|
|
19
|
+
function editorCaptureKey() {
|
|
20
|
+
const capture = deps.captureEditorDocument?.({ path: deps.getActiveFilePath?.(), projectId: deps.getProjectId?.() });
|
|
21
|
+
return capture?.ok ? capture : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sameEditorCapture(left, right) {
|
|
25
|
+
return Boolean(left?.ok && right?.ok) && left.view === right.view && left.state === right.state && left.doc === right.doc
|
|
26
|
+
&& left.text === right.text && ['path', 'projectId', 'documentId', 'targetDocumentId']
|
|
27
|
+
.every(key => left.documentIdentity[key] === right.documentIdentity[key]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasEditorOverlay(snapshot) {
|
|
31
|
+
// Navigation reads carry the open method as their source. Only ZIP and
|
|
32
|
+
// document fetches are independent of the mounted editor.
|
|
33
|
+
return snapshot?.files?.some(file => typeof file.content === 'string'
|
|
34
|
+
&& !/^overleaf-(?:zip|doc-fetch)/.test(file.source || ''));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasNavigationReads(snapshot) {
|
|
38
|
+
return snapshot?.files?.some(file => typeof file.content === 'string' && file.source !== 'active-editor'
|
|
39
|
+
&& !/^overleaf-(?:zip|doc-fetch)/.test(file.source || ''));
|
|
40
|
+
}
|
|
41
|
+
|
|
19
42
|
async function getProjectSnapshot(params = {}) {
|
|
20
43
|
const cacheKey = getSnapshotCacheKey(params);
|
|
21
44
|
const maxAgeMs = normalizeSnapshotMaxAge(params.maxAgeMs);
|
|
22
45
|
const now = Date.now();
|
|
23
46
|
const cacheEntry = snapshotCache.entries.get(cacheKey);
|
|
47
|
+
const captureKey = editorCaptureKey();
|
|
48
|
+
const projectId = deps.getProjectId?.();
|
|
24
49
|
|
|
25
|
-
|
|
26
|
-
return
|
|
50
|
+
function currentSnapshot(snapshot) {
|
|
51
|
+
if (deps.getProjectId?.() !== projectId) return withoutEditorFiles(snapshot, true);
|
|
52
|
+
if (!hasEditorOverlay(snapshot) || params.allowEditorNavigation === true || hasNavigationReads(snapshot)) return snapshot;
|
|
53
|
+
const captures = deps.getSnapshotEditorCaptures?.(snapshot) || [];
|
|
54
|
+
if (captures.length === 1 && deps.isCaptureCurrent?.(captures[0])) return snapshot;
|
|
55
|
+
// Fallback captures can be returned once, but never reused from cache.
|
|
56
|
+
if (!snapshot.files.some(file => file.documentIdentity !== undefined)) return snapshot;
|
|
57
|
+
return withoutEditorFiles(snapshot);
|
|
27
58
|
}
|
|
28
59
|
|
|
29
|
-
if (!params.force && cacheEntry?.
|
|
60
|
+
if (!params.force && cacheEntry?.pending && (params.allowEditorNavigation === false || params.zipOnly || params.serverOnly)
|
|
61
|
+
&& (params.serverOnly === true || sameEditorCapture(cacheEntry.pendingCaptureKey, captureKey))) {
|
|
62
|
+
const snapshot = await cacheEntry.pending;
|
|
63
|
+
return currentSnapshot(snapshot);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!params.force && cacheEntry?.value && now - cacheEntry.capturedAt <= maxAgeMs
|
|
67
|
+
&& (!hasEditorOverlay(cacheEntry.value) || (params.allowEditorNavigation !== true
|
|
68
|
+
&& sameEditorCapture(cacheEntry.captureKey, captureKey)))) {
|
|
30
69
|
return withSnapshotCacheMetadata(cacheEntry.value, 'memory', cacheEntry.capturedAt);
|
|
31
70
|
}
|
|
32
71
|
|
|
33
72
|
const pending = deps.buildProjectSnapshot(params)
|
|
34
73
|
.then(snapshot => {
|
|
74
|
+
const captures = deps.getSnapshotEditorCaptures?.(snapshot) || [];
|
|
75
|
+
const finalCapture = captures.length === 1 ? captures[0] : null;
|
|
76
|
+
snapshot = currentSnapshot(snapshot);
|
|
35
77
|
const capturedAt = Date.now();
|
|
36
78
|
const currentEntry = snapshotCache.entries.get(cacheKey);
|
|
37
79
|
if (currentEntry?.pending === pending) {
|
|
38
80
|
snapshotCache.entries.set(cacheKey, {
|
|
39
81
|
value: snapshot,
|
|
82
|
+
captureKey: params.allowEditorNavigation === true || hasNavigationReads(snapshot) ? null : finalCapture,
|
|
40
83
|
capturedAt,
|
|
41
84
|
pending: null
|
|
42
85
|
});
|
|
@@ -52,12 +95,22 @@
|
|
|
52
95
|
|
|
53
96
|
snapshotCache.entries.set(cacheKey, {
|
|
54
97
|
value: cacheEntry?.value || null,
|
|
98
|
+
captureKey: cacheEntry?.captureKey,
|
|
99
|
+
pendingCaptureKey: captureKey,
|
|
55
100
|
capturedAt: cacheEntry?.capturedAt || 0,
|
|
56
101
|
pending
|
|
57
102
|
});
|
|
58
103
|
return pending;
|
|
59
104
|
}
|
|
60
105
|
|
|
106
|
+
function withoutEditorFiles(snapshot, projectChanged = false) {
|
|
107
|
+
const files = projectChanged ? [] : (snapshot?.files || []).filter(file => /^overleaf-(?:zip|doc-fetch)/.test(file.source || ''));
|
|
108
|
+
const removed = (snapshot?.files || []).filter(file => !files.includes(file));
|
|
109
|
+
return { ...snapshot, files, capabilities: { ...snapshot?.capabilities, fullProjectSnapshot: false,
|
|
110
|
+
requestedPathsComplete: false, skipped: [...(snapshot?.capabilities?.skipped || []),
|
|
111
|
+
...removed.map(file => ({ path: file.path, reason: 'document_identity_mismatch' }))] } };
|
|
112
|
+
}
|
|
113
|
+
|
|
61
114
|
async function getProjectFileList(params = {}) {
|
|
62
115
|
const cacheKey = getProjectFileListCacheKey(params);
|
|
63
116
|
const maxAgeMs = normalizeFileListMaxAge(params.maxAgeMs);
|
|
@@ -17,12 +17,22 @@
|
|
|
17
17
|
const normalizeSafeProjectPath = typeof deps.normalizePath === 'function'
|
|
18
18
|
? deps.normalizePath
|
|
19
19
|
: fallbackNormalizeSafeProjectPath;
|
|
20
|
-
const readActiveEditorText = typeof deps.readActiveEditorText === 'function'
|
|
21
|
-
? deps.readActiveEditorText
|
|
22
|
-
: () => '';
|
|
23
20
|
let invalidatedAt = 0;
|
|
24
21
|
let preferredZipEndpoint = '';
|
|
25
22
|
const writebackReads = new Set();
|
|
23
|
+
const editorCaptures = new WeakMap();
|
|
24
|
+
|
|
25
|
+
function editorFile(path, ready, source) {
|
|
26
|
+
if (ready.documentIdentity && ready.capture) editorCaptures.set(ready.documentIdentity, ready.capture);
|
|
27
|
+
return { path, content: ready.text, source, kind: 'text',
|
|
28
|
+
...(ready.documentIdentity !== undefined ? { documentIdentity: ready.documentIdentity } : {}) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getSnapshotEditorCaptures(snapshot) {
|
|
32
|
+
return (snapshot?.files || []).filter(file => typeof file.content === 'string'
|
|
33
|
+
&& !/^overleaf-(?:zip|doc-fetch)/.test(file.source || ''))
|
|
34
|
+
.map(file => editorCaptures.get(file.documentIdentity));
|
|
35
|
+
}
|
|
26
36
|
|
|
27
37
|
function getProjectId() {
|
|
28
38
|
return treeOperations.getProjectId?.() || null;
|
|
@@ -214,6 +224,18 @@
|
|
|
214
224
|
}
|
|
215
225
|
|
|
216
226
|
async function buildProjectSnapshot(params = {}) {
|
|
227
|
+
const projectId = params.projectId || getProjectId();
|
|
228
|
+
if (!projectId || projectId !== getProjectId()) return projectChangedSnapshot(projectId);
|
|
229
|
+
const snapshot = await buildProjectSnapshotForProject({ ...params, projectId });
|
|
230
|
+
return getProjectId() === projectId ? snapshot : projectChangedSnapshot(projectId);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function projectChangedSnapshot(projectId) {
|
|
234
|
+
return { ok: false, id: projectId, files: [], capabilities: { fullProjectSnapshot: false,
|
|
235
|
+
skipped: [{ path: '', reason: 'document_identity_mismatch' }] } };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function buildProjectSnapshotForProject(params = {}) {
|
|
217
239
|
if (params.zipOnly || params.serverOnly === true) {
|
|
218
240
|
return buildZipOnlyProjectSnapshot(params);
|
|
219
241
|
}
|
|
@@ -242,9 +264,18 @@
|
|
|
242
264
|
|
|
243
265
|
const zipSnapshot = await fetchProjectZipSnapshot(params);
|
|
244
266
|
if (zipSnapshot.ok && zipSnapshot.files.length) {
|
|
267
|
+
const skipped = [...(zipSnapshot.skipped || [])];
|
|
268
|
+
const overlays = [...(lightweightSnapshot?.files || [])];
|
|
269
|
+
for (let index = 0; index < overlays.length; index++) {
|
|
270
|
+
if (overlays[index].source !== 'active-editor') continue;
|
|
271
|
+
const captured = await readActiveEditorOverlay(overlays[index].path, zipSnapshot.files, { projectId: params.projectId });
|
|
272
|
+
skipped.push(...captured.skipped);
|
|
273
|
+
overlays.splice(index, 1, ...captured.files);
|
|
274
|
+
if (!captured.files.length) index--;
|
|
275
|
+
}
|
|
245
276
|
const mergedFiles = mergeSnapshotFiles(
|
|
246
277
|
zipSnapshot.files,
|
|
247
|
-
filterZipSnapshotOverlayFiles(zipSnapshot.files,
|
|
278
|
+
filterZipSnapshotOverlayFiles(zipSnapshot.files, overlays, skipped)
|
|
248
279
|
);
|
|
249
280
|
const files = filterRequestedSnapshotFiles(mergedFiles, params, activePath);
|
|
250
281
|
const requestedPathsComplete = params.restrictToRequestedPathsOnly === true
|
|
@@ -261,7 +292,7 @@
|
|
|
261
292
|
contentComplete: !(zipSnapshot.skipped || []).length,
|
|
262
293
|
requestedPathsComplete,
|
|
263
294
|
method: 'overleaf-zip',
|
|
264
|
-
skipped
|
|
295
|
+
skipped,
|
|
265
296
|
diagnostics: {
|
|
266
297
|
zipEndpoint: zipSnapshot.endpoint,
|
|
267
298
|
zipTiming: zipSnapshot.diagnostics,
|
|
@@ -309,15 +340,16 @@
|
|
|
309
340
|
snapshotActivePath,
|
|
310
341
|
...(Array.isArray(params.focusFiles) ? params.focusFiles : [])
|
|
311
342
|
];
|
|
343
|
+
const enumeratedPaths = params.restrictToRequestedPathsOnly === true
|
|
344
|
+
? []
|
|
345
|
+
: collectProjectTextPaths(snapshotActivePath);
|
|
312
346
|
const projectPaths = window.CodexOverleafProjectFiles.collectUniqueTextPaths(
|
|
313
|
-
|
|
314
|
-
? requestedPaths
|
|
315
|
-
: [
|
|
316
|
-
...requestedPaths,
|
|
317
|
-
...collectProjectTextPaths(snapshotActivePath)
|
|
318
|
-
],
|
|
347
|
+
[...requestedPaths, ...enumeratedPaths],
|
|
319
348
|
80
|
|
320
349
|
);
|
|
350
|
+
const enumerationTruncated = projectPaths.truncated === true
|
|
351
|
+
|| enumeratedPaths.truncated === true
|
|
352
|
+
|| internalDocRecords.truncated === true;
|
|
321
353
|
const files = [];
|
|
322
354
|
const skipped = [...(zipSnapshot?.skipped || [])];
|
|
323
355
|
const ambiguousPaths = new Set(skipped.filter(file => file.reason === 'zip_path_duplicate').map(file => file.path));
|
|
@@ -332,7 +364,7 @@
|
|
|
332
364
|
for (const filePath of projectPaths) {
|
|
333
365
|
if (ambiguousPaths.has(filePath)) continue;
|
|
334
366
|
if (filePath === snapshotActivePath) {
|
|
335
|
-
const ready = await waitForActiveEditorText(filePath, 5000);
|
|
367
|
+
const ready = await waitForActiveEditorText(filePath, 5000, { allowEmpty: true, projectId: params.projectId });
|
|
336
368
|
if (!ready.ok) {
|
|
337
369
|
skipped.push({
|
|
338
370
|
path: filePath,
|
|
@@ -340,18 +372,14 @@
|
|
|
340
372
|
});
|
|
341
373
|
continue;
|
|
342
374
|
}
|
|
343
|
-
files.push(
|
|
344
|
-
path: filePath,
|
|
345
|
-
content: ready.text,
|
|
346
|
-
source: 'active-editor'
|
|
347
|
-
});
|
|
375
|
+
files.push(editorFile(filePath, ready, 'active-editor'));
|
|
348
376
|
previousSignature = contentSignature(ready.text);
|
|
349
377
|
continue;
|
|
350
378
|
}
|
|
351
379
|
|
|
352
380
|
const docRecord = docRecordByPath.get(filePath);
|
|
353
381
|
if (docRecord) {
|
|
354
|
-
const fetched = await fetchOverleafDocContent(docRecord);
|
|
382
|
+
const fetched = await fetchOverleafDocContent(docRecord, params.projectId);
|
|
355
383
|
if (fetched.ok) {
|
|
356
384
|
files.push({
|
|
357
385
|
path: filePath,
|
|
@@ -365,6 +393,7 @@
|
|
|
365
393
|
path: filePath,
|
|
366
394
|
reason: fetched.reason
|
|
367
395
|
});
|
|
396
|
+
if (fetched.reason === 'document_identity_mismatch') continue;
|
|
368
397
|
}
|
|
369
398
|
|
|
370
399
|
if (!canOpenInactiveFileForSnapshot(filePath, params, lightweightOnly, docRecord)) {
|
|
@@ -386,7 +415,9 @@
|
|
|
386
415
|
|
|
387
416
|
await waitForActiveFile(filePath, 5000);
|
|
388
417
|
const ready = await waitForActiveEditorText(filePath, 7000, {
|
|
389
|
-
notSignature: previousSignature
|
|
418
|
+
notSignature: previousSignature,
|
|
419
|
+
projectId: params.projectId,
|
|
420
|
+
allowEmpty: true
|
|
390
421
|
});
|
|
391
422
|
if (!ready.ok) {
|
|
392
423
|
skipped.push({
|
|
@@ -395,11 +426,7 @@
|
|
|
395
426
|
});
|
|
396
427
|
continue;
|
|
397
428
|
}
|
|
398
|
-
files.push(
|
|
399
|
-
path: filePath,
|
|
400
|
-
content: ready.text,
|
|
401
|
-
source: opened.method || 'file-tree-open'
|
|
402
|
-
});
|
|
429
|
+
files.push(editorFile(filePath, ready, opened.method || 'file-tree-open'));
|
|
403
430
|
previousSignature = contentSignature(ready.text);
|
|
404
431
|
}
|
|
405
432
|
|
|
@@ -409,14 +436,6 @@
|
|
|
409
436
|
await waitForActiveEditorText(activePath, 5000);
|
|
410
437
|
}
|
|
411
438
|
|
|
412
|
-
if (snapshotActivePath && !ambiguousPaths.has(snapshotActivePath) && !files.length && (!lightweightOnly || params.allowZipFallback === false)) {
|
|
413
|
-
files.push({
|
|
414
|
-
path: snapshotActivePath,
|
|
415
|
-
content: readActiveEditorText(),
|
|
416
|
-
source: 'active-editor'
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
439
|
const requestedPathsComplete = isCompleteProjectSnapshot(files, projectPaths);
|
|
421
440
|
return {
|
|
422
441
|
id: getProjectId(),
|
|
@@ -424,7 +443,7 @@
|
|
|
424
443
|
activePath: snapshotActivePath,
|
|
425
444
|
files,
|
|
426
445
|
capabilities: {
|
|
427
|
-
fullProjectSnapshot: params.restrictToRequestedPathsOnly !== true && requestedPathsComplete && zipSnapshot?.pathIdentityComplete !== false,
|
|
446
|
+
fullProjectSnapshot: params.restrictToRequestedPathsOnly !== true && !enumerationTruncated && requestedPathsComplete && zipSnapshot?.pathIdentityComplete !== false,
|
|
428
447
|
pathIdentityComplete: zipSnapshot?.pathIdentityComplete,
|
|
429
448
|
requestedPathsComplete,
|
|
430
449
|
method: resolvePageSnapshotMethod(files, lightweightOnly),
|
|
@@ -444,11 +463,11 @@
|
|
|
444
463
|
|
|
445
464
|
async function buildZipOnlyProjectSnapshot(params = {}) {
|
|
446
465
|
const activePath = getPassiveActiveFilePath();
|
|
447
|
-
// Capture read evidence together with the path/text before awaiting ZIP.
|
|
448
|
-
// A later editor identity cannot prove that an earlier empty read succeeded.
|
|
449
|
-
const activeOverlay = params.serverOnly === true ? [] : readActiveEditorOverlay(activePath);
|
|
450
|
-
|
|
451
466
|
const zipSnapshot = await fetchProjectZipSnapshot(params);
|
|
467
|
+
const captured = params.serverOnly === true ? { files: [], skipped: [] }
|
|
468
|
+
: await readActiveEditorOverlay(activePath, zipSnapshot.ok ? zipSnapshot.files : null, { passive: true, projectId: params.projectId });
|
|
469
|
+
const activeOverlay = captured.files;
|
|
470
|
+
const skipped = [...(zipSnapshot.skipped || []), ...captured.skipped];
|
|
452
471
|
if (zipSnapshot.ok && zipSnapshot.files.length) {
|
|
453
472
|
const files = filterRequestedSnapshotFiles(
|
|
454
473
|
mergeSnapshotFiles(zipSnapshot.files, filterZipSnapshotOverlayFiles(zipSnapshot.files, activeOverlay, zipSnapshot.skipped)),
|
|
@@ -469,7 +488,7 @@
|
|
|
469
488
|
contentComplete: !(zipSnapshot.skipped || []).length,
|
|
470
489
|
requestedPathsComplete,
|
|
471
490
|
method: 'overleaf-zip',
|
|
472
|
-
skipped
|
|
491
|
+
skipped,
|
|
473
492
|
diagnostics: {
|
|
474
493
|
zipEndpoint: zipSnapshot.endpoint,
|
|
475
494
|
zipTiming: zipSnapshot.diagnostics,
|
|
@@ -494,7 +513,7 @@
|
|
|
494
513
|
? 'active-editor-zip-only-fallback'
|
|
495
514
|
: 'zip-only-fallback-empty',
|
|
496
515
|
contentComplete: false,
|
|
497
|
-
skipped: [...
|
|
516
|
+
skipped: [...skipped, {
|
|
498
517
|
path: 'project.zip',
|
|
499
518
|
reason: zipSnapshot.reason || 'ZIP file identities were incomplete'
|
|
500
519
|
}],
|
|
@@ -531,32 +550,15 @@
|
|
|
531
550
|
return (files || []).filter(file => requestedSet.has(file?.path));
|
|
532
551
|
}
|
|
533
552
|
|
|
534
|
-
function readActiveEditorOverlay(activePath) {
|
|
535
|
-
if (!activePath) return [];
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
let observed;
|
|
544
|
-
if (identity?.type === 'codemirror-view' && typeof identity.doc?.toString === 'function') {
|
|
545
|
-
observed = identity.doc.toString();
|
|
546
|
-
} else if (identity?.type === 'textarea') {
|
|
547
|
-
observed = identity.node?.value;
|
|
548
|
-
} else if (identity?.type === 'contenteditable') {
|
|
549
|
-
const node = identity.node;
|
|
550
|
-
if (typeof node?.innerText === 'string' || typeof node?.textContent === 'string') {
|
|
551
|
-
observed = node.innerText || node.textContent || '';
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
if (typeof observed !== 'string' || observed !== content) return [];
|
|
555
|
-
} else if (!window.CodexOverleafProjectFiles.isUsableProjectFileContent(content)) return [];
|
|
556
|
-
return [{ path: activePath, content, source: 'active-editor', kind: 'text' }];
|
|
557
|
-
} catch (_error) {
|
|
558
|
-
return [];
|
|
559
|
-
}
|
|
553
|
+
async function readActiveEditorOverlay(activePath, referenceFiles, options = {}) {
|
|
554
|
+
if (!activePath) return { files: [], skipped: [] };
|
|
555
|
+
const references = referenceFiles ? new Map(referenceFiles
|
|
556
|
+
.filter(file => file.kind !== 'binary' && typeof file.content === 'string')
|
|
557
|
+
.map(file => [file.path, file.content])) : null;
|
|
558
|
+
const ready = await waitForActiveEditorText(activePath, 5000, { ...options, allowEmpty: true, references });
|
|
559
|
+
return ready.ok
|
|
560
|
+
? { files: [editorFile(activePath, ready, 'active-editor')], skipped: [] }
|
|
561
|
+
: { files: [], skipped: [{ path: activePath, reason: ready.code || ready.reason }] };
|
|
560
562
|
}
|
|
561
563
|
|
|
562
564
|
function mergeSnapshotFiles(primaryFiles = [], overrideFiles = []) {
|
|
@@ -609,7 +611,7 @@
|
|
|
609
611
|
return projectPaths.every(filePath => capturedPaths.has(filePath));
|
|
610
612
|
}
|
|
611
613
|
|
|
612
|
-
async function fetchOverleafDocContent(record) {
|
|
614
|
+
async function fetchOverleafDocContent(record, expectedProjectId = getProjectId()) {
|
|
613
615
|
if (!record?.id || !record.path) {
|
|
614
616
|
return {
|
|
615
617
|
ok: false,
|
|
@@ -625,6 +627,9 @@
|
|
|
625
627
|
};
|
|
626
628
|
}
|
|
627
629
|
|
|
630
|
+
const target = treeOperations.resolveTargetDocument?.(record.path, { projectId });
|
|
631
|
+
if (projectId !== expectedProjectId || (target && (target.status !== 'known' || target.documentId !== record.id.toLowerCase())))
|
|
632
|
+
return { ok: false, reason: 'document_identity_mismatch' };
|
|
628
633
|
try {
|
|
629
634
|
const response = await fetch(`${window.location.origin}/project/${encodeURIComponent(projectId)}/doc/${encodeURIComponent(record.id)}`, {
|
|
630
635
|
credentials: 'include',
|
|
@@ -639,6 +644,9 @@
|
|
|
639
644
|
};
|
|
640
645
|
}
|
|
641
646
|
const payload = await response.json();
|
|
647
|
+
const currentTarget = treeOperations.resolveTargetDocument?.(record.path, { projectId });
|
|
648
|
+
if (getProjectId() !== projectId || (target && (currentTarget?.status !== 'known' || currentTarget.documentId !== target.documentId)))
|
|
649
|
+
return { ok: false, reason: 'document_identity_mismatch' };
|
|
642
650
|
const content = readDocPayloadContent(payload);
|
|
643
651
|
if (!window.CodexOverleafProjectFiles.isUsableProjectFileContent(content)) {
|
|
644
652
|
return {
|
|
@@ -1071,6 +1079,7 @@
|
|
|
1071
1079
|
assembleSnapshot,
|
|
1072
1080
|
buildProjectFileList,
|
|
1073
1081
|
buildProjectSnapshot,
|
|
1082
|
+
getSnapshotEditorCaptures,
|
|
1074
1083
|
fetchProjectZipSnapshot,
|
|
1075
1084
|
getInvalidatedAt,
|
|
1076
1085
|
getRequestedSnapshotPaths,
|