codex-overleaf-link 2.3.5 → 2.3.6
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 +31 -24
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +4 -0
- package/extension/src/backgroundUpdateCoordinator.js +88 -4
- package/extension/src/content/contentRuntime.js +24 -7
- package/extension/src/content/diagnosticsController.js +13 -3
- package/extension/src/content/diagnosticsPanel.js +1 -1
- package/extension/src/content/generated/content.bundle.js +848 -245
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +2 -0
- package/extension/src/content/otWarmMirror.js +95 -65
- package/extension/src/content/otWarmMirrorController.js +102 -1
- package/extension/src/content/panelMaintenance.js +26 -15
- package/extension/src/content/providerSettingsCoordinator.js +1 -0
- package/extension/src/content/runFailureNotice.js +104 -0
- package/extension/src/content/runScrollLayout.js +124 -0
- package/extension/src/content/runTimelineView.js +36 -10
- package/extension/src/content/settingsPanel.js +28 -11
- package/extension/src/content/updateNotice.js +24 -4
- package/extension/src/page/overleafRealtimeObserver.js +116 -24
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/i18n.js +77 -37
- package/extension/src/shared/managedUpdateProjection.js +1 -1
- package/extension/src/shared/otText.js +0 -1
- package/extension/src/shared/sessionState.js +17 -21
- package/extension/src/shared/sessionTitle.js +120 -0
- package/extension/src/shared/updateRuntimeIdentity.js +32 -0
- package/extension/styles/panel.css +73 -14
- package/native-host/src/mirrorWorkspace.js +20 -1
- package/native-host/src/releaseProxy.js +177 -0
- package/native-host/src/releaseTransport.js +154 -0
- package/native-host/src/updateManager.js +40 -39
- package/package.json +1 -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": "15c3e103532c84dea57aeac33e02ec665fbf7518543e14daac2459c434b3e5d5",
|
|
6
|
+
"outputDigest": "5e1dba4bfcf4c595835312deabefa5ecdd2ee7c6d98f29d1b2bd9711ad5b195c",
|
|
7
|
+
"byteLength": 2375784,
|
|
8
8
|
"inputs": [
|
|
9
9
|
"extension/entries/content-entry.mjs",
|
|
10
10
|
"extension/src/content/activeTurnControl.js",
|
|
@@ -44,10 +44,12 @@
|
|
|
44
44
|
"extension/src/content/recentProjects.js",
|
|
45
45
|
"extension/src/content/reviewHunks.js",
|
|
46
46
|
"extension/src/content/runController.js",
|
|
47
|
+
"extension/src/content/runFailureNotice.js",
|
|
47
48
|
"extension/src/content/runGuidanceController.js",
|
|
48
49
|
"extension/src/content/runGuidanceView.js",
|
|
49
50
|
"extension/src/content/runQueueScheduler.js",
|
|
50
51
|
"extension/src/content/runResultActions.js",
|
|
52
|
+
"extension/src/content/runScrollLayout.js",
|
|
51
53
|
"extension/src/content/runSettlementPersistence.js",
|
|
52
54
|
"extension/src/content/runTimelineView.js",
|
|
53
55
|
"extension/src/content/scopedPersistenceCoordinator.js",
|
|
@@ -90,6 +92,7 @@
|
|
|
90
92
|
"extension/src/shared/scopedPersistenceTransaction.js",
|
|
91
93
|
"extension/src/shared/sensitiveScan.js",
|
|
92
94
|
"extension/src/shared/sessionState.js",
|
|
95
|
+
"extension/src/shared/sessionTitle.js",
|
|
93
96
|
"extension/src/shared/settlementFacts.js",
|
|
94
97
|
"extension/src/shared/staleGuard.js",
|
|
95
98
|
"extension/src/shared/storageDb.js",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
} = deps;
|
|
51
51
|
|
|
52
52
|
let currentOtStatus = 'off';
|
|
53
|
+
let otTogglePending = false;
|
|
53
54
|
let otSyncRequestId = 0;
|
|
54
55
|
let otWarmMirrorProjectId = '';
|
|
55
56
|
let lastExperimentalOtProjectId = '';
|
|
@@ -61,7 +62,10 @@
|
|
|
61
62
|
patchQueue: [],
|
|
62
63
|
lastStatus: 'off',
|
|
63
64
|
lastPatchAt: 0,
|
|
64
|
-
lastErrorCode: ''
|
|
65
|
+
lastErrorCode: '',
|
|
66
|
+
failureActive: false,
|
|
67
|
+
lastFailure: null,
|
|
68
|
+
suspended: false
|
|
65
69
|
};
|
|
66
70
|
let mirrorPrefetchState = {
|
|
67
71
|
inFlight: null,
|
|
@@ -159,14 +163,14 @@
|
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
async function handleExperimentalOtToggleClick(event) {
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// is the user's intended target. preventDefault then rolls the visual
|
|
165
|
-
// state back (synchronously, when dispatch completes), and the confirm
|
|
166
|
-
// flow applies the target only after approval.
|
|
166
|
+
// Capture the pre-activated target, then let cancelled click dispatch
|
|
167
|
+
// restore the old checkbox value before projecting the approved state.
|
|
167
168
|
const clicked = event?.currentTarget || event?.target;
|
|
168
169
|
const targetEnabled = clicked && typeof clicked.checked === 'boolean' ? clicked.checked : undefined;
|
|
170
|
+
const projectId = getCurrentProjectId();
|
|
169
171
|
event.preventDefault();
|
|
172
|
+
await new Promise(resolve => window.setTimeout(resolve, 0));
|
|
173
|
+
if (projectId !== getCurrentProjectId()) return;
|
|
170
174
|
await toggleExperimentalOtCheckbox(targetEnabled);
|
|
171
175
|
}
|
|
172
176
|
|
|
@@ -180,9 +184,12 @@
|
|
|
180
184
|
|
|
181
185
|
async function toggleExperimentalOtCheckbox(targetEnabled) {
|
|
182
186
|
const checkbox = getPanel()?.querySelector('[data-experimental-ot]');
|
|
183
|
-
if (!checkbox) {
|
|
187
|
+
if (!checkbox || otTogglePending) {
|
|
184
188
|
return;
|
|
185
189
|
}
|
|
190
|
+
const projectId = getCurrentProjectId();
|
|
191
|
+
otTogglePending = true;
|
|
192
|
+
try {
|
|
186
193
|
// Explicit target from the click interceptor wins; the keyboard/legacy
|
|
187
194
|
// path (no argument, un-flipped checkbox) falls back to inversion.
|
|
188
195
|
const nextEnabled = typeof targetEnabled === 'boolean' ? targetEnabled : !checkbox.checked;
|
|
@@ -200,11 +207,15 @@
|
|
|
200
207
|
} else {
|
|
201
208
|
closeDiagnosticsMenu();
|
|
202
209
|
}
|
|
210
|
+
if (projectId !== getCurrentProjectId() || checkbox !== getPanel()?.querySelector('[data-experimental-ot]')) return;
|
|
203
211
|
checkbox.checked = nextEnabled;
|
|
204
212
|
handleExperimentalOtToggleChange({ currentTarget: checkbox });
|
|
205
213
|
if (nextEnabled) {
|
|
206
214
|
showPluginToast(tr('experimentalOtEnabledToast'), { status: 'info' });
|
|
207
215
|
}
|
|
216
|
+
} finally {
|
|
217
|
+
otTogglePending = false;
|
|
218
|
+
}
|
|
208
219
|
}
|
|
209
220
|
|
|
210
221
|
function handleExperimentalOtToggleChange(event) {
|
|
@@ -213,6 +224,7 @@
|
|
|
213
224
|
return;
|
|
214
225
|
}
|
|
215
226
|
const projectId = getCurrentProjectId();
|
|
227
|
+
otWarmMirrorState.failureActive = false;
|
|
216
228
|
lastExperimentalOtProjectId = projectId;
|
|
217
229
|
setExperimentalOtEnabledForProject(projectId, checkbox.checked);
|
|
218
230
|
updateExperimentalOtToggleControl(checkbox.checked);
|
|
@@ -230,7 +242,7 @@
|
|
|
230
242
|
updateOtStatusDisplay(checkbox.checked ? (currentOtStatus === 'off' ? 'starting' : currentOtStatus) : 'off');
|
|
231
243
|
syncOtWarmMirrorController().catch(error => {
|
|
232
244
|
updateOtStatusDisplay('unavailable');
|
|
233
|
-
appendPlainLog(tx(`
|
|
245
|
+
appendPlainLog(tx(`OT warm mirror unavailable: ${error.message}`, `OT 预热镜像不可用:${error.message}`));
|
|
234
246
|
});
|
|
235
247
|
saveStateSoon();
|
|
236
248
|
}
|
|
@@ -252,6 +264,7 @@
|
|
|
252
264
|
otWarmMirrorProjectId = projectId;
|
|
253
265
|
ensureOtWarmMirrorStateProject(projectId);
|
|
254
266
|
if (enabled) {
|
|
267
|
+
otWarmMirrorState.suspended = false;
|
|
255
268
|
const pause = otWarmMirrorController?.shouldPauseOtWarmMirror?.({ running: Boolean(getCurrentRunView()) }) || { pause: false };
|
|
256
269
|
if (pause.pause) {
|
|
257
270
|
clearOtEventPolling({ clearPatchQueue: true });
|
|
@@ -263,9 +276,12 @@
|
|
|
263
276
|
} else {
|
|
264
277
|
clearOtEventPolling({ clearPatchQueue: true });
|
|
265
278
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
: await callPageBridge('stopOtObserver', {});
|
|
279
|
+
let response;
|
|
280
|
+
try {
|
|
281
|
+
response = enabled ? await callPageBridge('startOtObserver', { projectId }) : await callPageBridge('stopOtObserver', {});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
response = { ok: false, error: otWarmMirrorController.normalizeFailureCode(error?.code || error?.message, 'ot_bridge_unavailable') };
|
|
284
|
+
}
|
|
269
285
|
if (!isCurrentOtSync(requestId, projectId) || getCurrentProjectId() !== projectId) {
|
|
270
286
|
if (enabled) {
|
|
271
287
|
await handleStaleOtStartResponse(projectId, requestId);
|
|
@@ -282,7 +298,7 @@
|
|
|
282
298
|
}
|
|
283
299
|
const status = readOtBridgeStatus(response);
|
|
284
300
|
if (!isSuccessfulOtBridgeResponse(response) || status === 'unavailable') {
|
|
285
|
-
handleFailedOtStart(projectId, requestId);
|
|
301
|
+
await handleFailedOtStart(projectId, requestId, response);
|
|
286
302
|
return response;
|
|
287
303
|
}
|
|
288
304
|
const pauseAfterStart = otWarmMirrorController?.shouldPauseOtWarmMirror?.({ running: Boolean(getCurrentRunView()) }) || { pause: false };
|
|
@@ -290,6 +306,7 @@
|
|
|
290
306
|
await pauseOtWarmMirror(pauseAfterStart.reason || 'paused');
|
|
291
307
|
return response;
|
|
292
308
|
}
|
|
309
|
+
otWarmMirrorState.failureActive = false;
|
|
293
310
|
updateOtStatusDisplay(status);
|
|
294
311
|
scheduleOtEventPolling(projectId, { immediate: true });
|
|
295
312
|
return response;
|
|
@@ -314,11 +331,13 @@
|
|
|
314
331
|
}
|
|
315
332
|
}
|
|
316
333
|
|
|
317
|
-
function handleFailedOtStart(projectId, requestId) {
|
|
334
|
+
async function handleFailedOtStart(projectId, requestId, response) {
|
|
318
335
|
if (!isCurrentOtSync(requestId, projectId) || getCurrentProjectId() !== projectId) {
|
|
319
336
|
return;
|
|
320
337
|
}
|
|
321
338
|
clearOtEventPolling({ clearPatchQueue: true });
|
|
339
|
+
recordOtFailure(readOtBridgeErrorCode(response, 'ot_start_failed'), 'start');
|
|
340
|
+
otWarmMirrorState.suspended = true;
|
|
322
341
|
setExperimentalOtEnabledForProject(projectId, false);
|
|
323
342
|
const experimentalOtCheckbox = getPanel()?.querySelector('[data-experimental-ot]');
|
|
324
343
|
if (experimentalOtCheckbox) {
|
|
@@ -326,13 +345,14 @@
|
|
|
326
345
|
}
|
|
327
346
|
updateOtStatusDisplay('unavailable');
|
|
328
347
|
saveStateSoon();
|
|
348
|
+
try { await callPageBridge('stopOtObserver', { projectId }); } catch (_error) { /* preserve the original failure */ }
|
|
329
349
|
}
|
|
330
350
|
|
|
331
351
|
function readOtBridgeStatus(response) {
|
|
332
352
|
if (!response || response.ok === false) {
|
|
333
353
|
return 'unavailable';
|
|
334
354
|
}
|
|
335
|
-
return response.
|
|
355
|
+
return response.state || response.status || response.result?.state || response.result?.status || 'unavailable';
|
|
336
356
|
}
|
|
337
357
|
|
|
338
358
|
function ensureOtWarmMirrorStateProject(projectId) {
|
|
@@ -343,6 +363,9 @@
|
|
|
343
363
|
otWarmMirrorState.projectId = projectId;
|
|
344
364
|
otWarmMirrorState.lastPatchAt = 0;
|
|
345
365
|
otWarmMirrorState.lastErrorCode = '';
|
|
366
|
+
otWarmMirrorState.failureActive = false;
|
|
367
|
+
otWarmMirrorState.lastFailure = null;
|
|
368
|
+
otWarmMirrorState.suspended = false;
|
|
346
369
|
otWarmMirrorState.lastStatus = currentOtStatus;
|
|
347
370
|
}
|
|
348
371
|
|
|
@@ -386,7 +409,8 @@
|
|
|
386
409
|
&& otWarmMirrorController?.buildPatchFilesRequest
|
|
387
410
|
&& isExperimentalOtEnabledForProject(projectId)
|
|
388
411
|
&& otWarmMirrorProjectId === projectId
|
|
389
|
-
&& getCurrentProjectId() === projectId
|
|
412
|
+
&& getCurrentProjectId() === projectId
|
|
413
|
+
&& !otWarmMirrorState.suspended);
|
|
390
414
|
}
|
|
391
415
|
|
|
392
416
|
async function pollOtEvents(projectId = otWarmMirrorState.projectId || getCurrentProjectId()) {
|
|
@@ -409,11 +433,12 @@
|
|
|
409
433
|
return { ok: false, skipped: true, reason: 'project_changed' };
|
|
410
434
|
}
|
|
411
435
|
if (!isSuccessfulOtBridgeResponse(statusResponse)) {
|
|
412
|
-
|
|
436
|
+
recordOtFailure(readOtBridgeErrorCode(statusResponse, 'get_ot_status_failed'), 'observe');
|
|
413
437
|
updateOtStatusDisplay('unavailable');
|
|
414
438
|
return statusResponse;
|
|
415
439
|
}
|
|
416
440
|
const status = readOtBridgeStatus(statusResponse);
|
|
441
|
+
if (['unavailable', 'inconsistent'].includes(status)) recordOtFailure(readOtBridgeErrorCode(statusResponse, 'ot_observer_unavailable'), 'observe');
|
|
417
442
|
if (status) {
|
|
418
443
|
updateOtStatusDisplay(status);
|
|
419
444
|
}
|
|
@@ -423,7 +448,7 @@
|
|
|
423
448
|
return { ok: false, skipped: true, reason: 'project_changed' };
|
|
424
449
|
}
|
|
425
450
|
if (!isSuccessfulOtBridgeResponse(drainResponse)) {
|
|
426
|
-
|
|
451
|
+
recordOtFailure(readOtBridgeErrorCode(drainResponse, 'drain_ot_events_failed'), 'observe');
|
|
427
452
|
updateOtStatusDisplay('unavailable');
|
|
428
453
|
return drainResponse;
|
|
429
454
|
}
|
|
@@ -433,7 +458,8 @@
|
|
|
433
458
|
}
|
|
434
459
|
return drainResponse;
|
|
435
460
|
} catch (error) {
|
|
436
|
-
|
|
461
|
+
if (!canPollOtWarmMirror(projectId)) return { ok: false, skipped: true, reason: 'project_changed' };
|
|
462
|
+
recordOtFailure(error?.code || error?.message || 'ot_poll_failed', 'observe');
|
|
437
463
|
updateOtStatusDisplay('unavailable');
|
|
438
464
|
return { ok: false, error };
|
|
439
465
|
} finally {
|
|
@@ -465,12 +491,28 @@
|
|
|
465
491
|
return typeof value === 'string' ? value : fallback;
|
|
466
492
|
}
|
|
467
493
|
|
|
494
|
+
function recordOtFailure(code, phase) {
|
|
495
|
+
code = otWarmMirrorController.normalizeFailureCode(code);
|
|
496
|
+
otWarmMirrorState.lastErrorCode = code;
|
|
497
|
+
otWarmMirrorState.failureActive = true;
|
|
498
|
+
otWarmMirrorState.lastFailure = { code, phase, at: Date.now() };
|
|
499
|
+
}
|
|
500
|
+
|
|
468
501
|
function queueOtPatchEvents(events, projectId = getCurrentProjectId()) {
|
|
469
502
|
if (!Array.isArray(events) || !events.length || !canPollOtWarmMirror(projectId)) {
|
|
470
503
|
return;
|
|
471
504
|
}
|
|
472
505
|
ensureOtWarmMirrorStateProject(projectId);
|
|
473
|
-
otWarmMirrorState.patchQueue
|
|
506
|
+
const queued = otWarmMirrorController.queuePatchEvents(otWarmMirrorState.patchQueue, events);
|
|
507
|
+
if (!queued.ok) {
|
|
508
|
+
recordOtFailure(queued.reason, 'queue');
|
|
509
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
510
|
+
otWarmMirrorState.suspended = true;
|
|
511
|
+
updateOtStatusDisplay('inconsistent');
|
|
512
|
+
void callPageBridge('stopOtObserver', { projectId }).catch(() => {});
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
otWarmMirrorState.patchQueue = queued.queue;
|
|
474
516
|
scheduleOtPatchFlush(projectId);
|
|
475
517
|
}
|
|
476
518
|
|
|
@@ -510,11 +552,18 @@
|
|
|
510
552
|
return { ok: false, skipped: true, reason: pause.reason || 'paused' };
|
|
511
553
|
}
|
|
512
554
|
|
|
513
|
-
|
|
514
|
-
const batch = otWarmMirrorState.patchQueue.splice(0, maxBatch);
|
|
555
|
+
let batch = otWarmMirrorController.takePatchBatch(otWarmMirrorState.patchQueue);
|
|
515
556
|
if (!batch.length) {
|
|
516
557
|
return { ok: true, skipped: true, reason: 'empty_batch' };
|
|
517
558
|
}
|
|
559
|
+
const fresh = otWarmMirrorController.queuePatchEvents([], batch);
|
|
560
|
+
if (!fresh.ok) {
|
|
561
|
+
recordOtFailure(fresh.reason, 'queue');
|
|
562
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
563
|
+
updateOtStatusDisplay('inconsistent');
|
|
564
|
+
return { ok: false, skipped: true, reason: fresh.reason };
|
|
565
|
+
}
|
|
566
|
+
batch = fresh.queue;
|
|
518
567
|
|
|
519
568
|
otWarmMirrorState.flushing = true;
|
|
520
569
|
try {
|
|
@@ -530,46 +579,37 @@
|
|
|
530
579
|
}
|
|
531
580
|
|
|
532
581
|
const response = await sendBackgroundNative(request);
|
|
582
|
+
if (!canPollOtWarmMirror(projectId)) return { ok: false, skipped: true, reason: 'project_changed' };
|
|
533
583
|
if (!response?.ok) {
|
|
534
|
-
|
|
584
|
+
recordOtFailure(response?.error?.code || 'mirror_patch_failed', 'flush');
|
|
535
585
|
updateOtStatusDisplay('inconsistent');
|
|
536
586
|
return response;
|
|
537
587
|
}
|
|
538
588
|
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
? Number(result.skippedCount)
|
|
543
|
-
: skippedFiles.length;
|
|
544
|
-
if (skippedFiles.length || skippedCount > 0) {
|
|
545
|
-
otWarmMirrorState.lastErrorCode = 'mirror_patch_skipped';
|
|
546
|
-
updateOtStatusDisplay('inconsistent');
|
|
547
|
-
return response;
|
|
548
|
-
}
|
|
549
|
-
const appliedFiles = Array.isArray(result?.appliedFiles) ? result.appliedFiles : null;
|
|
550
|
-
const appliedCount = Number.isFinite(Number(result?.appliedCount))
|
|
551
|
-
? Number(result.appliedCount)
|
|
552
|
-
: NaN;
|
|
553
|
-
if (!appliedFiles || !Number.isFinite(appliedCount) || appliedCount <= 0 || appliedFiles.length !== appliedCount) {
|
|
554
|
-
otWarmMirrorState.lastErrorCode = 'mirror_patch_invalid_result';
|
|
589
|
+
const receipt = otWarmMirrorController.validatePatchReceipt(batch, response.result);
|
|
590
|
+
if (!receipt.ok) {
|
|
591
|
+
recordOtFailure(receipt.reason, 'flush');
|
|
555
592
|
updateOtStatusDisplay('inconsistent');
|
|
556
593
|
return response;
|
|
557
594
|
}
|
|
558
595
|
|
|
559
596
|
otWarmMirrorState.lastPatchAt = Date.now();
|
|
560
597
|
otWarmMirrorState.lastErrorCode = '';
|
|
561
|
-
|
|
598
|
+
otWarmMirrorState.failureActive = false;
|
|
599
|
+
if (canPollOtWarmMirror(projectId) && !getCurrentRunView()) {
|
|
562
600
|
updateOtStatusDisplay('observing');
|
|
563
601
|
}
|
|
564
602
|
return response;
|
|
565
603
|
} catch (error) {
|
|
566
|
-
|
|
604
|
+
if (!canPollOtWarmMirror(projectId)) return { ok: false, skipped: true, reason: 'project_changed' };
|
|
605
|
+
recordOtFailure(error?.code || 'mirror_patch_failed', 'flush');
|
|
567
606
|
updateOtStatusDisplay('inconsistent');
|
|
568
607
|
return { ok: false, error };
|
|
569
608
|
} finally {
|
|
570
609
|
otWarmMirrorState.flushing = false;
|
|
571
|
-
|
|
572
|
-
|
|
610
|
+
const nextProjectId = otWarmMirrorState.projectId;
|
|
611
|
+
if (otWarmMirrorState.patchQueue.length && canPollOtWarmMirror(nextProjectId) && !getCurrentRunView()) {
|
|
612
|
+
scheduleOtPatchFlush(nextProjectId, { immediate: true });
|
|
573
613
|
}
|
|
574
614
|
}
|
|
575
615
|
}
|
|
@@ -612,12 +652,13 @@
|
|
|
612
652
|
}
|
|
613
653
|
|
|
614
654
|
function updateOtStatusDisplay(status = currentOtStatus) {
|
|
615
|
-
const normalized = normalizeOtStatus(status);
|
|
655
|
+
const normalized = otWarmMirrorState.failureActive && status === 'observing' ? 'inconsistent' : normalizeOtStatus(status);
|
|
616
656
|
currentOtStatus = normalized;
|
|
617
657
|
otWarmMirrorState.lastStatus = normalized;
|
|
618
658
|
const statusElement = getPanel()?.querySelector('[data-ot-status]');
|
|
619
659
|
if (statusElement) {
|
|
620
|
-
const label = formatOtStatusLabel(normalized)
|
|
660
|
+
const label = formatOtStatusLabel(normalized) + (otWarmMirrorState.failureActive
|
|
661
|
+
? ': ' + tr(otWarmMirrorController.getFailureMessageKey(otWarmMirrorState.lastErrorCode)) : '');
|
|
621
662
|
statusElement.textContent = formatOtToggleStatusText(label);
|
|
622
663
|
statusElement.dataset.otStatus = normalized;
|
|
623
664
|
statusElement.title = formatOtToggleStatusText(label);
|
|
@@ -781,38 +822,24 @@
|
|
|
781
822
|
}
|
|
782
823
|
|
|
783
824
|
async function resolveWarmRunStart(taskContext = {}) {
|
|
825
|
+
const projectId = getCurrentProjectId();
|
|
784
826
|
const focusFiles = taskContext.focusFiles || [];
|
|
785
827
|
const mode = taskContext.mode || getState()?.mode;
|
|
786
828
|
const mirrorStatus = await getMirrorFreshness();
|
|
829
|
+
if (getCurrentProjectId() !== projectId) return { useExistingMirror: false, reason: 'project_changed' };
|
|
787
830
|
const otWarmStart = otWarmMirrorController.canUseOtWarmStart({
|
|
788
|
-
enabled: isExperimentalOtEnabled(),
|
|
831
|
+
enabled: isExperimentalOtEnabled() && !otWarmMirrorState.failureActive,
|
|
789
832
|
focusFiles,
|
|
790
833
|
mirrorStatus
|
|
791
834
|
});
|
|
792
|
-
if (otWarmStart.ok) {
|
|
793
|
-
return {
|
|
794
|
-
useExistingMirror: true,
|
|
795
|
-
warmStart: true,
|
|
796
|
-
otWarmStart: true,
|
|
797
|
-
reason: 'ot_focus_fresh',
|
|
798
|
-
mirrorStatus,
|
|
799
|
-
focusFiles: otWarmStart.focusFiles,
|
|
800
|
-
project: {
|
|
801
|
-
capabilities: {
|
|
802
|
-
fullProjectSnapshot: false,
|
|
803
|
-
method: 'ot-warm-mirror'
|
|
804
|
-
},
|
|
805
|
-
files: []
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
if (!mirrorStatus?.exists || !isMirrorReusable(mirrorStatus)) {
|
|
835
|
+
if (!mirrorStatus?.exists || (!otWarmStart.ok && !isMirrorReusable(mirrorStatus))) {
|
|
810
836
|
return { useExistingMirror: false, reason: 'mirror_not_fresh', mirrorStatus };
|
|
811
837
|
}
|
|
812
838
|
|
|
813
839
|
let overlayProject = null;
|
|
814
840
|
try {
|
|
815
841
|
overlayProject = await callPageBridge('getProjectSnapshot', {
|
|
842
|
+
projectId,
|
|
816
843
|
force: true,
|
|
817
844
|
maxAgeMs: 0,
|
|
818
845
|
preferLightweight: true,
|
|
@@ -826,6 +853,7 @@
|
|
|
826
853
|
} catch (error) {
|
|
827
854
|
return { useExistingMirror: false, reason: 'overlay_probe_failed', mirrorStatus };
|
|
828
855
|
}
|
|
856
|
+
if (getCurrentProjectId() !== projectId) return { useExistingMirror: false, reason: 'project_changed' };
|
|
829
857
|
const fileOverlays = buildSnapshotFileOverlays(overlayProject, focusFiles);
|
|
830
858
|
if (!fileOverlays.length) {
|
|
831
859
|
return { useExistingMirror: false, reason: 'missing_current_overlay', mirrorStatus };
|
|
@@ -840,7 +868,9 @@
|
|
|
840
868
|
return {
|
|
841
869
|
useExistingMirror: true,
|
|
842
870
|
warmStart: true,
|
|
843
|
-
|
|
871
|
+
otWarmStart: otWarmStart.ok,
|
|
872
|
+
focusFiles,
|
|
873
|
+
reason: otWarmStart.ok ? 'ot_focus_fresh' : mode === 'ask' ? 'warm_whole_project_ask' : 'warm_whole_project',
|
|
844
874
|
mirrorStatus,
|
|
845
875
|
fileOverlays,
|
|
846
876
|
project: overlayProject
|
|
@@ -928,7 +958,7 @@
|
|
|
928
958
|
return syncOtWarmMirrorController();
|
|
929
959
|
}).catch(error => {
|
|
930
960
|
updateOtStatusDisplay('unavailable');
|
|
931
|
-
appendPlainLog(tx(`
|
|
961
|
+
appendPlainLog(tx(`OT warm mirror unavailable: ${error.message}`, `OT 预热镜像不可用:${error.message}`));
|
|
932
962
|
});
|
|
933
963
|
}
|
|
934
964
|
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
const OT_POLL_INTERVAL_MS = 1000;
|
|
11
11
|
const OT_PATCH_DEBOUNCE_MS = 500;
|
|
12
12
|
const OT_MAX_PATCH_BATCH = 25;
|
|
13
|
+
const OT_FRESHNESS_MAX_AGE_MS = 30000;
|
|
14
|
+
const OT_MAX_FILE_BYTES = 1024 * 1024;
|
|
15
|
+
const OT_MAX_PENDING_EVENTS = 64;
|
|
16
|
+
const OT_MAX_PENDING_BYTES = 4 * 1024 * 1024;
|
|
17
|
+
const OT_MAX_BATCH_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
|
|
13
19
|
const PAUSE_STATES = new Set(['running', 'writing', 'undoing', 'compiling']);
|
|
14
20
|
|
|
15
21
|
function buildPatchFilesRequest({ projectId, events, nativeCompatibility } = {}) {
|
|
@@ -67,13 +73,14 @@
|
|
|
67
73
|
return { pause: false };
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
function canUseOtWarmStart({ enabled, focusFiles, mirrorStatus } = {}) {
|
|
76
|
+
function canUseOtWarmStart({ enabled, focusFiles, mirrorStatus, now = Date.now() } = {}) {
|
|
71
77
|
if (!enabled) {
|
|
72
78
|
return { ok: false, reason: 'disabled' };
|
|
73
79
|
}
|
|
74
80
|
if (!mirrorStatus || mirrorStatus.exists !== true) {
|
|
75
81
|
return { ok: false, reason: 'mirror_missing' };
|
|
76
82
|
}
|
|
83
|
+
if (mirrorStatus.dirty === true) return { ok: false, reason: 'mirror_dirty' };
|
|
77
84
|
|
|
78
85
|
const normalizedFocusFiles = normalizePaths(focusFiles);
|
|
79
86
|
if (!normalizedFocusFiles.length) {
|
|
@@ -85,6 +92,8 @@
|
|
|
85
92
|
if (file?.state !== 'fresh') {
|
|
86
93
|
continue;
|
|
87
94
|
}
|
|
95
|
+
const age = now - Date.parse(file.observedAt ?? file.lastPatchAt ?? '');
|
|
96
|
+
if (!Number.isFinite(age) || age < 0 || age > OT_FRESHNESS_MAX_AGE_MS) continue;
|
|
88
97
|
const filePath = normalizePath(file.path);
|
|
89
98
|
if (filePath) {
|
|
90
99
|
freshFiles.add(filePath);
|
|
@@ -102,6 +111,88 @@
|
|
|
102
111
|
return { ok: false, reason: 'missing_ot_fresh_focus' };
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
function textBytes(value) {
|
|
115
|
+
const text = String(value || '');
|
|
116
|
+
return encoder ? encoder.encode(text).byteLength : text.length * 3;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function patchBytes(event) { return textBytes(event.nextContent) + textBytes(event.path) + 512; }
|
|
120
|
+
|
|
121
|
+
function queuePatchEvents(queue, events, now = Date.now()) {
|
|
122
|
+
if (!Array.isArray(events) || events.length > OT_MAX_PENDING_EVENTS) return { ok: false, reason: 'ot_queue_limit' };
|
|
123
|
+
const next = Array.isArray(queue) ? queue.slice() : [];
|
|
124
|
+
let bytes = next.reduce((sum, event) => sum + patchBytes(event), 0);
|
|
125
|
+
if (next.length > OT_MAX_PENDING_EVENTS || bytes > OT_MAX_PENDING_BYTES) return { ok: false, reason: 'ot_queue_limit' };
|
|
126
|
+
for (const input of events) {
|
|
127
|
+
const path = normalizePath(input?.path);
|
|
128
|
+
const time = typeof input?.observedAt === 'number' ? input.observedAt : Date.parse(input?.observedAt || '');
|
|
129
|
+
if (!isSafeRelativePath(path) || path.length > 1024 || typeof input?.nextContent !== 'string' ||
|
|
130
|
+
typeof input.baseHash !== 'string' || !input.baseHash || input.baseHash.length > 128) {
|
|
131
|
+
return { ok: false, reason: 'ot_event_invalid' };
|
|
132
|
+
}
|
|
133
|
+
if (!Number.isFinite(time) || now - time < 0 || now - time > OT_FRESHNESS_MAX_AGE_MS) return { ok: false, reason: 'ot_event_expired' };
|
|
134
|
+
if (textBytes(input.nextContent) > OT_MAX_FILE_BYTES) return { ok: false, reason: 'ot_file_limit' };
|
|
135
|
+
const event = { path, baseHash: input.baseHash, nextContent: input.nextContent,
|
|
136
|
+
nextHash: typeof input.nextHash === 'string' && input.nextHash.length <= 128 ? input.nextHash : '',
|
|
137
|
+
observedAt: new Date(time).toISOString() };
|
|
138
|
+
if (isPrimitiveMetadata(input.observedVersion) && String(input.observedVersion).length <= 128) event.observedVersion = input.observedVersion;
|
|
139
|
+
const tail = next[next.length - 1];
|
|
140
|
+
if (tail?.path === path && tail.nextHash && tail.nextHash === event.baseHash) {
|
|
141
|
+
event.baseHash = tail.baseHash;
|
|
142
|
+
bytes -= patchBytes(next.pop());
|
|
143
|
+
}
|
|
144
|
+
if (event.nextHash !== event.baseHash) { next.push(event); bytes += patchBytes(event); }
|
|
145
|
+
if (next.length > OT_MAX_PENDING_EVENTS || bytes > OT_MAX_PENDING_BYTES) return { ok: false, reason: 'ot_queue_limit' };
|
|
146
|
+
}
|
|
147
|
+
return { ok: true, queue: next, bytes };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function takePatchBatch(queue) {
|
|
151
|
+
let bytes = 0;
|
|
152
|
+
let count = 0;
|
|
153
|
+
while (count < Math.min(queue.length, OT_MAX_PATCH_BATCH)) {
|
|
154
|
+
const size = patchBytes(queue[count]);
|
|
155
|
+
if (bytes + size > OT_MAX_BATCH_BYTES) break;
|
|
156
|
+
bytes += size;
|
|
157
|
+
count += 1;
|
|
158
|
+
}
|
|
159
|
+
return queue.splice(0, count);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function validatePatchReceipt(events, result = {}, now = Date.now()) {
|
|
163
|
+
if ((Array.isArray(result.skippedFiles) && result.skippedFiles.length) || Number(result.skippedCount || 0) > 0) {
|
|
164
|
+
return { ok: false, reason: 'mirror_patch_skipped' };
|
|
165
|
+
}
|
|
166
|
+
if (!events.length || Number(result.appliedCount) !== events.length || !Array.isArray(result.appliedFiles) ||
|
|
167
|
+
result.appliedFiles.length !== events.length || !result.appliedFiles.every((file, index) =>
|
|
168
|
+
normalizePath(file?.path) === events[index].path && (!events[index].nextHash || file.hash === events[index].nextHash))) {
|
|
169
|
+
return { ok: false, reason: 'mirror_patch_invalid_result' };
|
|
170
|
+
}
|
|
171
|
+
if (events.some(event => { const time = Date.parse(event.observedAt);
|
|
172
|
+
return !Number.isFinite(time) || time > now || now - time > OT_FRESHNESS_MAX_AGE_MS;
|
|
173
|
+
})) return { ok: false, reason: 'ot_event_expired' };
|
|
174
|
+
return { ok: true };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeFailureCode(value, fallback = 'ot_error') {
|
|
178
|
+
const code = typeof value === 'string' ? value : value?.code;
|
|
179
|
+
if (/^[a-z][a-z0-9_]{0,79}$/.test(code || '')) return code;
|
|
180
|
+
if (/page bridge|capability|bridge timed out/i.test(String(code || ''))) return 'ot_bridge_unavailable';
|
|
181
|
+
return fallback;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getFailureMessageKey(value) {
|
|
185
|
+
const code = normalizeFailureCode(value);
|
|
186
|
+
if (/bridge|ot_status|ot_events/.test(code)) return 'otFailureBridge';
|
|
187
|
+
if (/missing_ot_text|observer_create|observer_unavailable/.test(code)) return 'otFailureObserver';
|
|
188
|
+
if (code === 'ot_file_limit') return 'otFailureFileLimit';
|
|
189
|
+
if (code === 'ot_queue_limit') return 'otFailureQueueLimit';
|
|
190
|
+
if (code === 'ot_event_expired') return 'otFailureExpired';
|
|
191
|
+
if (/editor|active_path|document/.test(code)) return 'otFailureEditor';
|
|
192
|
+
if (/mirror|hash|baseline/.test(code)) return 'otFailureMirror';
|
|
193
|
+
return 'otFailureGeneric';
|
|
194
|
+
}
|
|
195
|
+
|
|
105
196
|
function normalizePath(value) {
|
|
106
197
|
return String(value ?? '')
|
|
107
198
|
.trim()
|
|
@@ -165,6 +256,16 @@
|
|
|
165
256
|
OT_POLL_INTERVAL_MS,
|
|
166
257
|
OT_PATCH_DEBOUNCE_MS,
|
|
167
258
|
OT_MAX_PATCH_BATCH,
|
|
259
|
+
OT_FRESHNESS_MAX_AGE_MS,
|
|
260
|
+
OT_MAX_FILE_BYTES,
|
|
261
|
+
OT_MAX_PENDING_EVENTS,
|
|
262
|
+
OT_MAX_PENDING_BYTES,
|
|
263
|
+
OT_MAX_BATCH_BYTES,
|
|
264
|
+
queuePatchEvents,
|
|
265
|
+
takePatchBatch,
|
|
266
|
+
validatePatchReceipt,
|
|
267
|
+
normalizeFailureCode,
|
|
268
|
+
getFailureMessageKey,
|
|
168
269
|
buildPatchFilesRequest,
|
|
169
270
|
canUseOtWarmStart,
|
|
170
271
|
normalizePath,
|
|
@@ -129,29 +129,40 @@
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// --- History & storage (v1.7.5): usage summary + clear-all in Settings ---
|
|
132
|
+
let storageUsageRequestId = 0;
|
|
132
133
|
function refreshStorageUsageSummary() {
|
|
133
134
|
const node = getSettingsPanelInstance()?.container?.querySelector('[data-storage-usage]');
|
|
134
135
|
if (!node) {
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
137
|
-
const
|
|
138
|
-
const
|
|
138
|
+
const requestId = ++storageUsageRequestId;
|
|
139
|
+
const projectId = getCurrentProjectId?.();
|
|
140
|
+
const isCurrent = () => requestId === storageUsageRequestId && projectId === getCurrentProjectId?.()
|
|
141
|
+
&& getSettingsPanelInstance()?.container?.querySelector('[data-storage-usage]') === node;
|
|
142
|
+
const current = getState();
|
|
143
|
+
const sessionCount = Array.isArray(current?.sessions) ? current.sessions.length : 0;
|
|
144
|
+
const runCount = Array.isArray(current?.runs) ? current.runs.length : 0;
|
|
139
145
|
const counts = tx(
|
|
140
|
-
`${sessionCount} session(s) · ${runCount}
|
|
141
|
-
|
|
146
|
+
`${sessionCount} loaded session(s) in this project · ${runCount} run(s) in the active session`,
|
|
147
|
+
`当前项目已加载 ${sessionCount} 个会话 · 当前会话 ${runCount} 轮运行`
|
|
142
148
|
);
|
|
149
|
+
// Dynamic text belongs to this renderer, not the loading-label translator.
|
|
150
|
+
node.removeAttribute?.('data-i18n');
|
|
143
151
|
node.textContent = counts;
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
const unavailable = () => {
|
|
153
|
+
if (isCurrent()) node.textContent = tx(`${counts} · Site storage estimate unavailable`, `${counts} · 本站点容量暂无法估算`);
|
|
154
|
+
};
|
|
155
|
+
let timer;
|
|
156
|
+
const estimate = Promise.resolve().then(() => navigator.storage?.estimate?.());
|
|
157
|
+
const deadline = new Promise(resolve => { timer = setTimeout(() => resolve(null), 2000); });
|
|
158
|
+
return Promise.race([estimate, deadline]).then(info => {
|
|
159
|
+
if (!isCurrent()) return;
|
|
160
|
+
const usage = info?.usage;
|
|
161
|
+
if (!Number.isFinite(usage) || usage < 0) { unavailable(); return; }
|
|
162
|
+
const mb = usage / (1024 * 1024);
|
|
163
|
+
const size = usage === 0 ? '0 KB' : mb >= 1 ? `${mb.toFixed(1)} MB` : `${Math.max(1, Math.round(usage / 1024))} KB`;
|
|
164
|
+
node.textContent = tx(`${counts} · Site total ~${size}`, `${counts} · 本站点总占用约 ${size}`);
|
|
165
|
+
}).catch(unavailable).finally(() => clearTimeout(timer));
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
async function clearAllHistoryWithConfirm() {
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
return {
|
|
60
60
|
open: () => open(instance),
|
|
61
61
|
refreshSummary: () => refreshSummary(instance),
|
|
62
|
+
renderSummary: () => updateSettingsSummary(instance),
|
|
62
63
|
ensureLoaded: () => ensureLoaded(instance),
|
|
63
64
|
getRunSelection: providerId => instance.loaded
|
|
64
65
|
? Profiles.buildRunSelection(instance.catalog, providerId || instance.getSelectedProviderId())
|