codex-overleaf-link 2.3.6 → 2.4.0
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 +59 -34
- package/README.zh-CN.md +543 -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 +53 -50
- 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 +583 -112
- 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/runResultActions.js +16 -1
- package/extension/src/content/runTimelineView.js +1 -1
- package/extension/src/content/sessionForkController.js +20 -11
- 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/textFileCreator.js +493 -0
- package/extension/src/page/trackedChangeCapture.js +342 -0
- package/extension/src/page/trackedChangesLifecycle.js +145 -149
- package/extension/src/page/treeOperations.js +30 -2
- package/extension/src/page/writebackRouter.js +94 -48
- package/extension/src/pageBridge.js +62 -25
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/pageRpcContract.js +2 -1
- package/extension/src/shared/sessionState.js +6 -2
- package/extension/src/shared/storageDb.js +10 -12
- package/extension/src/shared/storageRunActions.js +29 -1
- package/extension/src/shared/writebackSettlement.js +19 -5
- package/extension/styles/panel.css +55 -10
- package/native-host/src/taskRunnerRuntime.js +25 -0
- package/package.json +5 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"entry": "extension/entries/content-entry.mjs",
|
|
4
4
|
"output": "extension/src/content/generated/content.bundle.js",
|
|
5
|
-
"sourceDigest": "
|
|
6
|
-
"outputDigest": "
|
|
7
|
-
"byteLength":
|
|
5
|
+
"sourceDigest": "f146d7e07b67706193fa5fcff26d7c9719826ac745aec9b745dd95c52413d27c",
|
|
6
|
+
"outputDigest": "11993fd0746a026b1e74399060e70559eb569fca492fcdec36c2fb69a78d6b80",
|
|
7
|
+
"byteLength": 2404976,
|
|
8
8
|
"inputs": [
|
|
9
9
|
"extension/entries/content-entry.mjs",
|
|
10
10
|
"extension/src/content/activeTurnControl.js",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"extension/src/content/sessionPersistence.js",
|
|
63
63
|
"extension/src/content/settingsPanel.js",
|
|
64
64
|
"extension/src/content/themeController.js",
|
|
65
|
+
"extension/src/content/trackedChangeCaptureController.js",
|
|
65
66
|
"extension/src/content/updateIdle.js",
|
|
66
67
|
"extension/src/content/updateNotice.js",
|
|
67
68
|
"extension/src/content/writebackController.js",
|
|
@@ -134,7 +134,8 @@
|
|
|
134
134
|
return { stale: false, error: response?.error || { code: 'provider_models_unavailable' } };
|
|
135
135
|
}
|
|
136
136
|
const sourceModels = hasDiscoveredModels ? response.result.models : fallbackModels;
|
|
137
|
-
const
|
|
137
|
+
const preserveBuiltin = (selection?.providerId || 'builtin') === 'builtin' && (getState()?.providerId || 'builtin') === 'builtin';
|
|
138
|
+
const retainedSelectedModel = retainSelectedModel(sourceModels, currentSelectedModel, { preserveUnavailable: preserveBuiltin });
|
|
138
139
|
const normalized = modelCatalog.normalizeDiscoveredModels({ models: sourceModels, selectedModel: retainedSelectedModel });
|
|
139
140
|
if (normalized.usedFallback && response.result?.providerId && response.result.providerId !== 'builtin') {
|
|
140
141
|
applyUnavailableModelOptions({ code: 'provider_model_catalog_invalid', message: 'The active provider returned no usable models.' });
|
|
@@ -143,9 +144,12 @@
|
|
|
143
144
|
error: { code: 'provider_model_catalog_invalid', message: 'The active provider returned no usable models.' }
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
|
-
|
|
147
|
+
const renderedModels = preserveBuiltin
|
|
148
|
+
? Support.preserveSelectedCapabilities(normalized.models, retainedSelectedModel, getState(), !hasDiscoveredModels || response.result?.source === 'fallback')
|
|
149
|
+
: normalized.models;
|
|
150
|
+
renderModelOptions(renderedModels, retainedSelectedModel, { allowUnknownSelected: false });
|
|
147
151
|
modelDiscovery = {
|
|
148
|
-
status: hasDiscoveredModels && !normalized.usedFallback ? 'discovered' : 'fallback',
|
|
152
|
+
status: hasDiscoveredModels && !normalized.usedFallback && response.result?.source !== 'fallback' ? 'discovered' : 'fallback',
|
|
149
153
|
source: hasDiscoveredModels ? response.result?.source || 'unknown' : 'fallback',
|
|
150
154
|
fetchedAt: hasDiscoveredModels ? response.result?.fetchedAt || '' : '',
|
|
151
155
|
errorCode: hasDiscoveredModels ? '' : response?.error?.code || '',
|
|
@@ -172,15 +176,16 @@
|
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
function applyUnavailableModelOptions(error) {
|
|
179
|
+
const selectedModel = resolveSelectedModel();
|
|
175
180
|
const modelSelect = getPanel()?.querySelector('[data-model]');
|
|
176
181
|
if (modelSelect) {
|
|
177
182
|
modelSelect.textContent = '';
|
|
178
183
|
const option = document.createElement('option');
|
|
179
|
-
option.value =
|
|
184
|
+
option.value = selectedModel;
|
|
180
185
|
option.textContent = tx('Models unavailable', '模型不可用');
|
|
181
186
|
option.disabled = true;
|
|
182
187
|
modelSelect.append(option);
|
|
183
|
-
modelSelect.value =
|
|
188
|
+
modelSelect.value = selectedModel;
|
|
184
189
|
modelSelect.disabled = true;
|
|
185
190
|
}
|
|
186
191
|
modelDiscovery = {
|
|
@@ -190,8 +195,15 @@
|
|
|
190
195
|
errorCode: error?.code || '',
|
|
191
196
|
errorMessage: error?.message || (error ? String(error) : '')
|
|
192
197
|
};
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
const pendingModels = Support.preserveSelectedCapabilities(
|
|
199
|
+
[{ id: selectedModel, unverified: true }], selectedModel, getState(), true
|
|
200
|
+
);
|
|
201
|
+
renderReasoningOptions(pendingModels);
|
|
202
|
+
renderSpeedOptions(pendingModels);
|
|
203
|
+
for (const selector of ['[data-reasoning]', '[data-speed]']) {
|
|
204
|
+
const input = getPanel()?.querySelector(selector);
|
|
205
|
+
if (input) input.disabled = true;
|
|
206
|
+
}
|
|
195
207
|
renderModelConfigChoices();
|
|
196
208
|
updateModelDisplay();
|
|
197
209
|
}
|
|
@@ -200,9 +212,9 @@
|
|
|
200
212
|
const modelCatalog = getModelCatalog();
|
|
201
213
|
const fallbackModels = modelCatalog.FALLBACK_MODELS;
|
|
202
214
|
const sourceModels = fallbackModels;
|
|
203
|
-
const retainedSelectedModel = retainSelectedModel(sourceModels, selectedModel);
|
|
215
|
+
const retainedSelectedModel = retainSelectedModel(sourceModels, selectedModel, { preserveUnavailable: (getState()?.providerId || 'builtin') === 'builtin' });
|
|
204
216
|
const normalized = modelCatalog.normalizeDiscoveredModels({ models: sourceModels, selectedModel: retainedSelectedModel });
|
|
205
|
-
renderModelOptions(normalized.models, retainedSelectedModel);
|
|
217
|
+
renderModelOptions(Support.preserveSelectedCapabilities(normalized.models, retainedSelectedModel, getState(), true), retainedSelectedModel);
|
|
206
218
|
modelDiscovery = {
|
|
207
219
|
status: 'fallback',
|
|
208
220
|
source: 'fallback',
|
|
@@ -29,14 +29,49 @@
|
|
|
29
29
|
return String(label || '').replace(/^gpt[-\s]*/i, '');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function retainSelectedModel(models, selectedModel) {
|
|
32
|
+
function retainSelectedModel(models, selectedModel, options = {}) {
|
|
33
33
|
const selectedId = normalizeModelOptionId(selectedModel);
|
|
34
34
|
if (!selectedId) return '';
|
|
35
|
+
if (options.preserveUnavailable === true) return selectedId;
|
|
35
36
|
return (Array.isArray(models) ? models : []).some(model => (
|
|
36
37
|
normalizeModelOptionId(typeof model === 'string' ? model : model?.id) === selectedId
|
|
37
38
|
)) ? selectedId : '';
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// A fallback or missing catalog entry cannot revoke an explicit selection.
|
|
42
|
+
// Keep its saved controls provisional until real capability data arrives.
|
|
43
|
+
function preserveSelectedCapabilities(models, selectedModel, preferences = {}, provisional = false) {
|
|
44
|
+
const selectedId = normalizeModelOptionId(selectedModel);
|
|
45
|
+
const saved = preferences || {};
|
|
46
|
+
return (Array.isArray(models) ? models : []).map(model => {
|
|
47
|
+
if (model.id !== selectedId || (!provisional && !model.unverified)) return model;
|
|
48
|
+
const reasoningEfforts = normalizeReasoningEffortsForSelect([
|
|
49
|
+
...(model.reasoningEfforts || []), saved.reasoningEffort
|
|
50
|
+
]);
|
|
51
|
+
const speedTiers = normalizeSpeedTiersForSelect(model.speedTiers);
|
|
52
|
+
const speed = ['standard', 'fast'].includes(saved.speedTier) ? saved.speedTier : '';
|
|
53
|
+
if (speed && !speedTiers.includes(speed)) speedTiers.push(speed);
|
|
54
|
+
return {
|
|
55
|
+
...model, unverified: true, reasoningEfforts, speedTiers,
|
|
56
|
+
defaultReasoningEffort: reasoningEfforts.includes(saved.reasoningEffort)
|
|
57
|
+
? saved.reasoningEffort : model.defaultReasoningEffort,
|
|
58
|
+
defaultSpeedTier: speed || model.defaultSpeedTier || 'standard'
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getRenderedModelEntries(panel) {
|
|
64
|
+
return Array.from(panel?.querySelector('[data-model]')?.options || []).map(option => ({
|
|
65
|
+
id: option.value,
|
|
66
|
+
label: option.textContent,
|
|
67
|
+
reasoningEfforts: (option.dataset.reasoningEfforts || '').split(',').filter(Boolean),
|
|
68
|
+
defaultReasoningEffort: option.dataset.defaultReasoningEffort || '',
|
|
69
|
+
reasoningPresentation: option.dataset.reasoningPresentation || '',
|
|
70
|
+
speedTiers: (option.dataset.speedTiers || 'standard').split(',').filter(Boolean),
|
|
71
|
+
defaultSpeedTier: option.dataset.defaultSpeedTier || 'standard'
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
40
75
|
function shouldUseBuiltInFallback(response) {
|
|
41
76
|
const providerId = normalizeModelOptionId(response?.result?.providerId);
|
|
42
77
|
return response?.ok === true && (!providerId || providerId === 'builtin');
|
|
@@ -114,6 +149,8 @@
|
|
|
114
149
|
normalizeReasoningEffortsForSelect,
|
|
115
150
|
normalizeSpeedTiersForSelect,
|
|
116
151
|
retainSelectedModel,
|
|
152
|
+
preserveSelectedCapabilities,
|
|
153
|
+
getRenderedModelEntries,
|
|
117
154
|
shouldUseBuiltInFallback
|
|
118
155
|
};
|
|
119
156
|
return api;
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
async function dispatch(payload, channelMethod) {
|
|
24
24
|
const compatibilityGate = await ensureForMethod(payload?.method);
|
|
25
25
|
if (!compatibilityGate.ok) return compatibilityGate.response;
|
|
26
|
-
|
|
26
|
+
// Post-Undo cache bookkeeping must survive the ended run's cancellation.
|
|
27
|
+
if (gatedMethods.has(payload?.method) && payload.method !== 'mirror.invalidate') {
|
|
28
|
+
options.throwIfCancellationRequested?.();
|
|
29
|
+
}
|
|
27
30
|
return nativeChannel[channelMethod](
|
|
28
31
|
attachEvidence(payload, compatibilityGate.compatibility)
|
|
29
32
|
);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
(function initCodexOverleafOtWarmMirror() {
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
4
3
|
// Experimental OT warm-mirror glue carved out of contentRuntime.js (v1.4.9
|
|
5
4
|
// structural-debt phase 5): the per-project enable toggle flow, the
|
|
6
5
|
// poll/flush timers and patch queue, mirror prefetch, warm-start
|
|
@@ -52,6 +51,7 @@
|
|
|
52
51
|
let currentOtStatus = 'off';
|
|
53
52
|
let otTogglePending = false;
|
|
54
53
|
let otSyncRequestId = 0;
|
|
54
|
+
let otStartRetryCount = 0;
|
|
55
55
|
let otWarmMirrorProjectId = '';
|
|
56
56
|
let lastExperimentalOtProjectId = '';
|
|
57
57
|
let otWarmMirrorState = {
|
|
@@ -226,6 +226,7 @@
|
|
|
226
226
|
const projectId = getCurrentProjectId();
|
|
227
227
|
otWarmMirrorState.failureActive = false;
|
|
228
228
|
lastExperimentalOtProjectId = projectId;
|
|
229
|
+
otStartRetryCount = 0;
|
|
229
230
|
setExperimentalOtEnabledForProject(projectId, checkbox.checked);
|
|
230
231
|
updateExperimentalOtToggleControl(checkbox.checked);
|
|
231
232
|
// Per-card saved feedback, same contract as every other settings card.
|
|
@@ -306,8 +307,10 @@
|
|
|
306
307
|
await pauseOtWarmMirror(pauseAfterStart.reason || 'paused');
|
|
307
308
|
return response;
|
|
308
309
|
}
|
|
309
|
-
|
|
310
|
+
if (status === 'starting') recordOtFailure('missing_active_path', 'start');
|
|
311
|
+
else otWarmMirrorState.failureActive = false;
|
|
310
312
|
updateOtStatusDisplay(status);
|
|
313
|
+
otStartRetryCount = 0;
|
|
311
314
|
scheduleOtEventPolling(projectId, { immediate: true });
|
|
312
315
|
return response;
|
|
313
316
|
}
|
|
@@ -338,21 +341,36 @@
|
|
|
338
341
|
clearOtEventPolling({ clearPatchQueue: true });
|
|
339
342
|
recordOtFailure(readOtBridgeErrorCode(response, 'ot_start_failed'), 'start');
|
|
340
343
|
otWarmMirrorState.suspended = true;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
344
|
+
// Observer availability is transient; only an explicit user action may
|
|
345
|
+
// change the persisted opt-in. A cold editor/bridge must not turn OT off
|
|
346
|
+
// permanently. failureActive/suspended already prohibit cache reuse.
|
|
347
|
+
updateExperimentalOtToggleControl(isExperimentalOtEnabledForProject(projectId));
|
|
346
348
|
updateOtStatusDisplay('unavailable');
|
|
347
|
-
saveStateSoon();
|
|
348
349
|
try { await callPageBridge('stopOtObserver', { projectId }); } catch (_error) { /* preserve the original failure */ }
|
|
350
|
+
scheduleOtStartRetry(projectId, requestId);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function scheduleOtStartRetry(projectId, requestId) {
|
|
354
|
+
const retryDelays = [1000, 3000, 6000];
|
|
355
|
+
const canRetry = () => isCurrentOtSync(requestId, projectId) && getCurrentProjectId() === projectId
|
|
356
|
+
&& isExperimentalOtEnabledForProject(projectId) && !getCurrentRunView();
|
|
357
|
+
if (!canRetry() || otStartRetryCount >= retryDelays.length) return;
|
|
358
|
+
// Use the existing owned timer so disabling OT, changing projects and
|
|
359
|
+
// starting a new observation all cancel pending retries normally.
|
|
360
|
+
const delayMs = retryDelays[otStartRetryCount++];
|
|
361
|
+
otWarmMirrorState.pollTimer = window.setTimeout(() => {
|
|
362
|
+
otWarmMirrorState.pollTimer = null;
|
|
363
|
+
if (!canRetry()) return;
|
|
364
|
+
syncOtWarmMirrorController().catch(() => {
|
|
365
|
+
if (getCurrentProjectId() === projectId && isExperimentalOtEnabledForProject(projectId)) {
|
|
366
|
+
updateOtStatusDisplay('unavailable');
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}, delayMs);
|
|
349
370
|
}
|
|
350
371
|
|
|
351
372
|
function readOtBridgeStatus(response) {
|
|
352
|
-
|
|
353
|
-
return 'unavailable';
|
|
354
|
-
}
|
|
355
|
-
return response.state || response.status || response.result?.state || response.result?.status || 'unavailable';
|
|
373
|
+
return otWarmMirrorController.readOtBridgeStatus(response);
|
|
356
374
|
}
|
|
357
375
|
|
|
358
376
|
function ensureOtWarmMirrorStateProject(projectId) {
|
|
@@ -361,6 +379,7 @@
|
|
|
361
379
|
}
|
|
362
380
|
clearOtEventPolling({ clearPatchQueue: true });
|
|
363
381
|
otWarmMirrorState.projectId = projectId;
|
|
382
|
+
otStartRetryCount = 0;
|
|
364
383
|
otWarmMirrorState.lastPatchAt = 0;
|
|
365
384
|
otWarmMirrorState.lastErrorCode = '';
|
|
366
385
|
otWarmMirrorState.failureActive = false;
|
|
@@ -438,6 +457,7 @@
|
|
|
438
457
|
return statusResponse;
|
|
439
458
|
}
|
|
440
459
|
const status = readOtBridgeStatus(statusResponse);
|
|
460
|
+
if (status === 'observing' && otWarmMirrorState.lastFailure?.phase === 'start') Object.assign(otWarmMirrorState, { failureActive: false, lastErrorCode: '', lastFailure: null });
|
|
441
461
|
if (['unavailable', 'inconsistent'].includes(status)) recordOtFailure(readOtBridgeErrorCode(statusResponse, 'ot_observer_unavailable'), 'observe');
|
|
442
462
|
if (status) {
|
|
443
463
|
updateOtStatusDisplay(status);
|
|
@@ -470,27 +490,11 @@
|
|
|
470
490
|
}
|
|
471
491
|
|
|
472
492
|
function readOtBridgeEvents(response) {
|
|
473
|
-
|
|
474
|
-
return response;
|
|
475
|
-
}
|
|
476
|
-
if (Array.isArray(response?.events)) {
|
|
477
|
-
return response.events;
|
|
478
|
-
}
|
|
479
|
-
if (Array.isArray(response?.result?.events)) {
|
|
480
|
-
return response.result.events;
|
|
481
|
-
}
|
|
482
|
-
return [];
|
|
493
|
+
return otWarmMirrorController.readOtBridgeEvents(response);
|
|
483
494
|
}
|
|
484
|
-
|
|
485
495
|
function readOtBridgeErrorCode(response, fallback) {
|
|
486
|
-
|
|
487
|
-
|| response?.reason
|
|
488
|
-
|| response?.error?.code
|
|
489
|
-
|| response?.error
|
|
490
|
-
|| fallback;
|
|
491
|
-
return typeof value === 'string' ? value : fallback;
|
|
496
|
+
return otWarmMirrorController.readOtBridgeErrorCode(response, fallback);
|
|
492
497
|
}
|
|
493
|
-
|
|
494
498
|
function recordOtFailure(code, phase) {
|
|
495
499
|
code = otWarmMirrorController.normalizeFailureCode(code);
|
|
496
500
|
otWarmMirrorState.lastErrorCode = code;
|
|
@@ -174,6 +174,23 @@
|
|
|
174
174
|
return { ok: true };
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
function readOtBridgeStatus(response) {
|
|
178
|
+
if (!response || response.ok === false) return 'unavailable';
|
|
179
|
+
return response.state || response.status || response.result?.state || response.result?.status || 'unavailable';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readOtBridgeEvents(response) {
|
|
183
|
+
if (Array.isArray(response)) return response;
|
|
184
|
+
if (Array.isArray(response?.events)) return response.events;
|
|
185
|
+
if (Array.isArray(response?.result?.events)) return response.result.events;
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readOtBridgeErrorCode(response, fallback) {
|
|
190
|
+
const value = response?.lastErrorCode || response?.reason || response?.error?.code || response?.error || fallback;
|
|
191
|
+
return typeof value === 'string' ? value : fallback;
|
|
192
|
+
}
|
|
193
|
+
|
|
177
194
|
function normalizeFailureCode(value, fallback = 'ot_error') {
|
|
178
195
|
const code = typeof value === 'string' ? value : value?.code;
|
|
179
196
|
if (/^[a-z][a-z0-9_]{0,79}$/.test(code || '')) return code;
|
|
@@ -265,6 +282,9 @@
|
|
|
265
282
|
takePatchBatch,
|
|
266
283
|
validatePatchReceipt,
|
|
267
284
|
normalizeFailureCode,
|
|
285
|
+
readOtBridgeStatus,
|
|
286
|
+
readOtBridgeEvents,
|
|
287
|
+
readOtBridgeErrorCode,
|
|
268
288
|
getFailureMessageKey,
|
|
269
289
|
buildPatchFilesRequest,
|
|
270
290
|
canUseOtWarmStart,
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
['src/page/overleafProjectSnapshot.js', 'codex-overleaf-project-snapshot-script'],
|
|
22
22
|
['src/page/pageBridgeCapability.js', 'codex-overleaf-page-bridge-capability-script'],
|
|
23
23
|
['src/page/treeOperations.js', 'codex-overleaf-tree-operations-script', true],
|
|
24
|
+
['src/page/textFileCreator.js', 'codex-overleaf-text-file-creator-script', true],
|
|
24
25
|
['src/page/binaryAssetUploader.js', 'codex-overleaf-binary-asset-uploader-script', true],
|
|
25
26
|
['src/page/snapshotRouter.js', 'codex-overleaf-snapshot-router-script'],
|
|
26
27
|
['src/page/writeGuard.js', 'codex-overleaf-write-guard-script', true],
|
|
28
|
+
['src/page/trackedChangeCapture.js', 'codex-overleaf-tracked-change-capture-script', true],
|
|
27
29
|
['src/page/trackedChangesLifecycle.js', 'codex-overleaf-tracked-changes-lifecycle-script', true],
|
|
28
30
|
['src/page/writebackRouter.js', 'codex-overleaf-writeback-router-script', true],
|
|
29
31
|
['src/pageBridge.js', 'codex-overleaf-page-bridge-script', true]
|
|
@@ -107,7 +107,22 @@
|
|
|
107
107
|
if (!copied) throw new Error('clipboard_copy_failed');
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
return Object.freeze({ configureResultActions });
|
|
110
|
+
return Object.freeze({ configureResultActions, projectUndoAvailability });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function projectUndoAvailability(run, projectRunSettlement) {
|
|
114
|
+
const payload = run.recoveryPayload && typeof run.recoveryPayload === 'object'
|
|
115
|
+
? run.recoveryPayload : run;
|
|
116
|
+
if (!Array.isArray(payload.appliedOperations)) return projectRunSettlement(run);
|
|
117
|
+
// Forward binary writes do not provide a rollback. Filter them only for
|
|
118
|
+
// display; preserve the canonical settlement and every real recovery field.
|
|
119
|
+
const appliedOperations = payload.appliedOperations.filter(operation =>
|
|
120
|
+
operation?.type !== 'binary-create' && operation?.type !== 'overwrite-binary');
|
|
121
|
+
if (appliedOperations.length === payload.appliedOperations.length) return projectRunSettlement(run);
|
|
122
|
+
return projectRunSettlement({
|
|
123
|
+
...run,
|
|
124
|
+
recoveryPayload: { ...payload, appliedOperations }
|
|
125
|
+
});
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
return Object.freeze({ create });
|
|
@@ -932,7 +932,7 @@
|
|
|
932
932
|
return;
|
|
933
933
|
}
|
|
934
934
|
|
|
935
|
-
const projection =
|
|
935
|
+
const projection = RunResultActions.projectUndoAvailability(run, projectRunSettlement);
|
|
936
936
|
if (!projection.canUndo && run.undoStatus !== 'applied') {
|
|
937
937
|
button.hidden = true;
|
|
938
938
|
return;
|
|
@@ -16,19 +16,28 @@
|
|
|
16
16
|
const source = findSource(previousState, runId);
|
|
17
17
|
if (!source) return failFork('session_fork_source_missing', tr('forkRunUnavailable'));
|
|
18
18
|
const { session, run, runIndex } = source;
|
|
19
|
-
|
|
19
|
+
const draftThreadId = run.codexTurnId && run.interruptedDraft?.turnId === run.codexTurnId
|
|
20
|
+
? run.interruptedDraft.threadId : '';
|
|
21
|
+
// Provider switches replace the session thread, not the selected node's origin.
|
|
22
|
+
const sourceThreadId = String(run.codexThreadId || draftThreadId || session.codexThreadId || '').trim();
|
|
23
|
+
if (run.status === 'running' || !sourceThreadId || !run.codexTurnId) {
|
|
20
24
|
return failFork('session_fork_unavailable', tr('forkRunUnavailable'));
|
|
21
25
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
let response;
|
|
27
|
+
try {
|
|
28
|
+
response = await sendBackgroundNative({
|
|
29
|
+
method: 'codex.thread.fork',
|
|
30
|
+
params: {
|
|
31
|
+
threadId: sourceThreadId,
|
|
32
|
+
lastTurnId: run.codexTurnId,
|
|
33
|
+
speedTier: session.speedTier,
|
|
34
|
+
loadCodexLocalSkills: previousState.loadCodexLocalSkills !== false,
|
|
35
|
+
loadCodexOverleafSkills: previousState.loadCodexOverleafSkills !== false
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return failFork(error?.code || 'session_fork_failed', error?.message || tr('forkRunFailed'));
|
|
40
|
+
}
|
|
32
41
|
if (!response?.ok || !response.result?.threadId) {
|
|
33
42
|
return failFork(response?.error?.code || 'session_fork_failed', response?.error?.message || tr('forkRunFailed'));
|
|
34
43
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.CodexOverleafTrackedChangeCaptureController = api;
|
|
5
|
+
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
|
6
|
+
'use strict';
|
|
7
|
+
function create(deps = {}) {
|
|
8
|
+
const now = deps.now || (() => Date.now());
|
|
9
|
+
const defer = deps.setTimeout || setTimeout;
|
|
10
|
+
const cancelTimer = deps.clearTimeout || clearTimeout;
|
|
11
|
+
let timer = null, busy = false, disposed = false;
|
|
12
|
+
const runs = () => deps.getRuns() || [];
|
|
13
|
+
const pending = run => Array.isArray(run.trackedChangeCaptures)
|
|
14
|
+
&& run.trackedChangeCaptures.some(capture => capture.state === 'pending');
|
|
15
|
+
const terminal = run => ['accepted', 'rejected'].includes(run.trackedChangeStatus)
|
|
16
|
+
|| run.undoStatus === 'applied' || run.forkSnapshot === true;
|
|
17
|
+
function buildPostFiles(run) {
|
|
18
|
+
const expectedFiles = Array.isArray(run?.undoExpectedFiles) ? run.undoExpectedFiles : [];
|
|
19
|
+
const appliedOperations = Array.isArray(run?.appliedOperations) ? run.appliedOperations : [];
|
|
20
|
+
if (!expectedFiles.length || !appliedOperations.length) return [];
|
|
21
|
+
const postFilesByPath = deps.buildExpectedFilesAfterOperations({ files: expectedFiles }, appliedOperations);
|
|
22
|
+
const expectedPaths = new Set(expectedFiles.map(file => file?.path).filter(Boolean));
|
|
23
|
+
return Array.from(postFilesByPath.entries())
|
|
24
|
+
.filter(([path, content]) => expectedPaths.has(path) && typeof content === 'string')
|
|
25
|
+
.map(([path, content]) => ({ path, content }));
|
|
26
|
+
}
|
|
27
|
+
function hasLegacyUntrackedCheckpoint(run) {
|
|
28
|
+
if (!run || run.executionSnapshot || run.trackedChangeStatus || run.undoStatus === 'applied'
|
|
29
|
+
|| run.forkSnapshot || run.undoTrackedChanges?.length || run.trackedChangeCaptures?.length) return false;
|
|
30
|
+
const operations = Array.isArray(run.undoOperations) ? run.undoOperations : [];
|
|
31
|
+
const before = Array.isArray(run.undoExpectedFiles) ? run.undoExpectedFiles : [];
|
|
32
|
+
if (!operations.length || !before.length || before.some(file => !file?.path || typeof file.content !== 'string')) return false;
|
|
33
|
+
const paths = new Set(before.map(file => file.path));
|
|
34
|
+
if (paths.size !== before.length || operations.some(op => op?.type !== 'edit' || !paths.has(op.path))) return false;
|
|
35
|
+
const complete = files => Array.isArray(files) && files.length === paths.size
|
|
36
|
+
&& new Set(files.map(file => file?.path)).size === paths.size
|
|
37
|
+
&& files.every(file => paths.has(file?.path) && typeof file.content === 'string');
|
|
38
|
+
const storedPost = run.undoBaseFiles;
|
|
39
|
+
try {
|
|
40
|
+
const post = buildPostFiles(run);
|
|
41
|
+
if (!complete(storedPost) || !complete(post)) return false;
|
|
42
|
+
const postByPath = new Map(post.map(file => [file.path, file.content]));
|
|
43
|
+
if (!storedPost.every(file => postByPath.get(file.path) === file.content)) return false;
|
|
44
|
+
const restored = deps.buildExpectedFilesAfterOperations({ files: storedPost }, operations);
|
|
45
|
+
// A legacy checkpoint must prove both directions; empty refs alone grant nothing.
|
|
46
|
+
return before.every(file => restored.get(file.path) === file.content);
|
|
47
|
+
} catch (_error) { return false; }
|
|
48
|
+
}
|
|
49
|
+
function schedule() {
|
|
50
|
+
if (disposed || timer !== null || busy || !runs().some(pending)) return;
|
|
51
|
+
timer = defer(() => {
|
|
52
|
+
timer = null;
|
|
53
|
+
flush().catch(error => deps.onError?.(error));
|
|
54
|
+
}, 500);
|
|
55
|
+
}
|
|
56
|
+
function record(run, captures) {
|
|
57
|
+
if (!Array.isArray(captures) || !captures.length) return;
|
|
58
|
+
const next = [...(run.trackedChangeCaptures || []), ...captures.filter(Boolean)];
|
|
59
|
+
if (next.every(capture => capture.state === 'observed')) return;
|
|
60
|
+
run.trackedChangeCaptures = next;
|
|
61
|
+
deps.notify?.(run, next.some(capture => capture.state === 'pending') ? 'pending' : 'needs_review', {
|
|
62
|
+
captures: next.map(capture => ({ path: capture.path, reason: capture.reason, ...capture.diagnostics }))
|
|
63
|
+
});
|
|
64
|
+
deps.persist().then(schedule).catch(error => deps.onError?.(error));
|
|
65
|
+
}
|
|
66
|
+
async function flush() {
|
|
67
|
+
if (disposed || busy) return;
|
|
68
|
+
busy = true;
|
|
69
|
+
try {
|
|
70
|
+
for (const run of runs().filter(pending)) {
|
|
71
|
+
if (terminal(run)) { run.trackedChangeCaptures = []; await deps.persist(); continue; }
|
|
72
|
+
if (run.runProjectId !== deps.getProjectId()) continue;
|
|
73
|
+
const newer = runs().some(other => other.id !== run.id
|
|
74
|
+
&& other.runProjectId === run.runProjectId
|
|
75
|
+
&& Date.parse(other.startedAt) > Date.parse(run.startedAt));
|
|
76
|
+
const original = run.trackedChangeCaptures;
|
|
77
|
+
const captures = original.map(capture => ({ ...capture }));
|
|
78
|
+
const postFiles = buildPostFiles(run);
|
|
79
|
+
for (let i = 0; i < captures.length; i++) {
|
|
80
|
+
const capture = captures[i];
|
|
81
|
+
if (capture.state !== 'pending') continue;
|
|
82
|
+
const expected = postFiles.find(file => file.path === capture.path);
|
|
83
|
+
if (newer || !expected || now() >= capture.expiresAt || run.status === 'cancelled') {
|
|
84
|
+
capture.state = 'needs_review';
|
|
85
|
+
capture.reason = newer ? 'capture_superseded' : !expected ? 'capture_baseline_missing'
|
|
86
|
+
: run.status === 'cancelled' ? 'capture_cancelled' : 'capture_expired';
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let response;
|
|
90
|
+
try {
|
|
91
|
+
response = await deps.callPageBridge('reconcileTrackedChangeCapture', {
|
|
92
|
+
runProjectId: run.runProjectId, capture, expectedContent: expected.content
|
|
93
|
+
});
|
|
94
|
+
} catch (_error) {
|
|
95
|
+
response = null;
|
|
96
|
+
}
|
|
97
|
+
// Never patch a superseded hydrated record or another project's state.
|
|
98
|
+
if (disposed || deps.getProjectId() !== run.runProjectId
|
|
99
|
+
|| !runs().includes(run) || terminal(run) || run.trackedChangeCaptures !== original) return;
|
|
100
|
+
const superseded = runs().some(other => other.id !== run.id
|
|
101
|
+
&& other.runProjectId === run.runProjectId
|
|
102
|
+
&& Date.parse(other.startedAt) > Date.parse(run.startedAt));
|
|
103
|
+
if (superseded || run.status === 'cancelled') {
|
|
104
|
+
captures[i] = { ...capture, state: 'needs_review', reason: 'capture_superseded' };
|
|
105
|
+
} else if (response?.capture?.path === capture.path
|
|
106
|
+
&& response.capture.runProjectId === run.runProjectId
|
|
107
|
+
&& ['observed', 'pending', 'needs_review'].includes(response.capture.state)) {
|
|
108
|
+
captures[i] = response.capture;
|
|
109
|
+
} else {
|
|
110
|
+
captures[i] = { ...capture, state: 'needs_review', reason: 'capture_bridge_unavailable' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!runs().includes(run) || terminal(run) || deps.getProjectId() !== run.runProjectId) continue;
|
|
114
|
+
if (captures.length && captures.every(capture => capture.state === 'observed')) {
|
|
115
|
+
const refs = deps.normalizeTrackedChanges([
|
|
116
|
+
...(run.undoTrackedChanges || []), ...captures.flatMap(capture => capture.refs || [])
|
|
117
|
+
]);
|
|
118
|
+
if (refs.length) {
|
|
119
|
+
run.undoTrackedChanges = refs;
|
|
120
|
+
run.trackedChangeStatus = 'pending';
|
|
121
|
+
run.trackedChangeCaptures = [];
|
|
122
|
+
await deps.persist();
|
|
123
|
+
deps.refresh(run);
|
|
124
|
+
deps.notify?.(run, 'observed', { count: refs.length });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const changed = JSON.stringify(original.map(summary)) !== JSON.stringify(captures.map(summary));
|
|
129
|
+
if (changed) {
|
|
130
|
+
run.trackedChangeCaptures = captures;
|
|
131
|
+
await deps.persist();
|
|
132
|
+
if (!pending(run)) deps.notify?.(run, 'needs_review', {
|
|
133
|
+
reasons: captures.filter(capture => capture.state !== 'observed').map(capture => capture.reason)
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
busy = false;
|
|
139
|
+
if (runs().some(run => pending(run) && run.runProjectId === deps.getProjectId())) schedule();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function summary(capture) {
|
|
143
|
+
return { state: capture.state, reason: capture.reason, refs: (capture.refs || []).map(ref => ref.key) };
|
|
144
|
+
}
|
|
145
|
+
function dispose() {
|
|
146
|
+
disposed = true;
|
|
147
|
+
if (timer !== null) cancelTimer(timer);
|
|
148
|
+
timer = null;
|
|
149
|
+
}
|
|
150
|
+
return { record, schedule, flush, dispose, buildPostFiles, hasLegacyUntrackedCheckpoint };
|
|
151
|
+
}
|
|
152
|
+
return { create };
|
|
153
|
+
});
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
let tx;
|
|
19
19
|
let getLocale;
|
|
20
20
|
let appendRunEvent;
|
|
21
|
+
let appendRunRecordEvent;
|
|
21
22
|
// In-flight background mirror refresh (v1.7.5) — see the writeback path.
|
|
22
23
|
let pendingMirrorRefresh = null;
|
|
23
24
|
let appendChangeSummary;
|
|
@@ -69,6 +70,34 @@
|
|
|
69
70
|
: null;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function invalidateMirrorAfterUndo(runId, projectId, result = {}) {
|
|
74
|
+
if (!projectId || (result?.changedDocument !== true && !result?.applied?.length)) return Promise.resolve('not-attempted');
|
|
75
|
+
const pending = Promise.resolve(pendingMirrorRefresh).catch(() => {}).then(async () => {
|
|
76
|
+
const response = await sendBackgroundNative({ method: 'mirror.invalidate', params: { projectId } });
|
|
77
|
+
if (!response?.ok || response.result?.invalidated !== true) {
|
|
78
|
+
throw new Error(response?.error?.message || 'Native host did not confirm mirror invalidation.');
|
|
79
|
+
}
|
|
80
|
+
if (getCurrentProjectId() === projectId) {
|
|
81
|
+
resetContextProject();
|
|
82
|
+
await callPageBridge('invalidateProjectSnapshot', { invalidateFileList: true });
|
|
83
|
+
}
|
|
84
|
+
return 'invalidated';
|
|
85
|
+
}).catch(error => {
|
|
86
|
+
appendRunRecordEvent?.(runId, {
|
|
87
|
+
status: 'warning',
|
|
88
|
+
title: tx(
|
|
89
|
+
`Overleaf undo finished, but local mirror invalidation failed: ${error.message}. Refresh this project or reconnect the native host before running again.`,
|
|
90
|
+
`Overleaf 撤销已完成,但本地镜像失效处理失败:${error.message}。请刷新项目或重新连接 Native Host 后再运行。`
|
|
91
|
+
)
|
|
92
|
+
});
|
|
93
|
+
return 'failed';
|
|
94
|
+
}).finally(() => {
|
|
95
|
+
if (pendingMirrorRefresh === pending) pendingMirrorRefresh = null;
|
|
96
|
+
});
|
|
97
|
+
pendingMirrorRefresh = pending;
|
|
98
|
+
return pending;
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
async function applySyncChangesToOverleaf(syncChanges = [], project = {}, options = {}) {
|
|
73
102
|
const runMode = options.mode || getState().mode;
|
|
74
103
|
const runRequireReviewing = typeof options.requireReviewing === 'boolean'
|
|
@@ -822,6 +851,7 @@
|
|
|
822
851
|
tx,
|
|
823
852
|
getLocale,
|
|
824
853
|
appendRunEvent,
|
|
854
|
+
appendRunRecordEvent,
|
|
825
855
|
appendChangeSummary,
|
|
826
856
|
appendCompletionReport,
|
|
827
857
|
appendOperationsPreview,
|
|
@@ -867,6 +897,7 @@
|
|
|
867
897
|
return {
|
|
868
898
|
applySyncChangesToOverleaf,
|
|
869
899
|
resolveCompileLogContext,
|
|
900
|
+
invalidateMirrorAfterUndo,
|
|
870
901
|
getPendingMirrorRefresh: () => pendingMirrorRefresh
|
|
871
902
|
};
|
|
872
903
|
}
|