codex-overleaf-link 1.8.6 → 1.9.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 +45 -21
- package/bin/codex-overleaf-link.mjs +58 -0
- package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
- package/extension/assets/icons/codex-overleaf-icon.png +0 -0
- package/extension/assets/icons/icon128.png +0 -0
- package/extension/assets/icons/icon16.png +0 -0
- package/extension/assets/icons/icon32.png +0 -0
- package/extension/assets/icons/icon48.png +0 -0
- package/extension/bootstrap/background.js +404 -0
- package/extension/bootstrap/manifest.template.json +51 -0
- package/extension/bootstrap/popup.css +26 -0
- package/extension/bootstrap/popup.html +38 -0
- package/extension/bootstrap/popup.js +44 -0
- package/extension/bootstrap/runtimeContext.js +2 -0
- package/extension/runtime-manifest.json +60 -0
- package/extension/src/background.js +532 -0
- package/extension/src/content/applyResultFormatters.js +387 -0
- package/extension/src/content/composerAttachments.js +522 -0
- package/extension/src/content/composerPanel.js +254 -0
- package/extension/src/content/contentRuntime.js +8895 -0
- package/extension/src/content/contextTray.js +686 -0
- package/extension/src/content/diagnosticsController.js +659 -0
- package/extension/src/content/diagnosticsPanel.js +342 -0
- package/extension/src/content/diffReviewPanel.js +797 -0
- package/extension/src/content/localSkillsPanel.js +280 -0
- package/extension/src/content/markdownText.js +606 -0
- package/extension/src/content/mirrorHealth.js +92 -0
- package/extension/src/content/modelPicker.js +489 -0
- package/extension/src/content/nativeChannel.js +62 -0
- package/extension/src/content/otWarmMirror.js +964 -0
- package/extension/src/content/otWarmMirrorController.js +174 -0
- package/extension/src/content/panelMaintenance.js +367 -0
- package/extension/src/content/panelRenderer.js +253 -0
- package/extension/src/content/recentProjects.js +1063 -0
- package/extension/src/content/reviewHunks.js +247 -0
- package/extension/src/content/runController.js +242 -0
- package/extension/src/content/runTimelineView.js +1126 -0
- package/extension/src/content/sessionManager.js +487 -0
- package/extension/src/content/sessionPanel.js +263 -0
- package/extension/src/content/settingsPanel.js +527 -0
- package/extension/src/content/themeController.js +91 -0
- package/extension/src/content/updateIdle.js +67 -0
- package/extension/src/content/writebackController.js +300 -0
- package/extension/src/content/writebackOrchestrator.js +850 -0
- package/extension/src/contentScript.js +8 -0
- package/extension/src/page/compileBridge.js +460 -0
- package/extension/src/page/overleafCapabilities.js +100 -0
- package/extension/src/page/overleafEditor.js +282 -0
- package/extension/src/page/overleafProjectSnapshot.js +234 -0
- package/extension/src/page/overleafRealtimeObserver.js +491 -0
- package/extension/src/page/pageBridgeCapability.js +71 -0
- package/extension/src/page/snapshotRouter.js +948 -0
- package/extension/src/page/trackedChangesLifecycle.js +1506 -0
- package/extension/src/page/treeOperations.js +1584 -0
- package/extension/src/page/writeGuard.js +142 -0
- package/extension/src/page/writebackRouter.js +1934 -0
- package/extension/src/pageBridge.js +2224 -0
- package/extension/src/popup.js +247 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/styles/panel.css +4048 -0
- package/native-host/src/index.js +9 -2
- package/native-host/src/managedInstall.js +393 -0
- package/native-host/src/managedLauncherRuntime.js +71 -0
- package/native-host/src/nativeHostPlatform.js +16 -0
- package/native-host/src/runtimeInstaller.js +6 -1
- package/native-host/src/taskRunnerRuntime.js +8 -0
- package/native-host/src/updateArchive.js +253 -0
- package/native-host/src/updateManager.js +548 -0
- package/native-host/src/updateTrust.js +156 -0
- package/package.json +11 -2
- package/scripts/install-managed.mjs +64 -0
- package/scripts/uninstall-managed.mjs +48 -0
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
(function initCodexOverleafOtWarmMirror() {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const otWarmMirrorController = window.CodexOverleafOtWarmMirrorController;
|
|
5
|
+
const runController = window.CodexOverleafRunController;
|
|
6
|
+
const mirrorHealth = window.CodexOverleafMirrorHealth;
|
|
7
|
+
|
|
8
|
+
// Experimental OT warm-mirror glue carved out of contentRuntime.js (v1.4.9
|
|
9
|
+
// structural-debt phase 5): the per-project enable toggle flow, the
|
|
10
|
+
// poll/flush timers and patch queue, mirror prefetch, warm-start
|
|
11
|
+
// resolution, and the OT status display. Code moved verbatim; runtime
|
|
12
|
+
// collaborators are factory-injected and mutable runtime state is read
|
|
13
|
+
// through lazy getters (panel state writes go back through setState). The
|
|
14
|
+
// OT/prefetch view state below moved here with the code that owns it; the
|
|
15
|
+
// runtime reads it through the exported accessors and clears it on project
|
|
16
|
+
// navigation via clearMirrorPrefetchTimer()/releaseOtWarmMirrorProject().
|
|
17
|
+
function create(deps = {}) {
|
|
18
|
+
const {
|
|
19
|
+
tr,
|
|
20
|
+
tx,
|
|
21
|
+
closeDiagnosticsMenu,
|
|
22
|
+
syncCustomInstructionsEditorForProject,
|
|
23
|
+
throwIfRunCancellationRequested,
|
|
24
|
+
showPluginConfirm,
|
|
25
|
+
showPluginToast,
|
|
26
|
+
getCurrentProjectId,
|
|
27
|
+
updateProbeStatusOtSuffix,
|
|
28
|
+
sanitizeRunProjectSnapshot,
|
|
29
|
+
getMirrorFreshness,
|
|
30
|
+
buildSnapshotFileOverlays,
|
|
31
|
+
normalizeSnapshotPath,
|
|
32
|
+
formatProjectSnapshotUserLog,
|
|
33
|
+
formatProjectSnapshotWarning,
|
|
34
|
+
getProjectSnapshotWarnings,
|
|
35
|
+
sendBackgroundNative,
|
|
36
|
+
callPageBridge,
|
|
37
|
+
saveStateSoon,
|
|
38
|
+
finishRunView,
|
|
39
|
+
appendRunEvent,
|
|
40
|
+
appendCompletionReport,
|
|
41
|
+
appendLog,
|
|
42
|
+
appendPlainLog,
|
|
43
|
+
getPanel,
|
|
44
|
+
getState,
|
|
45
|
+
setState,
|
|
46
|
+
getCurrentRunView,
|
|
47
|
+
RUN_SNAPSHOT_ZIP_TIMEOUT_MS
|
|
48
|
+
} = deps;
|
|
49
|
+
|
|
50
|
+
let currentOtStatus = 'off';
|
|
51
|
+
let otSyncRequestId = 0;
|
|
52
|
+
let otWarmMirrorProjectId = '';
|
|
53
|
+
let lastExperimentalOtProjectId = '';
|
|
54
|
+
let otWarmMirrorState = {
|
|
55
|
+
projectId: '',
|
|
56
|
+
pollTimer: null,
|
|
57
|
+
flushTimer: null,
|
|
58
|
+
flushing: false,
|
|
59
|
+
patchQueue: [],
|
|
60
|
+
lastStatus: 'off',
|
|
61
|
+
lastPatchAt: 0,
|
|
62
|
+
lastErrorCode: ''
|
|
63
|
+
};
|
|
64
|
+
let mirrorPrefetchState = {
|
|
65
|
+
inFlight: null,
|
|
66
|
+
lastSuccessAt: 0,
|
|
67
|
+
lastErrorAt: 0,
|
|
68
|
+
lastError: null,
|
|
69
|
+
timer: null,
|
|
70
|
+
projectId: ''
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function getCurrentOtStatus() {
|
|
74
|
+
return currentOtStatus;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getOtWarmMirrorState() {
|
|
78
|
+
return otWarmMirrorState;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getLastExperimentalOtProjectId() {
|
|
82
|
+
return lastExperimentalOtProjectId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Clear the mirror-prefetch retry timer (called by the runtime when leaving
|
|
86
|
+
// a project, so a stale timer can't fire against the next project).
|
|
87
|
+
function clearMirrorPrefetchTimer() {
|
|
88
|
+
if (mirrorPrefetchState && mirrorPrefetchState.timer) {
|
|
89
|
+
window.clearTimeout(mirrorPrefetchState.timer);
|
|
90
|
+
mirrorPrefetchState.timer = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Reset the OT warm-mirror project binding when navigating away, so a stale
|
|
95
|
+
// projectId can't trick the canPollOtWarmMirror check after navigation.
|
|
96
|
+
function releaseOtWarmMirrorProject(prevProjectId) {
|
|
97
|
+
if (otWarmMirrorProjectId === prevProjectId) {
|
|
98
|
+
otWarmMirrorProjectId = '';
|
|
99
|
+
}
|
|
100
|
+
if (otWarmMirrorState && otWarmMirrorState.projectId === prevProjectId) {
|
|
101
|
+
otWarmMirrorState.projectId = '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isExperimentalOtEnabled() {
|
|
106
|
+
const projectId = getCurrentProjectId();
|
|
107
|
+
return isExperimentalOtEnabledForProject(projectId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isExperimentalOtEnabledForProject(projectId) {
|
|
111
|
+
return getState()?.experimentalOtByProject?.[projectId] === true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function setExperimentalOtEnabled(enabled) {
|
|
115
|
+
const projectId = getCurrentProjectId();
|
|
116
|
+
setExperimentalOtEnabledForProject(projectId, enabled);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setExperimentalOtEnabledForProject(projectId, enabled) {
|
|
120
|
+
if (!projectId) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setState({
|
|
124
|
+
...getState(),
|
|
125
|
+
experimentalOtByProject: {
|
|
126
|
+
...normalizeExperimentalOtByProject(getState()?.experimentalOtByProject),
|
|
127
|
+
[projectId]: enabled === true
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeExperimentalOtByProject(value) {
|
|
133
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
const normalized = {};
|
|
137
|
+
for (const key of Object.keys(value)) {
|
|
138
|
+
if (key) {
|
|
139
|
+
normalized[key] = value[key] === true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function syncExperimentalOtToggleForProject(projectId = getCurrentProjectId()) {
|
|
146
|
+
lastExperimentalOtProjectId = projectId;
|
|
147
|
+
const enabled = isExperimentalOtEnabledForProject(projectId);
|
|
148
|
+
const experimentalOtCheckbox = getPanel()?.querySelector('[data-experimental-ot]');
|
|
149
|
+
if (experimentalOtCheckbox) {
|
|
150
|
+
experimentalOtCheckbox.checked = enabled;
|
|
151
|
+
}
|
|
152
|
+
updateExperimentalOtToggleControl(enabled);
|
|
153
|
+
updateOtStatusDisplay(enabled
|
|
154
|
+
? (currentOtStatus === 'off' ? 'starting' : currentOtStatus)
|
|
155
|
+
: 'off');
|
|
156
|
+
return enabled;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function handleExperimentalOtToggleClick(event) {
|
|
160
|
+
// The handler is bound to the visible checkbox: by the time click fires,
|
|
161
|
+
// pre-click activation has ALREADY flipped .checked — that flipped value
|
|
162
|
+
// is the user's intended target. preventDefault then rolls the visual
|
|
163
|
+
// state back (synchronously, when dispatch completes), and the confirm
|
|
164
|
+
// flow applies the target only after approval.
|
|
165
|
+
const clicked = event?.currentTarget || event?.target;
|
|
166
|
+
const targetEnabled = clicked && typeof clicked.checked === 'boolean' ? clicked.checked : undefined;
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
await toggleExperimentalOtCheckbox(targetEnabled);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function handleExperimentalOtToggleKeydown(event) {
|
|
172
|
+
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
await toggleExperimentalOtCheckbox();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function toggleExperimentalOtCheckbox(targetEnabled) {
|
|
180
|
+
const checkbox = getPanel()?.querySelector('[data-experimental-ot]');
|
|
181
|
+
if (!checkbox) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Explicit target from the click interceptor wins; the keyboard/legacy
|
|
185
|
+
// path (no argument, un-flipped checkbox) falls back to inversion.
|
|
186
|
+
const nextEnabled = typeof targetEnabled === 'boolean' ? targetEnabled : !checkbox.checked;
|
|
187
|
+
if (nextEnabled) {
|
|
188
|
+
closeDiagnosticsMenu();
|
|
189
|
+
const approved = await showPluginConfirm({
|
|
190
|
+
title: tr('experimentalOtConfirmTitle'),
|
|
191
|
+
message: tr('experimentalOtConfirmMessage'),
|
|
192
|
+
confirmLabel: tr('experimentalOtConfirmEnable'),
|
|
193
|
+
cancelLabel: tr('confirmDefaultCancel')
|
|
194
|
+
});
|
|
195
|
+
if (!approved) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
closeDiagnosticsMenu();
|
|
200
|
+
}
|
|
201
|
+
checkbox.checked = nextEnabled;
|
|
202
|
+
handleExperimentalOtToggleChange({ currentTarget: checkbox });
|
|
203
|
+
if (nextEnabled) {
|
|
204
|
+
showPluginToast(tr('experimentalOtEnabledToast'), { status: 'info' });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function handleExperimentalOtToggleChange(event) {
|
|
209
|
+
const checkbox = event?.currentTarget || getPanel()?.querySelector('[data-experimental-ot]');
|
|
210
|
+
if (!checkbox) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const projectId = getCurrentProjectId();
|
|
214
|
+
lastExperimentalOtProjectId = projectId;
|
|
215
|
+
setExperimentalOtEnabledForProject(projectId, checkbox.checked);
|
|
216
|
+
updateExperimentalOtToggleControl(checkbox.checked);
|
|
217
|
+
// Per-card saved feedback, same contract as every other settings card.
|
|
218
|
+
const savedBadge = getPanel()?.querySelector('details[data-set-group="experimental"] [data-set-saved]');
|
|
219
|
+
if (savedBadge) {
|
|
220
|
+
savedBadge.hidden = false;
|
|
221
|
+
if (savedBadge._savedFlashTimer) {
|
|
222
|
+
clearTimeout(savedBadge._savedFlashTimer);
|
|
223
|
+
}
|
|
224
|
+
savedBadge._savedFlashTimer = setTimeout(() => {
|
|
225
|
+
savedBadge.hidden = true;
|
|
226
|
+
}, 1600);
|
|
227
|
+
}
|
|
228
|
+
updateOtStatusDisplay(checkbox.checked ? (currentOtStatus === 'off' ? 'starting' : currentOtStatus) : 'off');
|
|
229
|
+
syncOtWarmMirrorController().catch(error => {
|
|
230
|
+
updateOtStatusDisplay('unavailable');
|
|
231
|
+
appendPlainLog(tx(`Experimental OT warm mirror unavailable: ${error.message}`, `实验性 OT 预热镜像不可用:${error.message}`));
|
|
232
|
+
});
|
|
233
|
+
saveStateSoon();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function updateExperimentalOtToggleControl(enabled) {
|
|
237
|
+
// The visible switch (the checkbox itself) is the single control; keep it
|
|
238
|
+
// in sync for programmatic changes, then refresh the status line.
|
|
239
|
+
const checkbox = getPanel()?.querySelector('[data-experimental-ot]');
|
|
240
|
+
if (checkbox) {
|
|
241
|
+
checkbox.checked = enabled === true;
|
|
242
|
+
}
|
|
243
|
+
updateExperimentalOtMenuStatus();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function syncOtWarmMirrorController() {
|
|
247
|
+
const projectId = getCurrentProjectId();
|
|
248
|
+
const requestId = ++otSyncRequestId;
|
|
249
|
+
const enabled = isExperimentalOtEnabledForProject(projectId);
|
|
250
|
+
otWarmMirrorProjectId = projectId;
|
|
251
|
+
ensureOtWarmMirrorStateProject(projectId);
|
|
252
|
+
if (enabled) {
|
|
253
|
+
const pause = otWarmMirrorController?.shouldPauseOtWarmMirror?.({ running: Boolean(getCurrentRunView()) }) || { pause: false };
|
|
254
|
+
if (pause.pause) {
|
|
255
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
256
|
+
otWarmMirrorState.lastErrorCode = pause.reason || 'paused';
|
|
257
|
+
updateOtStatusDisplay('stale');
|
|
258
|
+
return { ok: true, skipped: true, reason: pause.reason || 'paused' };
|
|
259
|
+
}
|
|
260
|
+
updateOtStatusDisplay('starting');
|
|
261
|
+
} else {
|
|
262
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
263
|
+
}
|
|
264
|
+
const response = enabled
|
|
265
|
+
? await callPageBridge('startOtObserver', { projectId })
|
|
266
|
+
: await callPageBridge('stopOtObserver', {});
|
|
267
|
+
if (!isCurrentOtSync(requestId, projectId) || getCurrentProjectId() !== projectId) {
|
|
268
|
+
if (enabled) {
|
|
269
|
+
await handleStaleOtStartResponse(projectId, requestId);
|
|
270
|
+
}
|
|
271
|
+
return response;
|
|
272
|
+
}
|
|
273
|
+
if (!enabled) {
|
|
274
|
+
if (!isSuccessfulOtBridgeResponse(response)) {
|
|
275
|
+
updateOtStatusDisplay('unavailable');
|
|
276
|
+
return response;
|
|
277
|
+
}
|
|
278
|
+
updateOtStatusDisplay('off');
|
|
279
|
+
return response;
|
|
280
|
+
}
|
|
281
|
+
const status = readOtBridgeStatus(response);
|
|
282
|
+
if (!isSuccessfulOtBridgeResponse(response) || status === 'unavailable') {
|
|
283
|
+
handleFailedOtStart(projectId, requestId);
|
|
284
|
+
return response;
|
|
285
|
+
}
|
|
286
|
+
const pauseAfterStart = otWarmMirrorController?.shouldPauseOtWarmMirror?.({ running: Boolean(getCurrentRunView()) }) || { pause: false };
|
|
287
|
+
if (pauseAfterStart.pause) {
|
|
288
|
+
await pauseOtWarmMirror(pauseAfterStart.reason || 'paused');
|
|
289
|
+
return response;
|
|
290
|
+
}
|
|
291
|
+
updateOtStatusDisplay(status);
|
|
292
|
+
scheduleOtEventPolling(projectId, { immediate: true });
|
|
293
|
+
return response;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function isCurrentOtSync(requestId, projectId) {
|
|
297
|
+
return requestId === otSyncRequestId && projectId === otWarmMirrorProjectId;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isSuccessfulOtBridgeResponse(response) {
|
|
301
|
+
return Boolean(response) && response.ok !== false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function handleStaleOtStartResponse(projectId, requestId) {
|
|
305
|
+
if (isCurrentOtSync(requestId, projectId) && getCurrentProjectId() === projectId) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
309
|
+
await callPageBridge('stopOtObserver', {});
|
|
310
|
+
if (isExperimentalOtEnabled()) {
|
|
311
|
+
await syncOtWarmMirrorController();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function handleFailedOtStart(projectId, requestId) {
|
|
316
|
+
if (!isCurrentOtSync(requestId, projectId) || getCurrentProjectId() !== projectId) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
320
|
+
setExperimentalOtEnabledForProject(projectId, false);
|
|
321
|
+
const experimentalOtCheckbox = getPanel()?.querySelector('[data-experimental-ot]');
|
|
322
|
+
if (experimentalOtCheckbox) {
|
|
323
|
+
experimentalOtCheckbox.checked = false;
|
|
324
|
+
}
|
|
325
|
+
updateOtStatusDisplay('unavailable');
|
|
326
|
+
saveStateSoon();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function readOtBridgeStatus(response) {
|
|
330
|
+
if (!response || response.ok === false) {
|
|
331
|
+
return 'unavailable';
|
|
332
|
+
}
|
|
333
|
+
return response.getState() || response.status || response.result?.getState() || response.result?.status || 'unavailable';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function ensureOtWarmMirrorStateProject(projectId) {
|
|
337
|
+
if (!projectId || otWarmMirrorState.projectId === projectId) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
341
|
+
otWarmMirrorState.projectId = projectId;
|
|
342
|
+
otWarmMirrorState.lastPatchAt = 0;
|
|
343
|
+
otWarmMirrorState.lastErrorCode = '';
|
|
344
|
+
otWarmMirrorState.lastStatus = currentOtStatus;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function clearOtEventPolling(options = {}) {
|
|
348
|
+
if (otWarmMirrorState.pollTimer) {
|
|
349
|
+
window.clearTimeout(otWarmMirrorState.pollTimer);
|
|
350
|
+
otWarmMirrorState.pollTimer = null;
|
|
351
|
+
}
|
|
352
|
+
if (otWarmMirrorState.flushTimer) {
|
|
353
|
+
window.clearTimeout(otWarmMirrorState.flushTimer);
|
|
354
|
+
otWarmMirrorState.flushTimer = null;
|
|
355
|
+
}
|
|
356
|
+
if (options.clearPatchQueue !== false) {
|
|
357
|
+
otWarmMirrorState.patchQueue = [];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function scheduleOtEventPolling(projectId = getCurrentProjectId(), options = {}) {
|
|
362
|
+
if (!canPollOtWarmMirror(projectId)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
ensureOtWarmMirrorStateProject(projectId);
|
|
366
|
+
if (otWarmMirrorState.pollTimer) {
|
|
367
|
+
window.clearTimeout(otWarmMirrorState.pollTimer);
|
|
368
|
+
}
|
|
369
|
+
const delayMs = options.immediate
|
|
370
|
+
? 0
|
|
371
|
+
: Math.max(0, Number(otWarmMirrorController?.OT_POLL_INTERVAL_MS) || 1000);
|
|
372
|
+
otWarmMirrorState.pollTimer = window.setTimeout(() => {
|
|
373
|
+
otWarmMirrorState.pollTimer = null;
|
|
374
|
+
pollOtEvents(projectId).catch(error => {
|
|
375
|
+
otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'ot_poll_failed';
|
|
376
|
+
updateOtStatusDisplay('unavailable');
|
|
377
|
+
});
|
|
378
|
+
}, delayMs);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function canPollOtWarmMirror(projectId) {
|
|
382
|
+
return Boolean(projectId
|
|
383
|
+
&& otWarmMirrorController?.shouldPauseOtWarmMirror
|
|
384
|
+
&& otWarmMirrorController?.buildPatchFilesRequest
|
|
385
|
+
&& isExperimentalOtEnabledForProject(projectId)
|
|
386
|
+
&& otWarmMirrorProjectId === projectId
|
|
387
|
+
&& getCurrentProjectId() === projectId);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function pollOtEvents(projectId = otWarmMirrorState.projectId || getCurrentProjectId()) {
|
|
391
|
+
if (!canPollOtWarmMirror(projectId)) {
|
|
392
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
393
|
+
return { ok: false, skipped: true, reason: 'not_current_project' };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const pause = otWarmMirrorController.shouldPauseOtWarmMirror({ running: Boolean(getCurrentRunView()) });
|
|
397
|
+
if (pause.pause) {
|
|
398
|
+
otWarmMirrorState.lastErrorCode = pause.reason || 'paused';
|
|
399
|
+
updateOtStatusDisplay('stale');
|
|
400
|
+
clearOtEventPolling({ clearPatchQueue: false });
|
|
401
|
+
return { ok: false, skipped: true, reason: pause.reason || 'paused' };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const statusResponse = await callPageBridge('getOtStatus', { projectId });
|
|
406
|
+
if (!canPollOtWarmMirror(projectId)) {
|
|
407
|
+
return { ok: false, skipped: true, reason: 'project_changed' };
|
|
408
|
+
}
|
|
409
|
+
if (!isSuccessfulOtBridgeResponse(statusResponse)) {
|
|
410
|
+
otWarmMirrorState.lastErrorCode = readOtBridgeErrorCode(statusResponse, 'get_ot_status_failed');
|
|
411
|
+
updateOtStatusDisplay('unavailable');
|
|
412
|
+
return statusResponse;
|
|
413
|
+
}
|
|
414
|
+
const status = readOtBridgeStatus(statusResponse);
|
|
415
|
+
if (status) {
|
|
416
|
+
updateOtStatusDisplay(status);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const drainResponse = await callPageBridge('drainOtEvents', { projectId });
|
|
420
|
+
if (!canPollOtWarmMirror(projectId)) {
|
|
421
|
+
return { ok: false, skipped: true, reason: 'project_changed' };
|
|
422
|
+
}
|
|
423
|
+
if (!isSuccessfulOtBridgeResponse(drainResponse)) {
|
|
424
|
+
otWarmMirrorState.lastErrorCode = readOtBridgeErrorCode(drainResponse, 'drain_ot_events_failed');
|
|
425
|
+
updateOtStatusDisplay('unavailable');
|
|
426
|
+
return drainResponse;
|
|
427
|
+
}
|
|
428
|
+
const events = readOtBridgeEvents(drainResponse);
|
|
429
|
+
if (events.length) {
|
|
430
|
+
queueOtPatchEvents(events, projectId);
|
|
431
|
+
}
|
|
432
|
+
return drainResponse;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'ot_poll_failed';
|
|
435
|
+
updateOtStatusDisplay('unavailable');
|
|
436
|
+
return { ok: false, error };
|
|
437
|
+
} finally {
|
|
438
|
+
if (canPollOtWarmMirror(projectId) && !getCurrentRunView()) {
|
|
439
|
+
scheduleOtEventPolling(projectId);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function readOtBridgeEvents(response) {
|
|
445
|
+
if (Array.isArray(response)) {
|
|
446
|
+
return response;
|
|
447
|
+
}
|
|
448
|
+
if (Array.isArray(response?.events)) {
|
|
449
|
+
return response.events;
|
|
450
|
+
}
|
|
451
|
+
if (Array.isArray(response?.result?.events)) {
|
|
452
|
+
return response.result.events;
|
|
453
|
+
}
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function readOtBridgeErrorCode(response, fallback) {
|
|
458
|
+
const value = response?.lastErrorCode
|
|
459
|
+
|| response?.reason
|
|
460
|
+
|| response?.error?.code
|
|
461
|
+
|| response?.error
|
|
462
|
+
|| fallback;
|
|
463
|
+
return typeof value === 'string' ? value : fallback;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function queueOtPatchEvents(events, projectId = getCurrentProjectId()) {
|
|
467
|
+
if (!Array.isArray(events) || !events.length || !canPollOtWarmMirror(projectId)) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
ensureOtWarmMirrorStateProject(projectId);
|
|
471
|
+
otWarmMirrorState.patchQueue.push(...events);
|
|
472
|
+
scheduleOtPatchFlush(projectId);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function scheduleOtPatchFlush(projectId = otWarmMirrorState.projectId || getCurrentProjectId(), options = {}) {
|
|
476
|
+
if (!canPollOtWarmMirror(projectId) || !otWarmMirrorState.patchQueue.length) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (otWarmMirrorState.flushTimer) {
|
|
480
|
+
window.clearTimeout(otWarmMirrorState.flushTimer);
|
|
481
|
+
}
|
|
482
|
+
const delayMs = options.immediate
|
|
483
|
+
? 0
|
|
484
|
+
: Math.max(0, Number(otWarmMirrorController?.OT_PATCH_DEBOUNCE_MS) || 500);
|
|
485
|
+
otWarmMirrorState.flushTimer = window.setTimeout(() => {
|
|
486
|
+
otWarmMirrorState.flushTimer = null;
|
|
487
|
+
flushOtPatchBatch(projectId).catch(error => {
|
|
488
|
+
otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'ot_patch_failed';
|
|
489
|
+
updateOtStatusDisplay('inconsistent');
|
|
490
|
+
});
|
|
491
|
+
}, delayMs);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function flushOtPatchBatch(projectId = otWarmMirrorState.projectId || getCurrentProjectId()) {
|
|
495
|
+
if (otWarmMirrorState.flushing) {
|
|
496
|
+
return { ok: false, skipped: true, reason: 'ot_patch_in_flight' };
|
|
497
|
+
}
|
|
498
|
+
if (!canPollOtWarmMirror(projectId)) {
|
|
499
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
500
|
+
return { ok: false, skipped: true, reason: 'not_current_project' };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const pause = otWarmMirrorController.shouldPauseOtWarmMirror({ running: Boolean(getCurrentRunView()) });
|
|
504
|
+
if (pause.pause) {
|
|
505
|
+
otWarmMirrorState.lastErrorCode = pause.reason || 'paused';
|
|
506
|
+
updateOtStatusDisplay('stale');
|
|
507
|
+
clearOtEventPolling({ clearPatchQueue: false });
|
|
508
|
+
return { ok: false, skipped: true, reason: pause.reason || 'paused' };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const maxBatch = Math.max(1, Number(otWarmMirrorController?.OT_MAX_PATCH_BATCH) || 25);
|
|
512
|
+
const batch = otWarmMirrorState.patchQueue.splice(0, maxBatch);
|
|
513
|
+
if (!batch.length) {
|
|
514
|
+
return { ok: true, skipped: true, reason: 'empty_batch' };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
otWarmMirrorState.flushing = true;
|
|
518
|
+
try {
|
|
519
|
+
const request = {
|
|
520
|
+
...otWarmMirrorController.buildPatchFilesRequest({ projectId, events: batch }),
|
|
521
|
+
method: 'mirror.patchFiles'
|
|
522
|
+
};
|
|
523
|
+
if (!Array.isArray(request.params?.files) || request.params.files.length === 0) {
|
|
524
|
+
return { ok: true, skipped: true, reason: 'empty_filtered_batch' };
|
|
525
|
+
}
|
|
526
|
+
if (!canPollOtWarmMirror(projectId)) {
|
|
527
|
+
return { ok: false, skipped: true, reason: 'project_changed' };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const response = await sendBackgroundNative(request);
|
|
531
|
+
if (!response?.ok) {
|
|
532
|
+
otWarmMirrorState.lastErrorCode = response?.error?.code || response?.error?.message || 'mirror_patch_failed';
|
|
533
|
+
updateOtStatusDisplay('inconsistent');
|
|
534
|
+
return response;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const result = response.result || {};
|
|
538
|
+
const skippedFiles = Array.isArray(result?.skippedFiles) ? result.skippedFiles : [];
|
|
539
|
+
const skippedCount = Number.isFinite(Number(result?.skippedCount))
|
|
540
|
+
? Number(result.skippedCount)
|
|
541
|
+
: skippedFiles.length;
|
|
542
|
+
if (skippedFiles.length || skippedCount > 0) {
|
|
543
|
+
otWarmMirrorState.lastErrorCode = 'mirror_patch_skipped';
|
|
544
|
+
updateOtStatusDisplay('inconsistent');
|
|
545
|
+
return response;
|
|
546
|
+
}
|
|
547
|
+
const appliedFiles = Array.isArray(result?.appliedFiles) ? result.appliedFiles : null;
|
|
548
|
+
const appliedCount = Number.isFinite(Number(result?.appliedCount))
|
|
549
|
+
? Number(result.appliedCount)
|
|
550
|
+
: NaN;
|
|
551
|
+
if (!appliedFiles || !Number.isFinite(appliedCount) || appliedCount <= 0 || appliedFiles.length !== appliedCount) {
|
|
552
|
+
otWarmMirrorState.lastErrorCode = 'mirror_patch_invalid_result';
|
|
553
|
+
updateOtStatusDisplay('inconsistent');
|
|
554
|
+
return response;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
otWarmMirrorState.lastPatchAt = Date.now();
|
|
558
|
+
otWarmMirrorState.lastErrorCode = '';
|
|
559
|
+
if (canPollOtWarmMirror(projectId)) {
|
|
560
|
+
updateOtStatusDisplay('observing');
|
|
561
|
+
}
|
|
562
|
+
return response;
|
|
563
|
+
} catch (error) {
|
|
564
|
+
otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'mirror_patch_failed';
|
|
565
|
+
updateOtStatusDisplay('inconsistent');
|
|
566
|
+
return { ok: false, error };
|
|
567
|
+
} finally {
|
|
568
|
+
otWarmMirrorState.flushing = false;
|
|
569
|
+
if (otWarmMirrorState.patchQueue.length && canPollOtWarmMirror(projectId) && !getCurrentRunView()) {
|
|
570
|
+
scheduleOtPatchFlush(projectId, { immediate: true });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function pauseOtWarmMirror(reason = 'pause') {
|
|
576
|
+
const projectId = getCurrentProjectId();
|
|
577
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
578
|
+
if (!isExperimentalOtEnabledForProject(projectId)) {
|
|
579
|
+
return { ok: true, skipped: true, reason: 'disabled' };
|
|
580
|
+
}
|
|
581
|
+
ensureOtWarmMirrorStateProject(projectId);
|
|
582
|
+
otWarmMirrorState.lastErrorCode = reason;
|
|
583
|
+
updateOtStatusDisplay('stale');
|
|
584
|
+
try {
|
|
585
|
+
const response = await callPageBridge('stopOtObserver', { projectId, reason });
|
|
586
|
+
if (!isSuccessfulOtBridgeResponse(response)) {
|
|
587
|
+
otWarmMirrorState.lastErrorCode = readOtBridgeErrorCode(response, 'pause_ot_observer_failed');
|
|
588
|
+
updateOtStatusDisplay('unavailable');
|
|
589
|
+
}
|
|
590
|
+
return response;
|
|
591
|
+
} catch (error) {
|
|
592
|
+
otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'pause_ot_observer_failed';
|
|
593
|
+
updateOtStatusDisplay('unavailable');
|
|
594
|
+
return { ok: false, error };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function resumeOtWarmMirror(reason = 'resume') {
|
|
599
|
+
const projectId = getCurrentProjectId();
|
|
600
|
+
if (!isExperimentalOtEnabledForProject(projectId)) {
|
|
601
|
+
return { ok: true, skipped: true, reason: 'disabled' };
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
return await syncOtWarmMirrorController({ reason });
|
|
605
|
+
} catch (error) {
|
|
606
|
+
otWarmMirrorState.lastErrorCode = error?.code || error?.message || 'resume_ot_observer_failed';
|
|
607
|
+
updateOtStatusDisplay('unavailable');
|
|
608
|
+
return { ok: false, error };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function updateOtStatusDisplay(status = currentOtStatus) {
|
|
613
|
+
const normalized = normalizeOtStatus(status);
|
|
614
|
+
currentOtStatus = normalized;
|
|
615
|
+
otWarmMirrorState.lastStatus = normalized;
|
|
616
|
+
const statusElement = getPanel()?.querySelector('[data-ot-status]');
|
|
617
|
+
if (statusElement) {
|
|
618
|
+
const label = formatOtStatusLabel(normalized);
|
|
619
|
+
statusElement.textContent = formatOtToggleStatusText(label);
|
|
620
|
+
statusElement.dataset.otStatus = normalized;
|
|
621
|
+
statusElement.title = formatOtToggleStatusText(label);
|
|
622
|
+
}
|
|
623
|
+
updateExperimentalOtMenuStatus();
|
|
624
|
+
updateProbeStatusOtSuffix();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function formatOtStatusLabel(status) {
|
|
628
|
+
const labels = {
|
|
629
|
+
off: 'otStatusOff',
|
|
630
|
+
starting: 'otStatusStarting',
|
|
631
|
+
observing: 'otStatusObserving',
|
|
632
|
+
unavailable: 'otStatusUnavailable',
|
|
633
|
+
stale: 'otStatusStale',
|
|
634
|
+
inconsistent: 'otStatusInconsistent'
|
|
635
|
+
};
|
|
636
|
+
return tr(labels[normalizeOtStatus(status)] || 'otStatusUnavailable');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function formatOtToggleStatusText(label) {
|
|
640
|
+
return `OT: ${label}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function updateExperimentalOtMenuStatus() {
|
|
644
|
+
const statusElement = getPanel()?.querySelector('[data-experimental-ot-menu-status]');
|
|
645
|
+
if (!statusElement) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
statusElement.textContent = formatOtToggleStatusText(formatOtStatusLabel(currentOtStatus));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function normalizeOtStatus(status) {
|
|
652
|
+
const value = typeof status === 'string' ? status : '';
|
|
653
|
+
return ['off', 'starting', 'observing', 'unavailable', 'stale', 'inconsistent'].includes(value)
|
|
654
|
+
? value
|
|
655
|
+
: 'unavailable';
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function scheduleMirrorPrefetch(options = {}) {
|
|
659
|
+
syncMirrorPrefetchStateForProject();
|
|
660
|
+
if (!mirrorHealth?.shouldStartPrefetch) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const delayMs = Math.max(0, Number(options.delayMs) || 0);
|
|
664
|
+
if (mirrorPrefetchState.timer) {
|
|
665
|
+
window.clearTimeout(mirrorPrefetchState.timer);
|
|
666
|
+
mirrorPrefetchState.timer = null;
|
|
667
|
+
}
|
|
668
|
+
mirrorPrefetchState.timer = window.setTimeout(() => {
|
|
669
|
+
mirrorPrefetchState.timer = null;
|
|
670
|
+
syncMirrorPrefetch({ reason: options.reason || 'deferred' }).catch(error => {
|
|
671
|
+
mirrorPrefetchState.lastErrorAt = Date.now();
|
|
672
|
+
mirrorPrefetchState.lastError = error?.message || String(error);
|
|
673
|
+
});
|
|
674
|
+
}, delayMs);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function isExpectedPrefetchSkip(errorOrResponse) {
|
|
678
|
+
const code = errorOrResponse?.error?.code || errorOrResponse?.code || '';
|
|
679
|
+
const message = errorOrResponse?.error?.message || errorOrResponse?.message || String(errorOrResponse || '');
|
|
680
|
+
return code === 'project_locked'
|
|
681
|
+
|| code === 'project_changed'
|
|
682
|
+
|| /project_locked|project changed|project_changed|navigation/i.test(message);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function syncMirrorPrefetch(options = {}) {
|
|
686
|
+
syncMirrorPrefetchStateForProject();
|
|
687
|
+
const projectId = getCurrentProjectId();
|
|
688
|
+
const now = Date.now();
|
|
689
|
+
const readiness = mirrorHealth?.shouldStartPrefetch?.({
|
|
690
|
+
...mirrorPrefetchState,
|
|
691
|
+
busy: Boolean(getCurrentRunView()),
|
|
692
|
+
now
|
|
693
|
+
}) || { ok: false, reason: 'unavailable' };
|
|
694
|
+
if (!readiness.ok) {
|
|
695
|
+
return { ok: false, skipped: true, reason: readiness.reason };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
mirrorPrefetchState.projectId = projectId;
|
|
699
|
+
mirrorPrefetchState.lastAttemptAt = now;
|
|
700
|
+
const inFlight = (async () => {
|
|
701
|
+
const project = sanitizeRunProjectSnapshot(await callPageBridge('getProjectSnapshot', {
|
|
702
|
+
force: false,
|
|
703
|
+
maxAgeMs: 30000,
|
|
704
|
+
preferLightweight: false,
|
|
705
|
+
allowZipFallback: true,
|
|
706
|
+
allowEditorNavigation: false,
|
|
707
|
+
requireFullProject: true,
|
|
708
|
+
includeBinaryFiles: true,
|
|
709
|
+
zipOnly: true,
|
|
710
|
+
zipTimeoutMs: RUN_SNAPSHOT_ZIP_TIMEOUT_MS
|
|
711
|
+
}));
|
|
712
|
+
if (getCurrentProjectId() !== projectId) {
|
|
713
|
+
return { ok: false, skipped: true, reason: 'project_changed' };
|
|
714
|
+
}
|
|
715
|
+
if (project?.capabilities?.fullProjectSnapshot !== true) {
|
|
716
|
+
return { ok: false, skipped: true, reason: 'not_full_project' };
|
|
717
|
+
}
|
|
718
|
+
const response = await sendBackgroundNative({
|
|
719
|
+
method: 'mirror.sync',
|
|
720
|
+
params: {
|
|
721
|
+
projectId,
|
|
722
|
+
project,
|
|
723
|
+
reason: options.reason || 'prefetch'
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
if (!response?.ok) {
|
|
727
|
+
if (isExpectedPrefetchSkip(response)) {
|
|
728
|
+
return { ok: false, skipped: true, reason: response?.error?.code || 'expected_busy' };
|
|
729
|
+
}
|
|
730
|
+
throw new Error(response?.error?.message || 'mirror.sync failed');
|
|
731
|
+
}
|
|
732
|
+
if (getCurrentProjectId() !== projectId) {
|
|
733
|
+
return { ok: false, skipped: true, reason: 'project_changed' };
|
|
734
|
+
}
|
|
735
|
+
mirrorPrefetchState.lastSuccessAt = Date.now();
|
|
736
|
+
mirrorPrefetchState.lastError = null;
|
|
737
|
+
return { ok: true };
|
|
738
|
+
})();
|
|
739
|
+
|
|
740
|
+
mirrorPrefetchState.inFlight = inFlight;
|
|
741
|
+
try {
|
|
742
|
+
return await inFlight;
|
|
743
|
+
} catch (error) {
|
|
744
|
+
if (isExpectedPrefetchSkip(error)) {
|
|
745
|
+
return { ok: false, skipped: true, reason: error?.code || error?.message || 'expected_busy' };
|
|
746
|
+
}
|
|
747
|
+
mirrorPrefetchState.lastErrorAt = Date.now();
|
|
748
|
+
mirrorPrefetchState.lastError = error?.message || String(error);
|
|
749
|
+
return { ok: false, error };
|
|
750
|
+
} finally {
|
|
751
|
+
if (mirrorPrefetchState.inFlight === inFlight) {
|
|
752
|
+
mirrorPrefetchState.inFlight = null;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function settleMirrorPrefetchBeforeRun() {
|
|
758
|
+
syncMirrorPrefetchStateForProject();
|
|
759
|
+
if (mirrorPrefetchState.timer) {
|
|
760
|
+
window.clearTimeout(mirrorPrefetchState.timer);
|
|
761
|
+
mirrorPrefetchState.timer = null;
|
|
762
|
+
}
|
|
763
|
+
if (!mirrorPrefetchState.inFlight) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
await mirrorPrefetchState.inFlight;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
if (!isExpectedPrefetchSkip(error)) {
|
|
770
|
+
mirrorPrefetchState.lastErrorAt = Date.now();
|
|
771
|
+
mirrorPrefetchState.lastError = error?.message || String(error);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function isMirrorReusable(mirrorStatus) {
|
|
777
|
+
const health = mirrorHealth?.classifyMirrorHealth?.(mirrorStatus) || { reusable: false };
|
|
778
|
+
return health.reusable === true;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function resolveWarmRunStart(taskContext = {}) {
|
|
782
|
+
const focusFiles = taskContext.focusFiles || [];
|
|
783
|
+
const mode = taskContext.mode || getState()?.mode;
|
|
784
|
+
const mirrorStatus = await getMirrorFreshness();
|
|
785
|
+
const otWarmStart = otWarmMirrorController.canUseOtWarmStart({
|
|
786
|
+
enabled: isExperimentalOtEnabled(),
|
|
787
|
+
focusFiles,
|
|
788
|
+
mirrorStatus
|
|
789
|
+
});
|
|
790
|
+
if (otWarmStart.ok) {
|
|
791
|
+
return {
|
|
792
|
+
useExistingMirror: true,
|
|
793
|
+
warmStart: true,
|
|
794
|
+
otWarmStart: true,
|
|
795
|
+
reason: 'ot_focus_fresh',
|
|
796
|
+
mirrorStatus,
|
|
797
|
+
focusFiles: otWarmStart.focusFiles,
|
|
798
|
+
project: {
|
|
799
|
+
capabilities: {
|
|
800
|
+
fullProjectSnapshot: false,
|
|
801
|
+
method: 'ot-warm-mirror'
|
|
802
|
+
},
|
|
803
|
+
files: []
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
if (!mirrorStatus?.exists || !isMirrorReusable(mirrorStatus)) {
|
|
808
|
+
return { useExistingMirror: false, reason: 'mirror_not_fresh', mirrorStatus };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
let overlayProject = null;
|
|
812
|
+
try {
|
|
813
|
+
overlayProject = await callPageBridge('getProjectSnapshot', {
|
|
814
|
+
force: true,
|
|
815
|
+
maxAgeMs: 0,
|
|
816
|
+
preferLightweight: true,
|
|
817
|
+
allowZipFallback: false,
|
|
818
|
+
allowEditorNavigation: false,
|
|
819
|
+
restrictToRequestedPathsOnly: true,
|
|
820
|
+
requireFullProject: false,
|
|
821
|
+
includeBinaryFiles: false,
|
|
822
|
+
focusFiles
|
|
823
|
+
});
|
|
824
|
+
} catch (error) {
|
|
825
|
+
return { useExistingMirror: false, reason: 'overlay_probe_failed', mirrorStatus };
|
|
826
|
+
}
|
|
827
|
+
const fileOverlays = buildSnapshotFileOverlays(overlayProject, focusFiles);
|
|
828
|
+
if (!fileOverlays.length) {
|
|
829
|
+
return { useExistingMirror: false, reason: 'missing_current_overlay', mirrorStatus };
|
|
830
|
+
}
|
|
831
|
+
if (focusFiles.length && !focusFiles.every(path => {
|
|
832
|
+
const normalized = normalizeSnapshotPath(path);
|
|
833
|
+
return fileOverlays.some(file => normalizeSnapshotPath(file.path) === normalized);
|
|
834
|
+
})) {
|
|
835
|
+
return { useExistingMirror: false, reason: 'missing_focus_overlay', mirrorStatus };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return {
|
|
839
|
+
useExistingMirror: true,
|
|
840
|
+
warmStart: true,
|
|
841
|
+
reason: mode === 'ask' ? 'warm_whole_project_ask' : 'warm_whole_project',
|
|
842
|
+
mirrorStatus,
|
|
843
|
+
fileOverlays,
|
|
844
|
+
project: overlayProject
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function prepareMirrorStaleRetry({ project = {}, focusFiles = [] } = {}) {
|
|
849
|
+
throwIfRunCancellationRequested();
|
|
850
|
+
appendLog(formatProjectSnapshotUserLog(project));
|
|
851
|
+
const snapshotWarnings = getProjectSnapshotWarnings(project);
|
|
852
|
+
const focusedPartialSnapshot = runController.canUseFocusedPartialSnapshot({
|
|
853
|
+
project,
|
|
854
|
+
snapshotWarnings,
|
|
855
|
+
focusFiles,
|
|
856
|
+
isUsableProjectFileContent: window.CodexOverleafProjectFiles.isUsableProjectFileContent
|
|
857
|
+
});
|
|
858
|
+
if (snapshotWarnings.blocking.length && !focusedPartialSnapshot) {
|
|
859
|
+
for (const warning of snapshotWarnings.blocking) {
|
|
860
|
+
appendLog(tx(`Cannot continue: ${formatProjectSnapshotWarning(warning)}`, `无法继续:${formatProjectSnapshotWarning(warning)}`));
|
|
861
|
+
}
|
|
862
|
+
appendCompletionReport({
|
|
863
|
+
conclusion: tr('warmMirrorStaleBlockedConclusion'),
|
|
864
|
+
status: 'blocked',
|
|
865
|
+
operations: [],
|
|
866
|
+
applyResults: [],
|
|
867
|
+
nextStep: tr('warmMirrorStaleBlockedNextStep')
|
|
868
|
+
});
|
|
869
|
+
finishRunView(tr('warmMirrorStaleBlockedTitle'), 'failed');
|
|
870
|
+
return { ok: false };
|
|
871
|
+
}
|
|
872
|
+
if (focusedPartialSnapshot) {
|
|
873
|
+
appendRunEvent({
|
|
874
|
+
title: tx(
|
|
875
|
+
`Only selected context files were read: ${focusFiles.join(', ')}. This retry will read and write only these files.`,
|
|
876
|
+
`只读到你选择的上下文文件:${focusFiles.join(', ')};本次重试将只基于这些文件运行和写回。`
|
|
877
|
+
),
|
|
878
|
+
status: 'completed'
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
ok: true,
|
|
883
|
+
project,
|
|
884
|
+
restrictToFocusFiles: runController.shouldRestrictWritebackToFocus({ focusFiles })
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function syncMirrorPrefetchStateForProject() {
|
|
889
|
+
const projectId = getCurrentProjectId();
|
|
890
|
+
const projectChanged = Boolean(mirrorPrefetchState.projectId && mirrorPrefetchState.projectId !== projectId);
|
|
891
|
+
syncCustomInstructionsEditorForProject(projectId, { force: projectChanged });
|
|
892
|
+
syncOtWarmMirrorStateForProject();
|
|
893
|
+
if (!mirrorPrefetchState.projectId || !projectChanged) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (mirrorPrefetchState.timer) {
|
|
897
|
+
window.clearTimeout(mirrorPrefetchState.timer);
|
|
898
|
+
}
|
|
899
|
+
mirrorPrefetchState = {
|
|
900
|
+
inFlight: mirrorPrefetchState.inFlight,
|
|
901
|
+
lastSuccessAt: 0,
|
|
902
|
+
lastErrorAt: 0,
|
|
903
|
+
lastError: null,
|
|
904
|
+
timer: null,
|
|
905
|
+
projectId
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function syncOtWarmMirrorStateForProject() {
|
|
910
|
+
const projectId = getCurrentProjectId();
|
|
911
|
+
syncExperimentalOtToggleForProject(projectId);
|
|
912
|
+
if (!otWarmMirrorProjectId) {
|
|
913
|
+
otWarmMirrorProjectId = projectId;
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (otWarmMirrorProjectId === projectId) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
920
|
+
otWarmMirrorProjectId = projectId;
|
|
921
|
+
otSyncRequestId++;
|
|
922
|
+
callPageBridge('stopOtObserver', {}).then(response => {
|
|
923
|
+
if (!isSuccessfulOtBridgeResponse(response)) {
|
|
924
|
+
updateOtStatusDisplay('unavailable');
|
|
925
|
+
}
|
|
926
|
+
return syncOtWarmMirrorController();
|
|
927
|
+
}).catch(error => {
|
|
928
|
+
updateOtStatusDisplay('unavailable');
|
|
929
|
+
appendPlainLog(tx(`Experimental OT warm mirror unavailable: ${error.message}`, `实验性 OT 预热镜像不可用:${error.message}`));
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
getCurrentOtStatus,
|
|
935
|
+
getOtWarmMirrorState,
|
|
936
|
+
getLastExperimentalOtProjectId,
|
|
937
|
+
clearMirrorPrefetchTimer,
|
|
938
|
+
releaseOtWarmMirrorProject,
|
|
939
|
+
isExperimentalOtEnabled,
|
|
940
|
+
setExperimentalOtEnabledForProject,
|
|
941
|
+
normalizeExperimentalOtByProject,
|
|
942
|
+
syncExperimentalOtToggleForProject,
|
|
943
|
+
handleExperimentalOtToggleClick,
|
|
944
|
+
handleExperimentalOtToggleKeydown,
|
|
945
|
+
handleExperimentalOtToggleChange,
|
|
946
|
+
syncOtWarmMirrorController,
|
|
947
|
+
readOtBridgeStatus,
|
|
948
|
+
clearOtEventPolling,
|
|
949
|
+
canPollOtWarmMirror,
|
|
950
|
+
pauseOtWarmMirror,
|
|
951
|
+
resumeOtWarmMirror,
|
|
952
|
+
updateOtStatusDisplay,
|
|
953
|
+
formatOtStatusLabel,
|
|
954
|
+
updateExperimentalOtMenuStatus,
|
|
955
|
+
normalizeOtStatus,
|
|
956
|
+
scheduleMirrorPrefetch,
|
|
957
|
+
settleMirrorPrefetchBeforeRun,
|
|
958
|
+
resolveWarmRunStart,
|
|
959
|
+
prepareMirrorStaleRetry
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
window.CodexOverleafOtWarmMirror = { create };
|
|
964
|
+
})();
|