codex-overleaf-link 2.1.4 → 2.2.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 +27 -23
- package/extension/bootstrap/background.js +14 -1
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/runtime-manifest.json +11 -0
- package/extension/src/background.js +507 -8
- package/extension/src/backgroundUpdateCoordinator.js +284 -19
- package/extension/src/content/activeTurnControl.js +179 -0
- package/extension/src/content/composerPanel.js +19 -6
- package/extension/src/content/contentRuntime.js +335 -317
- package/extension/src/content/diagnosticsPanel.js +132 -1
- package/extension/src/content/markdownDomRenderer.js +451 -0
- package/extension/src/content/markdownText.js +86 -7
- package/extension/src/content/mathText.js +49 -4
- package/extension/src/content/modelPicker.js +14 -5
- package/extension/src/content/nativeChannel.js +7 -1
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/panelRenderer.js +112 -12
- package/extension/src/content/pendingInputView.js +106 -0
- package/extension/src/content/projectProviderSelection.js +68 -0
- package/extension/src/content/projectSessionCleanup.js +104 -0
- package/extension/src/content/providerSettingsCoordinator.js +170 -22
- package/extension/src/content/providerSettingsDialog.js +2 -2
- package/extension/src/content/recentProjects.js +24 -13
- package/extension/src/content/runController.js +2 -2
- package/extension/src/content/runGuidanceController.js +70 -0
- package/extension/src/content/runGuidanceView.js +48 -0
- package/extension/src/content/runQueueScheduler.js +260 -0
- package/extension/src/content/runTimelineView.js +33 -80
- package/extension/src/content/sessionManager.js +2 -2
- package/extension/src/content/sessionPersistence.js +62 -0
- package/extension/src/content/settingsPanel.js +1 -1
- package/extension/src/content/updateNotice.js +80 -1
- package/extension/src/page/writebackRouter.js +3 -9
- package/extension/src/shared/compatibility.js +2 -1
- package/extension/src/shared/i18n.js +54 -0
- package/extension/src/shared/providerProfiles.js +3 -2
- package/extension/src/shared/runInputQueue.js +253 -0
- package/extension/src/shared/sessionState.js +45 -22
- package/extension/src/shared/storageDb.js +39 -44
- package/extension/src/shared/storageMigration.js +123 -3
- package/extension/src/shared/storageRunActions.js +34 -1
- package/extension/src/shared/updateConsent.js +15 -4
- package/extension/styles/panel.css +370 -23
- package/extension/vendor/katex/katex.min.css +1 -1
- package/extension/vendor/markdown-it/LICENSE +22 -0
- package/extension/vendor/markdown-it/SOURCE.json +7 -0
- package/extension/vendor/markdown-it/VERSION +1 -0
- package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
- package/native-host/src/anthropicBridgeRequest.js +3 -3
- package/native-host/src/anthropicBridgeResponse.js +58 -17
- package/native-host/src/anthropicMessagesBridge.js +156 -23
- package/native-host/src/chatBridgeRequest.js +1 -1
- package/native-host/src/chatBridgeResponse.js +12 -8
- package/native-host/src/chatCompletionsBridge.js +22 -40
- package/native-host/src/codexPromptAssembly.js +3 -1
- package/native-host/src/codexProviderLaunch.js +6 -3
- package/native-host/src/codexSessionRunner.js +66 -66
- package/native-host/src/codexSessionTiming.js +45 -0
- package/native-host/src/diffEngine.js +10 -5
- package/native-host/src/index.js +58 -3
- package/native-host/src/managedInstall.js +45 -7
- package/native-host/src/providerProfile.js +2 -2
- package/native-host/src/providerStreamLifecycle.js +100 -0
- package/native-host/src/responsesPassthroughBridge.js +636 -0
- package/native-host/src/taskRunnerRuntime.js +81 -2
- package/native-host/src/updateArchive.js +4 -3
- package/native-host/src/updateManager.js +306 -19
- package/native-host/src/updateTrust.js +1 -1
- package/package.json +3 -1
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
'mirror.status',
|
|
37
37
|
'codex.models',
|
|
38
38
|
'codex.run',
|
|
39
|
+
'codex.steer',
|
|
39
40
|
'codex.providers.list',
|
|
40
41
|
'codex.providers.test',
|
|
41
42
|
'codex.providers.test.cancel',
|
|
@@ -113,6 +114,10 @@
|
|
|
113
114
|
const ContextTray = window.CodexOverleafContextTray;
|
|
114
115
|
const LocalSkillsPanel = window.CodexOverleafLocalSkillsPanel;
|
|
115
116
|
const runController = window.CodexOverleafRunController;
|
|
117
|
+
const RunInputQueue = window.CodexOverleafRunInputQueue;
|
|
118
|
+
const RunQueueScheduler = window.CodexOverleafRunQueueScheduler;
|
|
119
|
+
const ActiveTurnControl = window.CodexOverleafActiveTurnControl;
|
|
120
|
+
const PendingInputView = window.CodexOverleafPendingInputView;
|
|
116
121
|
const mirrorHealth = window.CodexOverleafMirrorHealth;
|
|
117
122
|
const otWarmMirrorController = window.CodexOverleafOtWarmMirrorController;
|
|
118
123
|
const PanelRenderer = window.CodexOverleafPanelRenderer;
|
|
@@ -172,7 +177,8 @@
|
|
|
172
177
|
tx,
|
|
173
178
|
callPageBridge,
|
|
174
179
|
getCurrentProjectReferenceFiles,
|
|
175
|
-
showPluginToast
|
|
180
|
+
showPluginToast,
|
|
181
|
+
recordRenderingDiagnostic: detail => appendTechnicalEvent({ type: 'rendering.diagnostic', detail })
|
|
176
182
|
});
|
|
177
183
|
const {
|
|
178
184
|
renderMarkdownInlineText,
|
|
@@ -597,6 +603,14 @@
|
|
|
597
603
|
updateModelDisplay
|
|
598
604
|
} = modelPicker;
|
|
599
605
|
|
|
606
|
+
const projectProviderSelection = window.CodexOverleafProjectProviderSelection.create({
|
|
607
|
+
tx, tr, getRunningSessionIds, showToast: showPluginToast, showConfirm: showPluginConfirm,
|
|
608
|
+
getState: () => state, setState: next => { state = next; }, getPanel: () => panel,
|
|
609
|
+
readSelectedModel: readSelectedModelInput, readSelectedSpeed: readSelectedSpeedInput,
|
|
610
|
+
getRunSelection: providerId => providerSettingsCoordinator.getRunSelection(providerId),
|
|
611
|
+
normalizeState: normalizePanelState, applyStateToPanel
|
|
612
|
+
});
|
|
613
|
+
|
|
600
614
|
const providerSettingsCoordinator = window.CodexOverleafProviderSettingsCoordinator.create({
|
|
601
615
|
tx,
|
|
602
616
|
sendBackgroundNative,
|
|
@@ -604,7 +618,8 @@
|
|
|
604
618
|
window,
|
|
605
619
|
getSettingsPanelInstance: () => settingsPanelInstance,
|
|
606
620
|
getSelectedModel: () => state?.model || '', getSelectedProviderId: () => state?.providerId || 'builtin',
|
|
607
|
-
setSelectedProviderId:
|
|
621
|
+
setSelectedProviderId: projectProviderSelection.commit,
|
|
622
|
+
confirmProviderSwitch: projectProviderSelection.confirmSwitch,
|
|
608
623
|
refreshModelOptions: loadModelOptions, persistInputs: persistPanelInputs
|
|
609
624
|
});
|
|
610
625
|
// v1.8.1 D3: the dashboard was a one-shot render — another tab finishing a
|
|
@@ -767,6 +782,9 @@
|
|
|
767
782
|
let state = null;
|
|
768
783
|
let storageKey = LEGACY_STORAGE_KEY;
|
|
769
784
|
let currentRunView = null;
|
|
785
|
+
let runQueueScheduler = null;
|
|
786
|
+
let runInputCoordinator = null;
|
|
787
|
+
const activeTurnControl = ActiveTurnControl.create({ chrome });
|
|
770
788
|
let saveStateTimer = null;
|
|
771
789
|
// Two-phase saveState scheduling. `saveStateInFlight` flips to true while
|
|
772
790
|
// an async saveState() is actually writing; `saveStateRunAfterFlight` is
|
|
@@ -787,7 +805,7 @@
|
|
|
787
805
|
let renderedSlashCommands = new Map();
|
|
788
806
|
let runCancellationRequested = false;
|
|
789
807
|
let runCancellationController = null;
|
|
790
|
-
let
|
|
808
|
+
let pluginConfirmController = null;
|
|
791
809
|
const updateIdleClient = root.CodexOverleafUpdateIdle?.create({
|
|
792
810
|
document,
|
|
793
811
|
getBusyState: () => ({
|
|
@@ -795,7 +813,7 @@
|
|
|
795
813
|
cancelling: runCancellationRequested,
|
|
796
814
|
storage: saveStateInFlight || Boolean(saveStateTimer),
|
|
797
815
|
reviewAction: trackedChangeInFlight.size > 0,
|
|
798
|
-
dialog: Boolean(
|
|
816
|
+
dialog: Boolean(pluginConfirmController?.isOpen())
|
|
799
817
|
}),
|
|
800
818
|
checkSaved: () => callPageBridge('waitForSaveState', { deadlineMs: 1500, pollIntervalMs: 100, requirePositiveSignal: true, allowQuietEditorFallback: true, quietFallbackMs: 1000 }),
|
|
801
819
|
onApplyingChange: applying => { if (panel) panel.dataset.updating = applying ? 'true' : 'false'; },
|
|
@@ -843,7 +861,7 @@
|
|
|
843
861
|
return;
|
|
844
862
|
}
|
|
845
863
|
if (nativeChannel.shouldHandleNativeEvent(message)) {
|
|
846
|
-
appendNativeEvent(message.event);
|
|
864
|
+
appendNativeEvent(message.event, message.journalSeq);
|
|
847
865
|
}
|
|
848
866
|
});
|
|
849
867
|
|
|
@@ -862,6 +880,15 @@
|
|
|
862
880
|
async function init() {
|
|
863
881
|
storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
|
|
864
882
|
state = normalizePanelState(await loadStoredState(), { restoreRunningRuns: true });
|
|
883
|
+
initializeRunQueueScheduler();
|
|
884
|
+
recoverInterruptedRunJournals()
|
|
885
|
+
.then(() => applyStateToPanel())
|
|
886
|
+
.catch(() => {});
|
|
887
|
+
setTimeout(() => {
|
|
888
|
+
recoverInterruptedRunJournals()
|
|
889
|
+
.then(() => applyStateToPanel())
|
|
890
|
+
.catch(() => {});
|
|
891
|
+
}, 900);
|
|
865
892
|
ensurePanelOpen();
|
|
866
893
|
root.CodexOverleafUpdateNotice?.mount?.(panel?.panelEl || panel, {
|
|
867
894
|
getLocale: () => state?.locale || 'en'
|
|
@@ -1338,7 +1365,8 @@
|
|
|
1338
1365
|
setElementTitleAndAria('[data-context-refresh]', tr('refreshFileList'), tr('refreshFileList'));
|
|
1339
1366
|
setElementTitleAndAria('[data-reasoning]', tr('reasoningLabel'), tr('reasoningLabel'));
|
|
1340
1367
|
setElementTitleAndAria('[data-speed]', tr('speedLabel'), tr('speedLabel'));
|
|
1341
|
-
setElementTitleAndAria('[data-run]', currentRunView ? tr('
|
|
1368
|
+
setElementTitleAndAria('[data-run]', currentRunView ? tr('queueNextInput') : tr('send'), currentRunView ? tr('queueNextInput') : tr('send'));
|
|
1369
|
+
setElementTitleAndAria('[data-stop-run]', tr('cancelRun'), tr('cancelRun'));
|
|
1342
1370
|
|
|
1343
1371
|
const actions = panel.querySelector('.codex-vscode-head-actions');
|
|
1344
1372
|
if (actions) {
|
|
@@ -1422,27 +1450,15 @@
|
|
|
1422
1450
|
return '';
|
|
1423
1451
|
}
|
|
1424
1452
|
|
|
1425
|
-
function installDiagnosticsDismiss() {
|
|
1426
|
-
// DiagnosticsPanel installs its outside-click dismiss listener during create().
|
|
1427
|
-
}
|
|
1453
|
+
function installDiagnosticsDismiss() {}
|
|
1428
1454
|
|
|
1429
|
-
function closeDiagnosticsResult() {
|
|
1430
|
-
DiagnosticsPanel.closeResult(diagnosticsPanelInstance);
|
|
1431
|
-
}
|
|
1455
|
+
function closeDiagnosticsResult() { DiagnosticsPanel.closeResult(diagnosticsPanelInstance); }
|
|
1432
1456
|
|
|
1433
|
-
function showDiagnosticsLoading(title, subtitle = tr('diagnosticsLoading')) {
|
|
1434
|
-
DiagnosticsPanel.showLoading(diagnosticsPanelInstance, title, subtitle);
|
|
1435
|
-
}
|
|
1457
|
+
function showDiagnosticsLoading(title, subtitle = tr('diagnosticsLoading')) { DiagnosticsPanel.showLoading(diagnosticsPanelInstance, title, subtitle); }
|
|
1436
1458
|
|
|
1437
|
-
function showDiagnosticsResult(result = {}) {
|
|
1438
|
-
DiagnosticsPanel.showResult(diagnosticsPanelInstance, result);
|
|
1439
|
-
}
|
|
1459
|
+
function showDiagnosticsResult(result = {}) { DiagnosticsPanel.showResult(diagnosticsPanelInstance, result); }
|
|
1440
1460
|
|
|
1441
|
-
|
|
1442
|
-
// diagnostics trigger's status dot.
|
|
1443
|
-
function setDiagnosticsHealth(health) {
|
|
1444
|
-
DiagnosticsPanel.updateStatus(diagnosticsPanelInstance, { health });
|
|
1445
|
-
}
|
|
1461
|
+
function setDiagnosticsHealth(health) { DiagnosticsPanel.updateStatus(diagnosticsPanelInstance, { health }); }
|
|
1446
1462
|
|
|
1447
1463
|
function installContextDismiss() {
|
|
1448
1464
|
return contextTrayController.installContextDismiss();
|
|
@@ -1646,7 +1662,7 @@
|
|
|
1646
1662
|
return contextTrayController.resetContextProject();
|
|
1647
1663
|
}
|
|
1648
1664
|
|
|
1649
|
-
async function runTask() {
|
|
1665
|
+
async function runTask(options = {}) {
|
|
1650
1666
|
// Barrier on a still-flying background mirror refresh from the previous
|
|
1651
1667
|
// turn (v1.7.5) BEFORE startRunView creates the new run view: the mirror's
|
|
1652
1668
|
// wrap-up events must land in the run that produced them, not the new
|
|
@@ -1665,7 +1681,8 @@
|
|
|
1665
1681
|
const submittedAttachments = getComposerAttachmentsForRun();
|
|
1666
1682
|
const submittedSkillInvocation = getComposerSkillInvocationForRun();
|
|
1667
1683
|
|
|
1668
|
-
const
|
|
1684
|
+
const queuedInput = options.queuedInput || null;
|
|
1685
|
+
const task = String(queuedInput?.text || state.task || '').trim();
|
|
1669
1686
|
if (!task) {
|
|
1670
1687
|
appendLog(tx('Enter a task first.', '请先输入任务。'));
|
|
1671
1688
|
return;
|
|
@@ -1681,9 +1698,13 @@
|
|
|
1681
1698
|
reasoningEffort: state.reasoningEffort,
|
|
1682
1699
|
speedTier: state.speedTier,
|
|
1683
1700
|
attachments: submittedAttachments,
|
|
1684
|
-
skillInvocation: submittedSkillInvocation
|
|
1701
|
+
skillInvocation: submittedSkillInvocation,
|
|
1702
|
+
queueItemId: queuedInput?.id || ''
|
|
1685
1703
|
});
|
|
1686
1704
|
const runSessionId = currentRunView.sessionId;
|
|
1705
|
+
if (queuedInput?.id) {
|
|
1706
|
+
await runQueueScheduler?.markExecuting(runSessionId, queuedInput.id, currentRunView.recordId);
|
|
1707
|
+
}
|
|
1687
1708
|
let runAuditDraft = null;
|
|
1688
1709
|
try {
|
|
1689
1710
|
runAuditDraft = await awaitRunStep(createAuditDraftForRun({
|
|
@@ -1693,10 +1714,11 @@
|
|
|
1693
1714
|
focusFiles: getActiveFocusFiles()
|
|
1694
1715
|
}));
|
|
1695
1716
|
announceCrossTabRunStart();
|
|
1696
|
-
//
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
clearTaskComposer({ keepAttachments:
|
|
1717
|
+
// Binary attachments belong to this submitted turn. The run record has
|
|
1718
|
+
// already captured their snapshots, so clear them from the composer
|
|
1719
|
+
// together with the submitted text. Persistent @context remains intact.
|
|
1720
|
+
clearTaskComposer({ keepAttachments: false });
|
|
1721
|
+
syncComposerSendAvailability(true);
|
|
1700
1722
|
appendRunEvent({
|
|
1701
1723
|
title: submittedSkillInvocation?.id === 'skill-installer'
|
|
1702
1724
|
? tx('I will use the Codex skill installer for this request.', '我会用 Codex skill installer 处理这个请求。')
|
|
@@ -1727,13 +1749,10 @@
|
|
|
1727
1749
|
return;
|
|
1728
1750
|
}
|
|
1729
1751
|
appendRunEvent({
|
|
1730
|
-
//
|
|
1731
|
-
// reads and writes to these files (shouldRestrictWritebackToFocus),
|
|
1732
|
-
// it does not merely "prioritize" them. The selection persists across
|
|
1733
|
-
// turns until cleared via the + tray.
|
|
1752
|
+
// @context prioritizes these files; complete project runs may still update related files.
|
|
1734
1753
|
title: tx(
|
|
1735
|
-
`This run
|
|
1736
|
-
|
|
1754
|
+
`This run will prioritize: ${formatContextItems(getActiveFocusFiles())} (persists across turns; clear via +)`,
|
|
1755
|
+
`本轮将优先处理:${formatContextItems(getActiveFocusFiles())}(跨轮保留,可在 + 中清除)`
|
|
1737
1756
|
),
|
|
1738
1757
|
status: 'completed'
|
|
1739
1758
|
});
|
|
@@ -1785,7 +1804,7 @@
|
|
|
1785
1804
|
focusFiles,
|
|
1786
1805
|
isUsableProjectFileContent: window.CodexOverleafProjectFiles.isUsableProjectFileContent
|
|
1787
1806
|
});
|
|
1788
|
-
let restrictToFocusFiles = runController.shouldRestrictWritebackToFocus({ focusFiles });
|
|
1807
|
+
let restrictToFocusFiles = runController.shouldRestrictWritebackToFocus({ focusFiles, focusedPartialSnapshot });
|
|
1789
1808
|
if (snapshotWarnings.blocking.length && !warmMirrorReuse.useExistingMirror && !focusedPartialSnapshot) {
|
|
1790
1809
|
for (const warning of snapshotWarnings.blocking) {
|
|
1791
1810
|
appendLog(tx(`Cannot continue: ${formatProjectSnapshotWarning(warning)}`, `无法继续:${formatProjectSnapshotWarning(warning)}`));
|
|
@@ -1827,8 +1846,8 @@
|
|
|
1827
1846
|
if (Array.isArray(warmMirrorReuse.focusFiles) && warmMirrorReuse.focusFiles.length) {
|
|
1828
1847
|
focusFiles = warmMirrorReuse.focusFiles;
|
|
1829
1848
|
}
|
|
1830
|
-
restrictToFocusFiles = true;
|
|
1831
1849
|
}
|
|
1850
|
+
restrictToFocusFiles = runController.shouldRestrictWritebackToFocus({ focusFiles, focusedPartialSnapshot: warmMirrorReuse.partialSnapshot, otWarmStart });
|
|
1832
1851
|
appendRunEvent({
|
|
1833
1852
|
title: warmMirrorReuse.warmStart
|
|
1834
1853
|
? tr('warmMirrorReuseTitle')
|
|
@@ -1912,9 +1931,7 @@
|
|
|
1912
1931
|
await awaitRunStep(settleMirrorPrefetchBeforeRun());
|
|
1913
1932
|
await awaitRunStep(providerSettingsCoordinator.ensureLoaded());
|
|
1914
1933
|
appendRunEvent({ title: tx('Local Codex session is starting.', '本地 Codex session 开始运行。'), status: 'running' });
|
|
1915
|
-
let response = await awaitRunStep(
|
|
1916
|
-
method: 'codex.run',
|
|
1917
|
-
params: buildCodexRunParams({
|
|
1934
|
+
let response = await awaitRunStep(sendTrackedCodexRun(buildCodexRunParams({
|
|
1918
1935
|
task,
|
|
1919
1936
|
project,
|
|
1920
1937
|
useExistingMirror,
|
|
@@ -1929,8 +1946,7 @@
|
|
|
1929
1946
|
attachments: submittedAttachments,
|
|
1930
1947
|
skillInvocation: submittedSkillInvocation,
|
|
1931
1948
|
submittedMode
|
|
1932
|
-
})
|
|
1933
|
-
}));
|
|
1949
|
+
})));
|
|
1934
1950
|
|
|
1935
1951
|
// Handle mirror_stale error by retrying with full sync
|
|
1936
1952
|
if (!response.ok && response.error?.code === 'mirror_stale' && useExistingMirror) {
|
|
@@ -1948,9 +1964,7 @@
|
|
|
1948
1964
|
fileOverlays = null;
|
|
1949
1965
|
otWarmStart = false;
|
|
1950
1966
|
restrictToFocusFiles = staleRetry.restrictToFocusFiles;
|
|
1951
|
-
response = await awaitRunStep(
|
|
1952
|
-
method: 'codex.run',
|
|
1953
|
-
params: buildCodexRunParams({
|
|
1967
|
+
response = await awaitRunStep(sendTrackedCodexRun(buildCodexRunParams({
|
|
1954
1968
|
task,
|
|
1955
1969
|
project,
|
|
1956
1970
|
useExistingMirror: false,
|
|
@@ -1965,8 +1979,7 @@
|
|
|
1965
1979
|
attachments: submittedAttachments,
|
|
1966
1980
|
skillInvocation: submittedSkillInvocation,
|
|
1967
1981
|
submittedMode
|
|
1968
|
-
})
|
|
1969
|
-
}));
|
|
1982
|
+
})));
|
|
1970
1983
|
}
|
|
1971
1984
|
|
|
1972
1985
|
if (!response.ok && response.error?.code === 'thread_resume_failed') {
|
|
@@ -1982,9 +1995,7 @@
|
|
|
1982
1995
|
}
|
|
1983
1996
|
}
|
|
1984
1997
|
appendRunEvent({ title: tx('Creating a new Codex conversation thread.', '正在新建 Codex 会话线程。'), status: 'running' });
|
|
1985
|
-
response = await
|
|
1986
|
-
method: 'codex.run',
|
|
1987
|
-
params: buildCodexRunParams({
|
|
1998
|
+
response = await sendTrackedCodexRun(buildCodexRunParams({
|
|
1988
1999
|
task,
|
|
1989
2000
|
project,
|
|
1990
2001
|
useExistingMirror,
|
|
@@ -1999,8 +2010,7 @@
|
|
|
1999
2010
|
attachments: submittedAttachments,
|
|
2000
2011
|
skillInvocation: submittedSkillInvocation,
|
|
2001
2012
|
submittedMode
|
|
2002
|
-
})
|
|
2003
|
-
});
|
|
2013
|
+
}));
|
|
2004
2014
|
} else {
|
|
2005
2015
|
appendRunEvent({ title: tx('Cancelled: user chose not to create a new thread.', '已取消:用户选择不新建线程。'), status: 'rejected' });
|
|
2006
2016
|
await finalizeAuditRecord(runAuditDraft, { resultStatus: 'rejected' });
|
|
@@ -2161,6 +2171,9 @@
|
|
|
2161
2171
|
mode: submittedMode,
|
|
2162
2172
|
requireReviewing: submittedRequireReviewing
|
|
2163
2173
|
});
|
|
2174
|
+
if (runCancellationRequested) {
|
|
2175
|
+
throw createRunCancellationError();
|
|
2176
|
+
}
|
|
2164
2177
|
await finalizeAuditRecord(runAuditDraft, {
|
|
2165
2178
|
...(syncOutcome.audit || {}),
|
|
2166
2179
|
sensitiveFindings: sensitiveFindings.findings,
|
|
@@ -2309,6 +2322,15 @@
|
|
|
2309
2322
|
});
|
|
2310
2323
|
finishRunView(tx('Task failed', '任务失败'), 'failed');
|
|
2311
2324
|
} finally {
|
|
2325
|
+
const settledView = currentRunView;
|
|
2326
|
+
const settledRecord = settledView ? findRunRecord(settledView.recordId, settledView.sessionId) : null;
|
|
2327
|
+
const settlement = {
|
|
2328
|
+
sessionId: settledView?.sessionId || '',
|
|
2329
|
+
status: settledView?.terminalStatus || settledRecord?.status || 'failed',
|
|
2330
|
+
queueItemId: settledView?.queueItemId || '',
|
|
2331
|
+
requestId: settledView?.nativeRequestId || '',
|
|
2332
|
+
continueQueue: runCancellationRequested
|
|
2333
|
+
};
|
|
2312
2334
|
setRunning(false);
|
|
2313
2335
|
nativeChannel.clearActiveRequest();
|
|
2314
2336
|
stopRunElapsedTick();
|
|
@@ -2318,7 +2340,12 @@
|
|
|
2318
2340
|
if (isExperimentalOtEnabled()) {
|
|
2319
2341
|
await resumeOtWarmMirror('run-settled');
|
|
2320
2342
|
}
|
|
2321
|
-
|
|
2343
|
+
await saveState().catch(() => {});
|
|
2344
|
+
activeTurnControl.release(settlement.requestId);
|
|
2345
|
+
if (settlement.requestId) {
|
|
2346
|
+
activeTurnControl.acknowledge(settlement.requestId).catch(() => {});
|
|
2347
|
+
}
|
|
2348
|
+
runQueueScheduler?.afterSettlement(settlement).catch(() => {});
|
|
2322
2349
|
}
|
|
2323
2350
|
}
|
|
2324
2351
|
|
|
@@ -2557,7 +2584,7 @@
|
|
|
2557
2584
|
return;
|
|
2558
2585
|
}
|
|
2559
2586
|
if (currentRunView) {
|
|
2560
|
-
return;
|
|
2587
|
+
return queueComposerInput();
|
|
2561
2588
|
}
|
|
2562
2589
|
runTask().catch(error => {
|
|
2563
2590
|
setRunning(false);
|
|
@@ -2570,13 +2597,73 @@
|
|
|
2570
2597
|
});
|
|
2571
2598
|
}
|
|
2572
2599
|
|
|
2600
|
+
const runGuidanceController = window.CodexOverleafRunGuidanceController.create({
|
|
2601
|
+
findRunRecord, appendRunEvent, getCurrentRunView: () => currentRunView,
|
|
2602
|
+
renderRunEvent, cssEscape, bumpUnreadIfDetached
|
|
2603
|
+
});
|
|
2604
|
+
|
|
2605
|
+
function initializeRunQueueScheduler() {
|
|
2606
|
+
runInputCoordinator = RunQueueScheduler.createCoordinator({
|
|
2607
|
+
getSession: sessionId => findSessionById(sessionId),
|
|
2608
|
+
getActiveSession: () => getActiveSession(state),
|
|
2609
|
+
setQueue: (sessionId, pendingInputs) => updateSessionById(sessionId, { pendingInputs }),
|
|
2610
|
+
getCurrentRun: () => currentRunView,
|
|
2611
|
+
canStart: sessionId => state?.activeSessionId === sessionId && isProjectEditorRoute(window.location),
|
|
2612
|
+
readInputs: readPanelInputs,
|
|
2613
|
+
getTask: () => panel?.querySelector('[data-task]')?.value || '',
|
|
2614
|
+
hasAttachments: () => getComposerAttachmentsForRun().length > 0,
|
|
2615
|
+
capturePayload: () => ({
|
|
2616
|
+
mode: state.mode,
|
|
2617
|
+
autoRecompile: state.autoRecompile !== false,
|
|
2618
|
+
requireReviewing: state.requireReviewing,
|
|
2619
|
+
focusFiles: getActiveFocusFiles()
|
|
2620
|
+
}),
|
|
2621
|
+
clearTask: () => {
|
|
2622
|
+
panel.querySelector('[data-task]').value = '';
|
|
2623
|
+
updateSessionById(state.activeSessionId, { task: '' });
|
|
2624
|
+
autosizeTaskTextarea();
|
|
2625
|
+
syncComposerSendAvailability();
|
|
2626
|
+
},
|
|
2627
|
+
applyQueuedInput: item => {
|
|
2628
|
+
state.autoRecompile = item.payload?.autoRecompile !== false;
|
|
2629
|
+
state = updateActiveSession(state, {
|
|
2630
|
+
task: item.text,
|
|
2631
|
+
mode: item.payload?.mode || state.mode,
|
|
2632
|
+
requireReviewing: item.payload?.requireReviewing !== false,
|
|
2633
|
+
focusFiles: item.payload?.focusFiles || []
|
|
2634
|
+
});
|
|
2635
|
+
applyStateToPanel();
|
|
2636
|
+
},
|
|
2637
|
+
run: item => runTask({ queuedInput: item }),
|
|
2638
|
+
sendSteer: params => sendBackgroundNative({ method: 'codex.steer', params }),
|
|
2639
|
+
getContainer: () => panel?.querySelector('[data-pending-inputs]'),
|
|
2640
|
+
createView: options => PendingInputView.create(options),
|
|
2641
|
+
appendEvent: (title, status) => appendRunEvent({ title, status }),
|
|
2642
|
+
appendGuidance: runGuidanceController.upsert,
|
|
2643
|
+
removeGuidance: runGuidanceController.remove,
|
|
2644
|
+
toast: (message, status) => showPluginToast(message, { status }),
|
|
2645
|
+
tr,
|
|
2646
|
+
save: () => saveState(),
|
|
2647
|
+
saveSoon: () => saveStateSoon(),
|
|
2648
|
+
randomUUID: () => crypto.randomUUID()
|
|
2649
|
+
});
|
|
2650
|
+
runQueueScheduler = runInputCoordinator.scheduler;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
function queueComposerInput() {
|
|
2654
|
+
return runInputCoordinator?.queueComposerInput();
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
function renderPendingInputs() {
|
|
2658
|
+
return runInputCoordinator?.render();
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2573
2661
|
async function cancelActiveRun() {
|
|
2574
2662
|
if (!currentRunView || runCancellationRequested) {
|
|
2575
2663
|
return;
|
|
2576
2664
|
}
|
|
2577
2665
|
runCancellationRequested = true;
|
|
2578
2666
|
runCancellationController?.abort();
|
|
2579
|
-
cancelActivePageBridgeRequests();
|
|
2580
2667
|
panel.dataset.cancelling = 'true';
|
|
2581
2668
|
setRunning(true);
|
|
2582
2669
|
appendRunEvent({
|
|
@@ -2761,125 +2848,16 @@
|
|
|
2761
2848
|
return confirmed ? 'new' : 'cancel';
|
|
2762
2849
|
}
|
|
2763
2850
|
|
|
2764
|
-
async function showPluginConfirm({
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
if (!panel) {
|
|
2772
|
-
ensurePanelOpen();
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
if (activePluginConfirmResolve) {
|
|
2776
|
-
activePluginConfirmResolve(false);
|
|
2777
|
-
activePluginConfirmResolve = null;
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
|
-
return new Promise(resolve => {
|
|
2781
|
-
let settled = false;
|
|
2782
|
-
const overlay = document.createElement('div');
|
|
2783
|
-
overlay.className = 'codex-plugin-confirm';
|
|
2784
|
-
overlay.setAttribute('data-plugin-confirm', 'true');
|
|
2785
|
-
overlay.setAttribute('role', 'dialog');
|
|
2786
|
-
overlay.setAttribute('aria-modal', 'true');
|
|
2787
|
-
overlay.setAttribute('aria-label', title);
|
|
2788
|
-
|
|
2789
|
-
const card = document.createElement('section');
|
|
2790
|
-
card.className = 'codex-plugin-confirm-card';
|
|
2791
|
-
|
|
2792
|
-
const head = document.createElement('div');
|
|
2793
|
-
head.className = 'codex-plugin-confirm-head';
|
|
2794
|
-
const icon = document.createElement('img');
|
|
2795
|
-
icon.className = 'codex-plugin-confirm-icon';
|
|
2796
|
-
icon.alt = '';
|
|
2797
|
-
icon.setAttribute('aria-hidden', 'true');
|
|
2798
|
-
icon.src = chrome.runtime.getURL('assets/icons/codex-overleaf-dialog-icon.png');
|
|
2799
|
-
const titleWrap = document.createElement('div');
|
|
2800
|
-
const brand = document.createElement('div');
|
|
2801
|
-
brand.className = 'codex-plugin-confirm-brand';
|
|
2802
|
-
brand.textContent = tr('confirmBrand');
|
|
2803
|
-
const titleEl = document.createElement('div');
|
|
2804
|
-
titleEl.className = 'codex-plugin-confirm-title';
|
|
2805
|
-
titleEl.textContent = title;
|
|
2806
|
-
titleWrap.append(brand, titleEl);
|
|
2807
|
-
head.append(icon, titleWrap);
|
|
2808
|
-
|
|
2809
|
-
const body = document.createElement('div');
|
|
2810
|
-
body.className = 'codex-plugin-confirm-body';
|
|
2811
|
-
body.textContent = String(message || '');
|
|
2812
|
-
|
|
2813
|
-
const actions = document.createElement('div');
|
|
2814
|
-
actions.className = 'codex-plugin-confirm-actions';
|
|
2815
|
-
const cancel = document.createElement('button');
|
|
2816
|
-
cancel.type = 'button';
|
|
2817
|
-
cancel.className = 'codex-plugin-confirm-cancel';
|
|
2818
|
-
cancel.textContent = cancelLabel;
|
|
2819
|
-
const confirm = document.createElement('button');
|
|
2820
|
-
confirm.type = 'button';
|
|
2821
|
-
confirm.className = 'codex-plugin-confirm-confirm';
|
|
2822
|
-
if (destructive) {
|
|
2823
|
-
confirm.dataset.destructive = 'true';
|
|
2824
|
-
}
|
|
2825
|
-
confirm.textContent = confirmLabel;
|
|
2826
|
-
actions.append(cancel, confirm);
|
|
2827
|
-
card.append(head, body, actions);
|
|
2828
|
-
overlay.append(card);
|
|
2829
|
-
panel.append(overlay);
|
|
2830
|
-
|
|
2831
|
-
const cleanup = value => {
|
|
2832
|
-
if (settled) {
|
|
2833
|
-
return;
|
|
2834
|
-
}
|
|
2835
|
-
settled = true;
|
|
2836
|
-
activePluginConfirmResolve = null;
|
|
2837
|
-
document.removeEventListener('keydown', onKeydown, true);
|
|
2838
|
-
overlay.remove();
|
|
2839
|
-
resolve(value);
|
|
2840
|
-
};
|
|
2841
|
-
activePluginConfirmResolve = cleanup;
|
|
2842
|
-
const onKeydown = event => {
|
|
2843
|
-
if (event.key === 'Escape') {
|
|
2844
|
-
event.preventDefault();
|
|
2845
|
-
cleanup(false);
|
|
2846
|
-
return;
|
|
2847
|
-
}
|
|
2848
|
-
if (event.key === 'Enter') {
|
|
2849
|
-
event.preventDefault();
|
|
2850
|
-
// Destructive dialogs never treat a bare Enter as consent — the
|
|
2851
|
-
// user must deliberately reach the (red) confirm button. Enter on
|
|
2852
|
-
// a focused button still activates it via the click handler.
|
|
2853
|
-
if (!destructive && event.target !== cancel) {
|
|
2854
|
-
cleanup(true);
|
|
2855
|
-
} else if (event.target === cancel) {
|
|
2856
|
-
cleanup(false);
|
|
2857
|
-
} else if (event.target === confirm) {
|
|
2858
|
-
cleanup(true);
|
|
2859
|
-
}
|
|
2860
|
-
return;
|
|
2861
|
-
}
|
|
2862
|
-
if (event.key === 'Tab') {
|
|
2863
|
-
// aria-modal contract: keep Tab cycling inside the two-button card
|
|
2864
|
-
// instead of drifting onto the Overleaf page behind the overlay.
|
|
2865
|
-
event.preventDefault();
|
|
2866
|
-
const next = event.target === cancel ? confirm : cancel;
|
|
2867
|
-
next.focus();
|
|
2868
|
-
}
|
|
2869
|
-
};
|
|
2870
|
-
|
|
2871
|
-
overlay.addEventListener('click', event => {
|
|
2872
|
-
if (event.target === overlay) {
|
|
2873
|
-
cleanup(false);
|
|
2874
|
-
}
|
|
2851
|
+
async function showPluginConfirm(options = {}) {
|
|
2852
|
+
if (!pluginConfirmController) {
|
|
2853
|
+
pluginConfirmController = PanelRenderer.createConfirmController({
|
|
2854
|
+
getPanel: () => panel,
|
|
2855
|
+
ensurePanelOpen,
|
|
2856
|
+
getIconUrl: () => chrome.runtime.getURL('assets/icons/codex-overleaf-dialog-icon.png'),
|
|
2857
|
+
tr
|
|
2875
2858
|
});
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
document.addEventListener('keydown', onKeydown, true);
|
|
2879
|
-
// Destructive dialogs open on the SAFE button so a stray keystroke
|
|
2880
|
-
// cannot delete anything; benign confirms keep the fast path.
|
|
2881
|
-
(destructive ? cancel : confirm).focus();
|
|
2882
|
-
});
|
|
2859
|
+
}
|
|
2860
|
+
return pluginConfirmController.show(options);
|
|
2883
2861
|
}
|
|
2884
2862
|
|
|
2885
2863
|
function getEnabledCodexOverleafSkillIds() {
|
|
@@ -2933,10 +2911,97 @@
|
|
|
2933
2911
|
});
|
|
2934
2912
|
return {
|
|
2935
2913
|
...params,
|
|
2914
|
+
clientRunId: currentRunView?.recordId || '',
|
|
2915
|
+
clientSessionId: currentRunView?.sessionId || '',
|
|
2936
2916
|
providerSelection: providerSettingsCoordinator.getRunSelection(state?.providerId)
|
|
2937
2917
|
};
|
|
2938
2918
|
}
|
|
2939
2919
|
|
|
2920
|
+
async function sendTrackedCodexRun(params) {
|
|
2921
|
+
const compatibilityGate = await ensureNativeCompatibilityForMethod('codex.run');
|
|
2922
|
+
if (!compatibilityGate.ok) {
|
|
2923
|
+
return compatibilityGate.response;
|
|
2924
|
+
}
|
|
2925
|
+
throwIfCancelledBeforeNativeDispatch('codex.run');
|
|
2926
|
+
const tracked = nativeChannel.sendNativeTracked(attachNativeCompatibilityEvidence({
|
|
2927
|
+
method: 'codex.run',
|
|
2928
|
+
params
|
|
2929
|
+
}, compatibilityGate.compatibility));
|
|
2930
|
+
if (currentRunView) {
|
|
2931
|
+
currentRunView.nativeRequestId = tracked.id;
|
|
2932
|
+
const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
2933
|
+
if (record) {
|
|
2934
|
+
record.nativeRequestId = tracked.id;
|
|
2935
|
+
}
|
|
2936
|
+
activeTurnControl.bind({
|
|
2937
|
+
requestId: tracked.id,
|
|
2938
|
+
projectKey: currentRunView.runProjectId,
|
|
2939
|
+
clientRunId: currentRunView.recordId,
|
|
2940
|
+
sessionId: currentRunView.sessionId
|
|
2941
|
+
});
|
|
2942
|
+
saveStateSoon();
|
|
2943
|
+
}
|
|
2944
|
+
return tracked.promise.finally(() => {
|
|
2945
|
+
if (currentRunView?.nativeRequestId === tracked.id) {
|
|
2946
|
+
currentRunView.activeTurn = null;
|
|
2947
|
+
renderPendingInputs();
|
|
2948
|
+
}
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
async function recoverInterruptedRunJournals() {
|
|
2953
|
+
const projectKey = getCurrentProjectId();
|
|
2954
|
+
const recovery = await activeTurnControl.recoverJournals({
|
|
2955
|
+
projectKey,
|
|
2956
|
+
findSession: findSessionById,
|
|
2957
|
+
getActiveSession: () => getActiveSession(state),
|
|
2958
|
+
createInterruptedRun: journal => ({
|
|
2959
|
+
id: journal.clientRunId,
|
|
2960
|
+
task: journal.task || 'interrupted task',
|
|
2961
|
+
mode: journal.mode || 'ask',
|
|
2962
|
+
model: journal.model || '',
|
|
2963
|
+
status: 'interrupted',
|
|
2964
|
+
statusText: tr('restoredRunStoppedStatus'),
|
|
2965
|
+
runProjectId: projectKey,
|
|
2966
|
+
startedAt: journal.createdAt || new Date().toISOString(),
|
|
2967
|
+
finishedAt: journal.updatedAt || new Date().toISOString(),
|
|
2968
|
+
events: [],
|
|
2969
|
+
attachments: [],
|
|
2970
|
+
undoOperations: [],
|
|
2971
|
+
undoTrackedChanges: [],
|
|
2972
|
+
undoExpectedFiles: []
|
|
2973
|
+
}),
|
|
2974
|
+
mapEvent: (raw, journal) => {
|
|
2975
|
+
const activity = mapAgentEventToActivity(raw, { locale: getLocale() });
|
|
2976
|
+
if (!activity?.visible || activity.kind === 'technical') {
|
|
2977
|
+
return null;
|
|
2978
|
+
}
|
|
2979
|
+
return {
|
|
2980
|
+
kind: activity.kind || 'activity',
|
|
2981
|
+
title: sanitizeAssistantVisibleText(activity.title) || 'Event',
|
|
2982
|
+
status: activity.status || 'running',
|
|
2983
|
+
detail: sanitizeAssistantVisibleValue(activity.detail),
|
|
2984
|
+
timestamp: raw.timestamp || journal.updatedAt || new Date().toISOString(),
|
|
2985
|
+
streamKey: sanitizeAssistantVisibleText(activity.streamKey),
|
|
2986
|
+
streamRole: sanitizeAssistantVisibleText(activity.streamRole),
|
|
2987
|
+
appendText: sanitizeAssistantVisibleText(activity.appendText),
|
|
2988
|
+
replaceText: activity.replaceText
|
|
2989
|
+
};
|
|
2990
|
+
},
|
|
2991
|
+
upsertStream: upsertRunStreamRecordEvent,
|
|
2992
|
+
pauseSessionQueue: (session, reason) => {
|
|
2993
|
+
session.pendingInputs = RunInputQueue.pauseAll(session.pendingInputs || [], reason);
|
|
2994
|
+
},
|
|
2995
|
+
maxEvents: MAX_RUN_EVENTS
|
|
2996
|
+
});
|
|
2997
|
+
if (recovery.changed) {
|
|
2998
|
+
await saveState();
|
|
2999
|
+
for (const requestId of recovery.acknowledgeIds) {
|
|
3000
|
+
await activeTurnControl.acknowledge(requestId).catch(() => false);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
|
|
2940
3005
|
async function createAuditDraftForRun(input = {}) {
|
|
2941
3006
|
const StorageDb = window.CodexOverleafStorageDb;
|
|
2942
3007
|
if (!StorageDb || !AuditRecords) {
|
|
@@ -3126,6 +3191,8 @@
|
|
|
3126
3191
|
}
|
|
3127
3192
|
|
|
3128
3193
|
function appendRunCancelledReport() {
|
|
3194
|
+
const hasQueuedFollowUp = (getActiveSession(state)?.pendingInputs || [])
|
|
3195
|
+
.some(item => item?.status === 'queued');
|
|
3129
3196
|
appendRunEvent({
|
|
3130
3197
|
title: tx('Current Codex task was cancelled.', '已中断当前 Codex 任务。'),
|
|
3131
3198
|
status: 'failed'
|
|
@@ -3135,7 +3202,9 @@
|
|
|
3135
3202
|
status: 'rejected',
|
|
3136
3203
|
operations: [],
|
|
3137
3204
|
applyResults: [],
|
|
3138
|
-
nextStep:
|
|
3205
|
+
nextStep: hasQueuedFollowUp
|
|
3206
|
+
? tx('The first queued follow-up will start next.', '接下来会自动启动队首的后续任务。')
|
|
3207
|
+
: tx('Edit the task and run again.', '可以修改任务后重新运行。')
|
|
3139
3208
|
});
|
|
3140
3209
|
}
|
|
3141
3210
|
|
|
@@ -3148,17 +3217,27 @@
|
|
|
3148
3217
|
task.style.height = `${Math.min(task.scrollHeight, 160)}px`;
|
|
3149
3218
|
}
|
|
3150
3219
|
|
|
3151
|
-
function syncComposerSendAvailability() {
|
|
3220
|
+
function syncComposerSendAvailability(runningOverride) {
|
|
3152
3221
|
const runButton = panel?.querySelector('[data-run]');
|
|
3153
3222
|
if (!runButton) {
|
|
3154
3223
|
return;
|
|
3155
3224
|
}
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3225
|
+
const hasInput = Boolean(String(panel?.querySelector('[data-task]')?.value || '').trim());
|
|
3226
|
+
const running = typeof runningOverride === 'boolean'
|
|
3227
|
+
? runningOverride
|
|
3228
|
+
: Boolean(currentRunView);
|
|
3229
|
+
const action = running && !hasInput ? 'cancel' : 'send';
|
|
3230
|
+
const label = action === 'cancel'
|
|
3231
|
+
? tr('cancelRun')
|
|
3232
|
+
: running
|
|
3233
|
+
? tr('queueNextInput')
|
|
3234
|
+
: tr('send');
|
|
3235
|
+
runButton.dataset.action = action;
|
|
3236
|
+
runButton.disabled = action === 'cancel'
|
|
3237
|
+
? runCancellationRequested
|
|
3238
|
+
: !hasInput;
|
|
3239
|
+
runButton.title = label;
|
|
3240
|
+
runButton.setAttribute('aria-label', label);
|
|
3162
3241
|
}
|
|
3163
3242
|
|
|
3164
3243
|
function handleTaskInputKeydown(event) {
|
|
@@ -3175,6 +3254,7 @@
|
|
|
3175
3254
|
function handleTaskInput() {
|
|
3176
3255
|
autosizeTaskTextarea();
|
|
3177
3256
|
syncComposerSendAvailability();
|
|
3257
|
+
renderPendingInputs();
|
|
3178
3258
|
scheduleMirrorPrefetch({
|
|
3179
3259
|
reason: 'composer-input',
|
|
3180
3260
|
delayMs: mirrorHealth?.PREFETCH_DEBOUNCE_MS || 1200
|
|
@@ -5243,102 +5323,23 @@
|
|
|
5243
5323
|
}
|
|
5244
5324
|
|
|
5245
5325
|
function formatProjectSnapshotUserLog(project) {
|
|
5246
|
-
|
|
5247
|
-
if (!files.length) {
|
|
5248
|
-
return tx(
|
|
5249
|
-
'No Overleaf project files were read yet. Make sure the project has loaded, or open a .tex file in Overleaf and retry.',
|
|
5250
|
-
'还没有读到 Overleaf 项目文件。请确认项目已加载完成,或在 Overleaf 点开一个 .tex 文件后重试。'
|
|
5251
|
-
);
|
|
5252
|
-
}
|
|
5253
|
-
|
|
5254
|
-
const textCount = files.filter(isTextSnapshotFile).length;
|
|
5255
|
-
const binaryCount = files.length - textCount;
|
|
5256
|
-
const resourceText = binaryCount ? tx(`, ${binaryCount} asset file(s)`, `,${binaryCount} 个资源文件`) : '';
|
|
5257
|
-
const activePath = project.activePath ? tx(`, current file: ${project.activePath}`, `,当前文件:${project.activePath}`) : '';
|
|
5258
|
-
const focusFiles = getActiveFocusFiles();
|
|
5259
|
-
const focusText = focusFiles.length ? tx(`, focus: ${focusFiles.join(', ')}`, `,优先处理:${focusFiles.join(', ')}`) : '';
|
|
5260
|
-
return tx(
|
|
5261
|
-
`Read Overleaf project: ${textCount} text file(s)${resourceText}${activePath}${focusText}.`,
|
|
5262
|
-
`已读取 Overleaf 项目:${textCount} 个文本文件${resourceText}${activePath}${focusText}。`
|
|
5263
|
-
);
|
|
5326
|
+
return DiagnosticsPanel.formatProjectSnapshotUserLog(project, { tx, getFocusFiles: getActiveFocusFiles });
|
|
5264
5327
|
}
|
|
5265
5328
|
|
|
5266
5329
|
function formatProjectSnapshotFileSize(file) {
|
|
5267
|
-
|
|
5268
|
-
return `${String(file.content || '').length} chars`;
|
|
5269
|
-
}
|
|
5270
|
-
if (Number.isFinite(Number(file?.size))) {
|
|
5271
|
-
return `${Number(file.size)} bytes`;
|
|
5272
|
-
}
|
|
5273
|
-
return file?.contentBase64 ? `${String(file.contentBase64).length} base64` : 'binary';
|
|
5330
|
+
return DiagnosticsPanel.formatProjectSnapshotFileSize(file);
|
|
5274
5331
|
}
|
|
5275
5332
|
|
|
5276
5333
|
function formatProjectSnapshotWarning(warning) {
|
|
5277
|
-
|
|
5278
|
-
return tx('No project source files were read. Make sure Overleaf has loaded, or open a .tex file and retry.', '没有读取到项目源文件。请确认 Overleaf 页面加载完成,或点开一个 .tex 文件后重试。');
|
|
5279
|
-
}
|
|
5280
|
-
if (/Full project snapshot was not captured/i.test(warning)) {
|
|
5281
|
-
return tx('The full Overleaf project was not read. To avoid refreshing the local workspace from an incomplete snapshot, reload Overleaf or retry later.', '没有读到完整的 Overleaf 项目。为了避免本地 workspace 用残缺快照覆盖项目,请刷新 Overleaf 或稍后重试。');
|
|
5282
|
-
}
|
|
5283
|
-
if (/empty or still loading/i.test(warning)) {
|
|
5284
|
-
return tx('Some file content is still loading. Wait for Overleaf to finish loading, then retry.', '读取到的文件内容还在加载中。请等 Overleaf 加载完成后重试。');
|
|
5285
|
-
}
|
|
5286
|
-
if (/suspiciously short|shorter than 80 characters/i.test(warning)) {
|
|
5287
|
-
return tx('Some file content is very short and may not have fully loaded. Results may be incomplete.', '部分文件内容很短,可能还没完全加载。结果可能不完整。');
|
|
5288
|
-
}
|
|
5289
|
-
if (/identical captured content/i.test(warning)) {
|
|
5290
|
-
return tx('Several files appear to have identical captured content, so Overleaf file switching may have failed. Reload and retry.', '多个文件内容看起来相同,Overleaf 文件切换可能没有成功。请刷新页面后重试。');
|
|
5291
|
-
}
|
|
5292
|
-
if (/were skipped/i.test(warning)) {
|
|
5293
|
-
return tx('Some project files were skipped, usually images, PDFs, or unreadable files.', '有些项目文件被跳过,通常是图片、PDF 或无法读取的文件。');
|
|
5294
|
-
}
|
|
5295
|
-
return warning;
|
|
5334
|
+
return DiagnosticsPanel.formatProjectSnapshotWarning(warning, tx);
|
|
5296
5335
|
}
|
|
5297
5336
|
|
|
5298
5337
|
function appendEditorDiagnostics(editorDiagnostics, projectDiagnostics) {
|
|
5299
|
-
|
|
5300
|
-
const active = editorDiagnostics.active;
|
|
5301
|
-
appendLog(`Editor probe: active ${active?.tag || 'none'} ${active?.ariaLabel || active?.className || ''} len=${active?.valueLength || 0}; textareas=${editorDiagnostics.textareaCount || 0}; editables=${editorDiagnostics.editableCount || 0}; iframes=${editorDiagnostics.iframeCount || 0}.`);
|
|
5302
|
-
if (editorDiagnostics.documentStats) {
|
|
5303
|
-
const stats = editorDiagnostics.documentStats;
|
|
5304
|
-
appendLog(`DOM stats: elements=${stats.elementCount || 0}; textareas=${stats.textareaCount || 0}; cm=${stats.cmCount || 0}; role textbox=${stats.roleTextboxCount || 0}.`);
|
|
5305
|
-
}
|
|
5306
|
-
if (editorDiagnostics.unstableStore) {
|
|
5307
|
-
const store = editorDiagnostics.unstableStore;
|
|
5308
|
-
const readable = (store.readable || [])
|
|
5309
|
-
.map(item => `${item.path}:${item.present ? item.type : 'missing'}`)
|
|
5310
|
-
.join(', ');
|
|
5311
|
-
appendLog(`Overleaf store: ${store.present ? 'present' : 'missing'}${readable ? `; ${readable}` : ''}.`);
|
|
5312
|
-
}
|
|
5313
|
-
if (editorDiagnostics.codeMirrorView) {
|
|
5314
|
-
const view = editorDiagnostics.codeMirrorView;
|
|
5315
|
-
appendLog(`CodeMirror view: docLength=${view.docLength || 0}; dispatch=${Boolean(view.hasDispatch)}; source=${view.source || 'unknown'}.`);
|
|
5316
|
-
}
|
|
5317
|
-
if (editorDiagnostics.globals) {
|
|
5318
|
-
const globals = editorDiagnostics.globals;
|
|
5319
|
-
appendLog(`Overleaf globals: overleaf=${globals.overleaf || 'missing'}; Overleaf=${globals.Overleaf || 'missing'}; _ide=${globals._ide || 'missing'}.`);
|
|
5320
|
-
}
|
|
5321
|
-
for (const item of [...(editorDiagnostics.textareas || []), ...(editorDiagnostics.editables || [])].slice(0, 3)) {
|
|
5322
|
-
appendLog(`Editor candidate: ${item.tag || 'node'} ${item.ariaLabel || item.className || item.id || ''} len=${item.valueLength || 0}.`);
|
|
5323
|
-
}
|
|
5324
|
-
}
|
|
5325
|
-
if (projectDiagnostics) {
|
|
5326
|
-
const records = projectDiagnostics.docRecords || [];
|
|
5327
|
-
appendLog(`Project probe: doc records=${projectDiagnostics.docRecordCount || 0}; roots=${(projectDiagnostics.internalRootKeys || []).slice(0, 6).join(', ') || 'none'}.`);
|
|
5328
|
-
for (const record of records.slice(0, 4)) {
|
|
5329
|
-
appendLog(`Doc record: ${record.path} id=${record.id} source=${record.source || 'internal'}.`);
|
|
5330
|
-
}
|
|
5331
|
-
}
|
|
5338
|
+
DiagnosticsPanel.appendEditorDiagnostics(editorDiagnostics, projectDiagnostics, appendLog);
|
|
5332
5339
|
}
|
|
5333
5340
|
|
|
5334
5341
|
function appendProjectWarnings(project) {
|
|
5335
|
-
|
|
5336
|
-
for (const warning of warnings.blocking) {
|
|
5337
|
-
appendLog(`Snapshot blocked: ${warning}`);
|
|
5338
|
-
}
|
|
5339
|
-
for (const warning of warnings.nonBlocking) {
|
|
5340
|
-
appendLog(`Snapshot warning: ${warning}`);
|
|
5341
|
-
}
|
|
5342
|
+
DiagnosticsPanel.appendProjectWarnings(project, { getWarnings: getProjectSnapshotWarnings, appendLog });
|
|
5342
5343
|
}
|
|
5343
5344
|
|
|
5344
5345
|
function getProjectSnapshotWarnings(project) {
|
|
@@ -5388,40 +5389,15 @@
|
|
|
5388
5389
|
}
|
|
5389
5390
|
|
|
5390
5391
|
function isTextSnapshotFile(file) {
|
|
5391
|
-
return
|
|
5392
|
+
return DiagnosticsPanel.isTextSnapshotFile(file);
|
|
5392
5393
|
}
|
|
5393
5394
|
|
|
5394
5395
|
function uniqueContentSignatures(files) {
|
|
5395
|
-
|
|
5396
|
-
const content = String(file.content || '');
|
|
5397
|
-
return `${content.length}:${content.slice(0, 120)}:${content.slice(-120)}`;
|
|
5398
|
-
});
|
|
5399
|
-
return Array.from(new Set(signatures));
|
|
5396
|
+
return DiagnosticsPanel.uniqueContentSignatures(files);
|
|
5400
5397
|
}
|
|
5401
5398
|
|
|
5402
5399
|
function appendReviewingDiagnostics(diagnostics) {
|
|
5403
|
-
|
|
5404
|
-
appendLog(tx('Probe diagnostics unavailable.', '诊断探针不可用。'));
|
|
5405
|
-
return;
|
|
5406
|
-
}
|
|
5407
|
-
|
|
5408
|
-
appendLog(`Probe saw ${diagnostics.controlCount || 0} controls; body Reviewing=${Boolean(diagnostics.bodyTextHasReviewing)}, text Reviewing=${Boolean(diagnostics.textContentHasReviewing)}.`);
|
|
5409
|
-
const controls = diagnostics.reviewLikeControls || [];
|
|
5410
|
-
if (!controls.length) {
|
|
5411
|
-
appendLog('Probe did not see review/track/suggest controls in the page DOM.');
|
|
5412
|
-
return;
|
|
5413
|
-
}
|
|
5414
|
-
|
|
5415
|
-
for (const control of controls.slice(0, 4)) {
|
|
5416
|
-
const label = [
|
|
5417
|
-
control.text,
|
|
5418
|
-
control.ariaLabel && `aria:${control.ariaLabel}`,
|
|
5419
|
-
control.title && `title:${control.title}`,
|
|
5420
|
-
control.dataTestId && `test:${control.dataTestId}`,
|
|
5421
|
-
control.id && `id:${control.id}`
|
|
5422
|
-
].filter(Boolean).join(' | ');
|
|
5423
|
-
appendLog(`Review-like ${control.tag || 'node'}: ${label || control.htmlSnippet || '(no label)'}`);
|
|
5424
|
-
}
|
|
5400
|
+
DiagnosticsPanel.appendReviewingDiagnostics(diagnostics, { appendLog, tx });
|
|
5425
5401
|
}
|
|
5426
5402
|
|
|
5427
5403
|
function sendNative(payload) {
|
|
@@ -5991,6 +5967,7 @@
|
|
|
5991
5967
|
runs: Array.isArray(session.runs) ? session.runs : [],
|
|
5992
5968
|
history: Array.isArray(session.history) ? session.history : [],
|
|
5993
5969
|
task: typeof session.task === 'string' ? session.task : '',
|
|
5970
|
+
pendingInputs: Array.isArray(session.pendingInputs) ? session.pendingInputs : [],
|
|
5994
5971
|
mode: session.mode || prefs.mode || 'confirm', providerId: session.providerId || 'builtin',
|
|
5995
5972
|
model: session.model || prefs.model || 'gpt-5.4',
|
|
5996
5973
|
reasoningEffort: session.reasoningEffort || prefs.reasoningEffort || 'high',
|
|
@@ -6152,14 +6129,11 @@
|
|
|
6152
6129
|
updatedAt: session.updatedAt
|
|
6153
6130
|
}, { preserveRunActionPayload: true })
|
|
6154
6131
|
));
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
await Promise.all(existingSessions
|
|
6161
|
-
.filter(session => session?.id && !keepSessionIds.has(session.id))
|
|
6162
|
-
.map(session => StorageDb.deleteRecord('sessions', session.id)));
|
|
6132
|
+
await window.CodexOverleafSessionPersistence.writeSessions({
|
|
6133
|
+
Migration, StorageDb, projectId,
|
|
6134
|
+
deletedSessionIds: options?.deletedSessionIds,
|
|
6135
|
+
sessionRecords
|
|
6136
|
+
});
|
|
6163
6137
|
} catch (error) {
|
|
6164
6138
|
// Fallback: try legacy save
|
|
6165
6139
|
try {
|
|
@@ -6313,7 +6287,7 @@
|
|
|
6313
6287
|
saveStateSoon();
|
|
6314
6288
|
return;
|
|
6315
6289
|
}
|
|
6316
|
-
if (event?.target?.matches?.('[data-speed]'))
|
|
6290
|
+
if (event?.target?.matches?.('[data-speed]')) renderReasoningOptions(getRenderedModelEntries());
|
|
6317
6291
|
renderSpeedOptions(getRenderedModelEntries());
|
|
6318
6292
|
readPanelInputs();
|
|
6319
6293
|
renderModelConfigChoices();
|
|
@@ -6367,14 +6341,21 @@
|
|
|
6367
6341
|
syncExperimentalOtToggleForProject(projectId);
|
|
6368
6342
|
}
|
|
6369
6343
|
persistComposerAttachmentsToState();
|
|
6344
|
+
const model = readSelectedModelInput();
|
|
6345
|
+
const reasoningEffort = panel.querySelector('[data-reasoning]').value;
|
|
6346
|
+
const speedTier = readSelectedSpeedInput();
|
|
6370
6347
|
state = updateActiveSession(state, {
|
|
6371
|
-
model: readSelectedModelInput(),
|
|
6372
|
-
reasoningEffort: panel.querySelector('[data-reasoning]').value,
|
|
6373
|
-
speedTier: readSelectedSpeedInput(),
|
|
6374
6348
|
mode: panel.querySelector('[data-mode]').value,
|
|
6375
6349
|
task: panel.querySelector('[data-task]').value,
|
|
6376
6350
|
requireReviewing: panel.querySelector('[data-require-reviewing]').checked
|
|
6377
6351
|
});
|
|
6352
|
+
state.providerId = state.providerId || 'builtin';
|
|
6353
|
+
state.model = model;
|
|
6354
|
+
state.reasoningEffort = reasoningEffort;
|
|
6355
|
+
state.speedTier = speedTier;
|
|
6356
|
+
if (state.session) {
|
|
6357
|
+
state.session.providerId = state.providerId;
|
|
6358
|
+
}
|
|
6378
6359
|
state.autoRecompile = panel.querySelector('[data-auto-recompile]')?.checked !== false;
|
|
6379
6360
|
const experimentalOtCheckbox = panel.querySelector('[data-experimental-ot]');
|
|
6380
6361
|
if (experimentalOtCheckbox) {
|
|
@@ -6519,12 +6500,7 @@
|
|
|
6519
6500
|
|
|
6520
6501
|
|
|
6521
6502
|
function setRunning(running) {
|
|
6522
|
-
|
|
6523
|
-
// Running -> the button is Cancel and must never be disabled; idle -> it
|
|
6524
|
-
// is Send and disables on an empty composer (feedback instead of no-op).
|
|
6525
|
-
runButton.disabled = running ? false : !String(panel.querySelector('[data-task]')?.value || '').trim();
|
|
6526
|
-
runButton.title = running ? tr('cancelRun') : tr('send');
|
|
6527
|
-
runButton.setAttribute('aria-label', running ? tr('cancelRun') : tr('send'));
|
|
6503
|
+
syncComposerSendAvailability(running);
|
|
6528
6504
|
panel.querySelector('[data-new-session]').disabled = false;
|
|
6529
6505
|
panel.querySelector('[data-diagnostics-snapshot]').disabled = running;
|
|
6530
6506
|
if (running) {
|
|
@@ -6532,9 +6508,10 @@
|
|
|
6532
6508
|
}
|
|
6533
6509
|
panel.dataset.running = running ? 'true' : 'false';
|
|
6534
6510
|
panel.dataset.cancelling = running && runCancellationRequested ? 'true' : 'false';
|
|
6511
|
+
renderPendingInputs();
|
|
6535
6512
|
}
|
|
6536
6513
|
|
|
6537
|
-
function startRunView({ task, mode, model, reasoningEffort, speedTier }) {
|
|
6514
|
+
function startRunView({ task, mode, model, reasoningEffort, speedTier, queueItemId = '' }) {
|
|
6538
6515
|
let attachments = [];
|
|
6539
6516
|
if (Array.isArray(arguments[0]?.attachments)) {
|
|
6540
6517
|
attachments = arguments[0].attachments;
|
|
@@ -6558,6 +6535,10 @@
|
|
|
6558
6535
|
undoTrackedChanges: [],
|
|
6559
6536
|
undoExpectedFiles: [],
|
|
6560
6537
|
undoStatus: '',
|
|
6538
|
+
queueItemId,
|
|
6539
|
+
nativeRequestId: '',
|
|
6540
|
+
codexTurnId: '',
|
|
6541
|
+
nativeEventSeq: 0,
|
|
6561
6542
|
// Welcome-panel + write-guard:
|
|
6562
6543
|
// Immutable per-run capture of the project this run was submitted
|
|
6563
6544
|
// against. Every writeback / accept / undo dispatch attaches this id
|
|
@@ -6602,7 +6583,10 @@
|
|
|
6602
6583
|
report: root.querySelector('[data-run-report]'),
|
|
6603
6584
|
status: root.querySelector('[data-run-status]'),
|
|
6604
6585
|
projectFiles: captureProjectReferenceFiles(arguments[0]?.project),
|
|
6605
|
-
startedAt: Date.now()
|
|
6586
|
+
startedAt: Date.now(),
|
|
6587
|
+
queueItemId,
|
|
6588
|
+
nativeRequestId: '',
|
|
6589
|
+
activeTurn: null
|
|
6606
6590
|
};
|
|
6607
6591
|
}
|
|
6608
6592
|
|
|
@@ -6619,6 +6603,8 @@
|
|
|
6619
6603
|
record.status = status;
|
|
6620
6604
|
record.statusText = statusText;
|
|
6621
6605
|
record.finishedAt = new Date().toISOString();
|
|
6606
|
+
currentRunView.terminalStatus = status;
|
|
6607
|
+
runGuidanceController.settleView(currentRunView);
|
|
6622
6608
|
// Welcome-panel + write-guard:
|
|
6623
6609
|
// when the user navigated away mid-run, the URL-derived projectId now
|
|
6624
6610
|
// belongs to a different project (or /project). Routing this finish
|
|
@@ -6649,11 +6635,16 @@
|
|
|
6649
6635
|
}
|
|
6650
6636
|
|
|
6651
6637
|
|
|
6652
|
-
function appendNativeEvent(event) {
|
|
6638
|
+
function appendNativeEvent(event, journalSeq = 0) {
|
|
6653
6639
|
if (!event) {
|
|
6654
6640
|
return;
|
|
6655
6641
|
}
|
|
6656
6642
|
|
|
6643
|
+
if (event.type === 'codex.turn.bound') {
|
|
6644
|
+
bindActiveTurn(event.detail || {}, journalSeq);
|
|
6645
|
+
return;
|
|
6646
|
+
}
|
|
6647
|
+
|
|
6657
6648
|
const activity = mapAgentEventToActivity(event, { locale: getLocale() });
|
|
6658
6649
|
if (!activity?.visible || activity.kind === 'technical') {
|
|
6659
6650
|
return;
|
|
@@ -6669,10 +6660,30 @@
|
|
|
6669
6660
|
streamKey: activity.streamKey,
|
|
6670
6661
|
streamRole: activity.streamRole,
|
|
6671
6662
|
appendText: activity.appendText,
|
|
6672
|
-
replaceText: activity.replaceText
|
|
6663
|
+
replaceText: activity.replaceText,
|
|
6664
|
+
nativeEventSeq: journalSeq
|
|
6673
6665
|
});
|
|
6674
6666
|
}
|
|
6675
6667
|
|
|
6668
|
+
function bindActiveTurn(detail = {}, journalSeq = 0) {
|
|
6669
|
+
if (!currentRunView) {
|
|
6670
|
+
return;
|
|
6671
|
+
}
|
|
6672
|
+
const threadId = String(detail.threadId || '');
|
|
6673
|
+
const turnId = String(detail.turnId || '');
|
|
6674
|
+
currentRunView.activeTurn = { threadId, turnId };
|
|
6675
|
+
const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
6676
|
+
if (record) {
|
|
6677
|
+
record.codexTurnId = turnId;
|
|
6678
|
+
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(journalSeq || 0));
|
|
6679
|
+
}
|
|
6680
|
+
if (threadId) {
|
|
6681
|
+
updateSessionById(currentRunView.sessionId, { codexThreadId: threadId });
|
|
6682
|
+
}
|
|
6683
|
+
renderPendingInputs();
|
|
6684
|
+
saveState().catch(() => {});
|
|
6685
|
+
}
|
|
6686
|
+
|
|
6676
6687
|
function appendTechnicalEvent(event) {
|
|
6677
6688
|
void event;
|
|
6678
6689
|
}
|
|
@@ -6703,6 +6714,7 @@
|
|
|
6703
6714
|
kind: input.kind || 'activity',
|
|
6704
6715
|
subagent: input.subagent === true ? true : undefined,
|
|
6705
6716
|
technicalDetail: sanitizeAssistantVisibleValue(input.technicalDetail),
|
|
6717
|
+
guidanceId: sanitizeAssistantVisibleText(input.guidanceId),
|
|
6706
6718
|
streamKey: sanitizeAssistantVisibleText(input.streamKey),
|
|
6707
6719
|
streamRole: sanitizeAssistantVisibleText(input.streamRole),
|
|
6708
6720
|
appendText: typeof input.appendText === 'string' ? sanitizeAssistantVisibleText(input.appendText) : input.appendText,
|
|
@@ -6734,6 +6746,9 @@
|
|
|
6734
6746
|
const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
6735
6747
|
let renderedEvent = event;
|
|
6736
6748
|
if (record) {
|
|
6749
|
+
if (Number.isFinite(Number(input.nativeEventSeq))) {
|
|
6750
|
+
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(input.nativeEventSeq || 0));
|
|
6751
|
+
}
|
|
6737
6752
|
// v1.8.1: keep the session's activity timestamp honest DURING a run —
|
|
6738
6753
|
// it was pinned to run start, so the dashboard's 30-minute zombie
|
|
6739
6754
|
// heuristic mislabeled any long live run as interrupted (fleet P1).
|
|
@@ -6807,6 +6822,7 @@
|
|
|
6807
6822
|
currentRunView.root = root;
|
|
6808
6823
|
currentRunView.runProcess = root.querySelector('[data-run-process]');
|
|
6809
6824
|
currentRunView.processLabel = root.querySelector('[data-run-process-summary]');
|
|
6825
|
+
currentRunView.guidance = root.querySelector('[data-run-guidance]');
|
|
6810
6826
|
currentRunView.events = root.querySelector('[data-run-events]');
|
|
6811
6827
|
currentRunView.report = root.querySelector('[data-run-report]');
|
|
6812
6828
|
currentRunView.status = root.querySelector('[data-run-status]');
|
|
@@ -6824,6 +6840,7 @@
|
|
|
6824
6840
|
if (event.kind === 'technical') {
|
|
6825
6841
|
return;
|
|
6826
6842
|
}
|
|
6843
|
+
if (runGuidanceController.appendToView(event, view)) return;
|
|
6827
6844
|
if (event.kind === 'stream') {
|
|
6828
6845
|
// Stream deltas update an existing line in place — they must not inflate
|
|
6829
6846
|
// the unread-step counter.
|
|
@@ -8092,7 +8109,8 @@
|
|
|
8092
8109
|
detail: event.detail,
|
|
8093
8110
|
timestamp: event.timestamp || new Date().toISOString(),
|
|
8094
8111
|
kind: event.kind || 'activity',
|
|
8095
|
-
technicalDetail: event.technicalDetail
|
|
8112
|
+
technicalDetail: event.technicalDetail,
|
|
8113
|
+
guidanceId: sanitizeAssistantVisibleText(event.guidanceId)
|
|
8096
8114
|
};
|
|
8097
8115
|
record.events = [...(record.events || []), normalized].slice(-MAX_RUN_EVENTS);
|
|
8098
8116
|
saveStateSoon();
|