codex-overleaf-link 2.5.1 → 2.6.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 +149 -398
- package/README.zh-CN.md +153 -404
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +78 -3
- package/extension/src/backgroundSharedSessions.js +55 -2
- package/extension/src/backgroundUpdateCoordinator.js +128 -12
- package/extension/src/content/activeTurnControl.js +164 -7
- package/extension/src/content/composerAttachments.js +11 -2
- package/extension/src/content/composerPanel.js +92 -12
- package/extension/src/content/contentRuntime.js +920 -276
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsPanel.js +18 -0
- package/extension/src/content/generated/content.bundle.js +4496 -924
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +4 -0
- package/extension/src/content/localSkillsPanel.js +2 -2
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +42 -5
- package/extension/src/content/modelPicker.js +1 -0
- package/extension/src/content/nativeCompatibilityController.js +29 -10
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +2 -2
- package/extension/src/content/panelMaintenance.js +6 -2
- package/extension/src/content/panelRenderer.js +51 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectSettingsCoordinator.js +30 -6
- package/extension/src/content/providerSettingsDialog.js +2 -1
- package/extension/src/content/recentProjects.js +4 -3
- package/extension/src/content/runActivitySummary.js +122 -14
- package/extension/src/content/runCompletionMeta.js +194 -6
- package/extension/src/content/runController.js +2 -1
- package/extension/src/content/runFailureNotice.js +8 -1
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +9 -5
- package/extension/src/content/runRecoveryActions.js +2 -1
- package/extension/src/content/runResultActions.js +22 -20
- package/extension/src/content/runReviewController.js +94 -35
- package/extension/src/content/runTimelineView.js +224 -25
- package/extension/src/content/scopedPersistencePanelState.js +92 -23
- package/extension/src/content/sensitivePreflight.js +14 -1
- package/extension/src/content/sessionManager.js +49 -3
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +15 -2
- package/extension/src/content/settingsWorkbench.js +127 -89
- package/extension/src/content/sharedSessionViewSync.js +4 -3
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateNotice.js +60 -6
- package/extension/src/content/writebackOrchestrator.js +35 -22
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +9 -1
- package/extension/src/page/writebackRouter.js +24 -77
- package/extension/src/pageBridge.js +107 -66
- package/extension/src/popup.js +1 -1
- package/extension/src/shared/agentTranscript.js +350 -17
- package/extension/src/shared/auditRecords.js +2 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/compileAdapter.js +58 -17
- package/extension/src/shared/composerAttachmentDraft.js +6 -1
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/failureReasons.js +42 -3
- package/extension/src/shared/i18n.js +299 -29
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/sessionState.js +63 -4
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/sharedSessionBridge.js +81 -13
- package/extension/src/shared/staleGuard.js +72 -63
- package/extension/src/shared/storageDb.js +35 -3
- package/extension/src/shared/storageMigration.js +7 -1
- package/extension/src/shared/storageRunActions.js +62 -8
- package/extension/src/shared/undoOperations.js +126 -28
- package/extension/src/shared/updateRuntimeIdentity.js +16 -2
- package/extension/src/shared/writebackSettlement.js +21 -3
- package/extension/styles/panel.css +1106 -490
- package/native-host/src/bootstrapUpdate.js +52 -16
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +10 -1
- package/native-host/src/chatCompletionsBridge.js +29 -2
- package/native-host/src/codexPromptAssembly.js +86 -118
- package/native-host/src/codexSessionRunner.js +126 -17
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/taskRunnerRuntime.js +18 -6
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/updateManager.js +120 -15
- package/package.json +5 -8
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
const RUN_PROJECT_SYNC_MAX_AGE_MS = 30000;
|
|
34
34
|
const RUN_SNAPSHOT_ZIP_TIMEOUT_MS = 30000;
|
|
35
35
|
const STREAM_RENDER_FLUSH_MS = 80;
|
|
36
|
+
const REVIEWING_CONFIRM_TIMEOUT_MS = 120000;
|
|
36
37
|
const STREAM_SAVE_DELAY_MS = 1000;
|
|
37
38
|
const MAX_RUN_EVENTS = 300;
|
|
38
39
|
const MAX_SAFE_UNDO_REPLACEALL_CHARS = 2000;
|
|
@@ -98,7 +99,8 @@
|
|
|
98
99
|
recordSessionResult,
|
|
99
100
|
selectVisibleSessionsForList,
|
|
100
101
|
setActiveSession,
|
|
101
|
-
updateActiveSession
|
|
102
|
+
updateActiveSession,
|
|
103
|
+
updateActiveSessionDraft
|
|
102
104
|
} = Modules.SessionState;
|
|
103
105
|
const LineReferences = Modules.LineReferences;
|
|
104
106
|
const { getProjectStorageKey } = Modules.StorageKeys;
|
|
@@ -403,7 +405,10 @@
|
|
|
403
405
|
},
|
|
404
406
|
getPanel: () => panel,
|
|
405
407
|
getState: () => state,
|
|
406
|
-
setState: next => {
|
|
408
|
+
setState: next => {
|
|
409
|
+
if (next.activeSessionId !== state?.activeSessionId) composerDraftEditedHere = false;
|
|
410
|
+
state = next;
|
|
411
|
+
},
|
|
407
412
|
getCurrentRunView: () => currentRunView,
|
|
408
413
|
getSettingsPanelInstance: () => settingsPanelInstance,
|
|
409
414
|
prepareRetryReplacement: (run, task) => prepareRetryReplacement(run, task)
|
|
@@ -424,7 +429,10 @@
|
|
|
424
429
|
const sessionForkController = Modules.SessionForkController.create({
|
|
425
430
|
SessionState: Modules.SessionState,
|
|
426
431
|
getState: () => state,
|
|
427
|
-
setState: next => {
|
|
432
|
+
setState: next => {
|
|
433
|
+
if (next.activeSessionId !== state?.activeSessionId) composerDraftEditedHere = false;
|
|
434
|
+
state = next;
|
|
435
|
+
},
|
|
428
436
|
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
|
|
429
437
|
sendBackgroundNative, saveState,
|
|
430
438
|
getEnabledCodexLocalSkillIds: () => getEnabledCodexLocalSkillIds(),
|
|
@@ -440,15 +448,14 @@
|
|
|
440
448
|
});
|
|
441
449
|
const runResultActions = Modules.RunResultActions.create({
|
|
442
450
|
tr,
|
|
443
|
-
getLocale,
|
|
444
451
|
forkRunFromNode: sessionForkController.forkRunFromNode
|
|
445
452
|
});
|
|
446
453
|
const runTimelineView = Modules.RunTimelineView.create({
|
|
447
454
|
RunActivitySummary: Modules.RunActivitySummary,
|
|
448
455
|
RunPresence: Modules.RunPresence,
|
|
456
|
+
PanelIcons: Modules.PanelIcons,
|
|
449
457
|
// recentProjects is composed below; the empty state only reads it after startup.
|
|
450
458
|
getProjectName: () => { try { return recentProjects.readCurrentProjectNameFromDom(); } catch { return ''; } },
|
|
451
|
-
getCurrentProjectId: () => getCurrentProjectId(),
|
|
452
459
|
RunActivityModel: Modules.RunActivityModel,
|
|
453
460
|
SubagentActivityView: Modules.SubagentActivityView,
|
|
454
461
|
RunGuidanceView: Modules.RunGuidanceView,
|
|
@@ -458,6 +465,7 @@
|
|
|
458
465
|
RunFailureNotice: Modules.RunFailureNotice,
|
|
459
466
|
I18n: i18n,
|
|
460
467
|
FailureReasons,
|
|
468
|
+
projectContextLimitEvent: event => Modules.AgentTranscript.projectContextLimitEvent(event, getLocale()),
|
|
461
469
|
localizeVisibleReason: value => localizeVisibleReason(value),
|
|
462
470
|
localizeCompileReason: value => Modules.CompileAdapter.localizeCompileReason(value, getLocale()),
|
|
463
471
|
tr,
|
|
@@ -477,7 +485,8 @@
|
|
|
477
485
|
cssEscape,
|
|
478
486
|
renderMarkdownInlineText,
|
|
479
487
|
renderMarkdownBlockText,
|
|
480
|
-
decorateCompletionReport: Modules.MarkdownDomRenderer.decorateCompletionReport,
|
|
488
|
+
decorateCompletionReport: (container, options) => Modules.MarkdownDomRenderer.decorateCompletionReport(container,
|
|
489
|
+
{ ...options, formatOperationType }),
|
|
481
490
|
sanitizeAssistantVisibleText,
|
|
482
491
|
sanitizeAssistantVisibleValue,
|
|
483
492
|
buildMarkdownInlineNodes,
|
|
@@ -521,6 +530,7 @@
|
|
|
521
530
|
SessionPanel: Modules.SessionPanel,
|
|
522
531
|
SessionState: Modules.SessionState,
|
|
523
532
|
SessionMenuView: Modules.SessionMenuView,
|
|
533
|
+
PanelIcons: Modules.PanelIcons,
|
|
524
534
|
tr,
|
|
525
535
|
showPluginConfirm,
|
|
526
536
|
showPluginToast,
|
|
@@ -528,10 +538,29 @@
|
|
|
528
538
|
saveState,
|
|
529
539
|
saveStateSoon,
|
|
530
540
|
readPanelInputs,
|
|
541
|
+
flushComposerDraft: () => flushComposerDraft({ requireDurable: true }),
|
|
542
|
+
setSessionTransition: pending => {
|
|
543
|
+
if (pending) {
|
|
544
|
+
closeSlashMenu();
|
|
545
|
+
contextUsageView?.close();
|
|
546
|
+
}
|
|
547
|
+
const ready = composerPanelInstance?.setSessionTransition(pending);
|
|
548
|
+
syncComposerSendAvailability();
|
|
549
|
+
if (!pending && deferredSessionStart) {
|
|
550
|
+
const deferred = deferredSessionStart;
|
|
551
|
+
deferredSessionStart = null;
|
|
552
|
+
if (deferred.scope === getComposerDraftScope()) runQueueScheduler?.scheduleOne(deferred.sessionId);
|
|
553
|
+
}
|
|
554
|
+
return ready;
|
|
555
|
+
},
|
|
556
|
+
getDraftScope: getComposerDraftScope,
|
|
531
557
|
applyStateToPanel, syncSessionProvider: () => providerSettingsCoordinator.syncSessionProvider(),
|
|
532
558
|
getPanel: () => panel,
|
|
533
559
|
getState: () => state,
|
|
534
|
-
setState: next => {
|
|
560
|
+
setState: next => {
|
|
561
|
+
if (next.activeSessionId !== state?.activeSessionId) composerDraftEditedHere = false;
|
|
562
|
+
state = next;
|
|
563
|
+
},
|
|
535
564
|
getSessionPanelInstance: () => sessionPanelInstance,
|
|
536
565
|
formatSessionTime
|
|
537
566
|
});
|
|
@@ -595,6 +624,7 @@
|
|
|
595
624
|
selectionContext: value,
|
|
596
625
|
...(options.edit ? { mode: 'auto' } : {})
|
|
597
626
|
});
|
|
627
|
+
syncProbeStatus();
|
|
598
628
|
if (options.edit && panel) {
|
|
599
629
|
const modeInput = panel.querySelector('[data-mode]');
|
|
600
630
|
if (modeInput) modeInput.value = 'auto';
|
|
@@ -762,6 +792,7 @@
|
|
|
762
792
|
|| { providerId: state?.providerId || 'builtin', providerRevision: 0 },
|
|
763
793
|
readSelectedSpeedInput,
|
|
764
794
|
getRenderedModelEntries,
|
|
795
|
+
onSelectionRendered: renderContextUsage,
|
|
765
796
|
persistPanelInputs,
|
|
766
797
|
closeDiagnosticsMenu,
|
|
767
798
|
closeCustomInstructionsSettings,
|
|
@@ -844,8 +875,11 @@
|
|
|
844
875
|
document.addEventListener('visibilitychange', () => {
|
|
845
876
|
if (document.visibilityState === 'visible') {
|
|
846
877
|
refreshDashboardSoon();
|
|
878
|
+
} else {
|
|
879
|
+
flushComposerDraftOnLeave();
|
|
847
880
|
}
|
|
848
881
|
});
|
|
882
|
+
window.addEventListener('pagehide', flushComposerDraftOnLeave);
|
|
849
883
|
|
|
850
884
|
const recentProjects = Modules.RecentProjects.create({
|
|
851
885
|
ProjectSessionCleanup: Modules.ProjectSessionCleanup,
|
|
@@ -887,6 +921,7 @@
|
|
|
887
921
|
const MAX_ATTACHMENT_PREVIEW_DATA_URL_CHARS = 768 * 1024;
|
|
888
922
|
const composerAttachmentController = Modules.ComposerAttachments.createComposerAttachmentController({
|
|
889
923
|
getPanel: () => panel,
|
|
924
|
+
icons: Modules.PanelIcons,
|
|
890
925
|
tr,
|
|
891
926
|
tx,
|
|
892
927
|
appendPlainLog,
|
|
@@ -917,11 +952,14 @@
|
|
|
917
952
|
});
|
|
918
953
|
const contextTrayController = ContextTray.createContextTrayController({
|
|
919
954
|
root: window,
|
|
955
|
+
icons: Modules.PanelIcons,
|
|
920
956
|
document,
|
|
921
957
|
getPanel: () => panel,
|
|
922
958
|
getState: () => state,
|
|
923
959
|
setState: nextState => {
|
|
924
960
|
state = nextState;
|
|
961
|
+
// Paint focus changes before ContextTray waits for durable persistence.
|
|
962
|
+
syncProbeStatus();
|
|
925
963
|
},
|
|
926
964
|
saveState,
|
|
927
965
|
saveStateSoon,
|
|
@@ -985,6 +1023,7 @@
|
|
|
985
1023
|
let settingsPanelInstance = null;
|
|
986
1024
|
let diagnosticsPanelInstance = null;
|
|
987
1025
|
let composerPanelInstance = null;
|
|
1026
|
+
let contextUsageView = null;
|
|
988
1027
|
let state = null;
|
|
989
1028
|
let storageKey = LEGACY_STORAGE_KEY;
|
|
990
1029
|
let globalPreferencesController = null;
|
|
@@ -992,9 +1031,14 @@
|
|
|
992
1031
|
let runSubmissionPending = false;
|
|
993
1032
|
let pendingRetryReplacement = null;
|
|
994
1033
|
let runQueueScheduler = null;
|
|
1034
|
+
let deferredSessionStart = null;
|
|
995
1035
|
let runInputCoordinator = null;
|
|
996
1036
|
const activeTurnControl = ActiveTurnControl.create({ chrome });
|
|
997
1037
|
let saveStateTimer = null;
|
|
1038
|
+
const DRAFT_SAVE_DELAY_MS = 800;
|
|
1039
|
+
const DRAFT_SAVE_MAX_WAIT_MS = 4000;
|
|
1040
|
+
let draftSaveTimer = null;
|
|
1041
|
+
let pendingDraftSave = null;
|
|
998
1042
|
// Two-phase saveState scheduling. `saveStateInFlight` flips to true while
|
|
999
1043
|
// an async saveState() is actually writing; `saveStateRunAfterFlight` is
|
|
1000
1044
|
// set when a fresh saveStateSoon() fires during the in-flight phase. When
|
|
@@ -1011,6 +1055,8 @@
|
|
|
1011
1055
|
let storageNoticeKeys = new Set();
|
|
1012
1056
|
let composerSkillInvocation = null;
|
|
1013
1057
|
let composerDraftRevision = 0;
|
|
1058
|
+
// Page-local provenance only; the draft itself stays shared per conversation.
|
|
1059
|
+
let composerDraftEditedHere = false;
|
|
1014
1060
|
let slashCodexOverleafSkills = [];
|
|
1015
1061
|
let slashCodexOverleafSkillsLoaded = false;
|
|
1016
1062
|
let slashCodexOverleafSkillsLoading = false;
|
|
@@ -1033,6 +1079,7 @@
|
|
|
1033
1079
|
showPluginToast,
|
|
1034
1080
|
getPanel: () => panel,
|
|
1035
1081
|
ensurePanelOpen,
|
|
1082
|
+
getIconUrl: () => chrome.runtime.getURL('assets/icons/codex-overleaf-dialog-icon.png'),
|
|
1036
1083
|
getPanelRendererInstance: () => panelRendererInstance,
|
|
1037
1084
|
setBadge: (...args) => PanelRenderer.setBadge(...args),
|
|
1038
1085
|
setDiagnosticsHealth,
|
|
@@ -1051,7 +1098,7 @@
|
|
|
1051
1098
|
getBusyState: () => ({
|
|
1052
1099
|
run: Boolean(currentRunView),
|
|
1053
1100
|
cancelling: runCancellationRequested,
|
|
1054
|
-
storage: saveStateInFlight || Boolean(saveStateTimer),
|
|
1101
|
+
storage: saveStateInFlight || Boolean(saveStateTimer) || Boolean(pendingDraftSave),
|
|
1055
1102
|
reviewAction: trackedChangeInFlight.size > 0,
|
|
1056
1103
|
dialog: Boolean(pluginConfirmController?.isOpen())
|
|
1057
1104
|
}),
|
|
@@ -1081,7 +1128,7 @@
|
|
|
1081
1128
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
1082
1129
|
if (message?.type === 'codex-overleaf/runtime-health-probe') {
|
|
1083
1130
|
sendResponse?.({
|
|
1084
|
-
ok: true,
|
|
1131
|
+
ok: true, runOwnerProtocol: 2, runOwners: [activeTurnControl.getOwner()].filter(Boolean),
|
|
1085
1132
|
version: CodexOverleafCompatibility?.BUILD_TARGET_VERSION || chrome.runtime.getManifest().version
|
|
1086
1133
|
});
|
|
1087
1134
|
return false;
|
|
@@ -1146,6 +1193,7 @@
|
|
|
1146
1193
|
await refreshAccountScopeId();
|
|
1147
1194
|
storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
|
|
1148
1195
|
state = normalizeLoadedPanelState(await loadStoredState());
|
|
1196
|
+
composerDraftEditedHere = false;
|
|
1149
1197
|
await getRunReviewController().recoverInterruptedUndoStatuses();
|
|
1150
1198
|
initializeRunQueueScheduler();
|
|
1151
1199
|
recoverInterruptedRunJournals()
|
|
@@ -1251,7 +1299,6 @@
|
|
|
1251
1299
|
callbacks: {
|
|
1252
1300
|
onLauncherToggle: () => ensurePanelOpen(),
|
|
1253
1301
|
onClosePanel: () => closePanel(),
|
|
1254
|
-
onRefresh: () => refreshProbe({ userInitiated: true }),
|
|
1255
1302
|
onNewSession: () => startNewSession(),
|
|
1256
1303
|
onChangeHistory: () => openChangeHistory(),
|
|
1257
1304
|
onSettingsClick: () => toggleCustomInstructionsSettings(),
|
|
@@ -1273,7 +1320,9 @@
|
|
|
1273
1320
|
diagnosticsPanelInstance = DiagnosticsPanel.create({
|
|
1274
1321
|
container: panelRendererInstance.diagnosticsSlot,
|
|
1275
1322
|
i18n: { tr },
|
|
1323
|
+
icons: Modules.PanelIcons,
|
|
1276
1324
|
callbacks: {
|
|
1325
|
+
onRefresh: () => refreshProbe({ userInitiated: true }),
|
|
1277
1326
|
onBeforeOpen: () => {
|
|
1278
1327
|
closeModelConfigPopover();
|
|
1279
1328
|
closeContextTray();
|
|
@@ -1311,6 +1360,7 @@
|
|
|
1311
1360
|
projectId: getCurrentProjectId(),
|
|
1312
1361
|
i18n: { tr },
|
|
1313
1362
|
button: panel.querySelector('[data-custom-instructions-settings]'),
|
|
1363
|
+
getProjectName: () => { try { return recentProjects.readCurrentProjectNameFromDom(); } catch { return ''; } },
|
|
1314
1364
|
callbacks: {
|
|
1315
1365
|
onBack: () => closeCustomInstructionsSettings(),
|
|
1316
1366
|
onInputChange: event => persistPanelInputs(event),
|
|
@@ -1351,9 +1401,12 @@
|
|
|
1351
1401
|
composerPanelInstance = ComposerPanel.create({
|
|
1352
1402
|
container: panelRendererInstance.composerSlot,
|
|
1353
1403
|
i18n: { tr },
|
|
1404
|
+
icons: Modules.PanelIcons,
|
|
1354
1405
|
attachmentController: composerAttachmentController,
|
|
1355
1406
|
callbacks: {
|
|
1356
1407
|
onSubmit: () => safeRunTask(),
|
|
1408
|
+
getTaskScope: getComposerDraftScope,
|
|
1409
|
+
getTaskValue: () => state.task,
|
|
1357
1410
|
isRunning: () => Boolean(currentRunView),
|
|
1358
1411
|
onCancel: () => cancelActiveRun(),
|
|
1359
1412
|
onPaste: handleComposerPaste,
|
|
@@ -1369,7 +1422,7 @@
|
|
|
1369
1422
|
onAttachClick: () => panel.querySelector('[data-attach-input]')?.click(),
|
|
1370
1423
|
onAttachInput: handleAttachInputChange,
|
|
1371
1424
|
onContextRefresh: () => loadContextFiles({ force: true }),
|
|
1372
|
-
onModelConfigToggle: () => toggleModelConfigPopover(),
|
|
1425
|
+
onModelConfigToggle: () => { contextUsageView?.close(); toggleModelConfigPopover(); },
|
|
1373
1426
|
onModelConfigChoiceClick: event => {
|
|
1374
1427
|
handleModelConfigChoiceClick(event).catch(error => {
|
|
1375
1428
|
appendPlainLog(tx(`Failed to update model settings: ${error.message}`, `更新模型设置失败:${error.message}`));
|
|
@@ -1382,6 +1435,18 @@
|
|
|
1382
1435
|
}
|
|
1383
1436
|
});
|
|
1384
1437
|
|
|
1438
|
+
contextUsageView = Modules.ContextUsageView.create({
|
|
1439
|
+
getPanel: () => panel, getLocale, tr,
|
|
1440
|
+
onOpen: () => {
|
|
1441
|
+
closeModelConfigPopover();
|
|
1442
|
+
closeContextTray();
|
|
1443
|
+
closeSlashMenu();
|
|
1444
|
+
},
|
|
1445
|
+
getCompactAvailability,
|
|
1446
|
+
level: Modules.ContextUsage.level,
|
|
1447
|
+
onCompact: () => safeRunTask({ operation: 'compact' })
|
|
1448
|
+
});
|
|
1449
|
+
renderContextUsage();
|
|
1385
1450
|
installContextDismiss();
|
|
1386
1451
|
installModelConfigDismiss();
|
|
1387
1452
|
bindLogAutoFollow();
|
|
@@ -1559,7 +1624,7 @@
|
|
|
1559
1624
|
panelRendererInstance?.setLocale?.({ launcher: tx('Show or hide Codex panel', '显示或隐藏 Codex 边栏'), close: tx('Close Codex panel', '关闭 Codex 边栏') });
|
|
1560
1625
|
|
|
1561
1626
|
setElementTitleAndAria('[data-panel-resize-handle]', tr('resizePanel'), tr('resizePanel'));
|
|
1562
|
-
|
|
1627
|
+
setRefreshProbeLoading(panel?.querySelector('[data-refresh]')?.dataset.loading === 'true');
|
|
1563
1628
|
// State-dependent tooltip: re-derive from the dot's current health so a
|
|
1564
1629
|
// locale/session refresh doesn't clobber it back to the generic label.
|
|
1565
1630
|
setDiagnosticsHealth(panel.querySelector('[data-diagnostics-health-dot]')?.dataset.health || 'unknown');
|
|
@@ -1610,7 +1675,7 @@
|
|
|
1610
1675
|
}
|
|
1611
1676
|
const reviewToggle = panel.querySelector('.codex-review-toggle');
|
|
1612
1677
|
if (reviewToggle) {
|
|
1613
|
-
|
|
1678
|
+
syncReviewingTooltip();
|
|
1614
1679
|
}
|
|
1615
1680
|
const recompileToggle = panel.querySelector('.codex-recompile-toggle');
|
|
1616
1681
|
if (recompileToggle) {
|
|
@@ -1633,7 +1698,11 @@
|
|
|
1633
1698
|
updateExperimentalOtMenuStatus();
|
|
1634
1699
|
renderModelConfigChoices();
|
|
1635
1700
|
updateModelDisplay();
|
|
1701
|
+
// The header switcher's title and accessible name carry localized text too.
|
|
1702
|
+
applySessionLabel();
|
|
1636
1703
|
renderSessionList();
|
|
1704
|
+
renderContextUsage();
|
|
1705
|
+
if (panel.querySelector('[data-slash-menu]')?.hidden === false) updateSlashMenuForTaskInput();
|
|
1637
1706
|
renderContextSelection();
|
|
1638
1707
|
renderContextSummary();
|
|
1639
1708
|
renderPendingInputs();
|
|
@@ -1810,7 +1879,8 @@
|
|
|
1810
1879
|
// Capture the click before any preparation can yield to another draft/session.
|
|
1811
1880
|
readPanelInputs();
|
|
1812
1881
|
const queuedInput = options.queuedInput || null;
|
|
1813
|
-
const
|
|
1882
|
+
const compact = !queuedInput && options.operation === 'compact';
|
|
1883
|
+
const task = compact ? '/compact' : String(queuedInput?.text || state.task || '').trim();
|
|
1814
1884
|
if (!task) {
|
|
1815
1885
|
appendLog(tx('Enter a task first.', '请先输入任务。'));
|
|
1816
1886
|
return;
|
|
@@ -1818,32 +1888,38 @@
|
|
|
1818
1888
|
const submittedSessionId = state.activeSessionId;
|
|
1819
1889
|
const submittedSession = runController.captureSessionContext(state);
|
|
1820
1890
|
const submittedThreadId = getActiveSession(state)?.codexThreadId || '';
|
|
1891
|
+
if (compact && !submittedThreadId) {
|
|
1892
|
+
showPluginToast(tr('compactNoThread'), { status: 'info' });
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1821
1895
|
const submittedDraft = state.task;
|
|
1822
1896
|
const submittedRetryReplacement = pendingRetryReplacement;
|
|
1823
1897
|
const submittedWritingStyleScope = { accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId() };
|
|
1824
1898
|
const submittedPanelState = {
|
|
1825
|
-
mode: state.mode,
|
|
1899
|
+
mode: compact ? 'ask' : state.mode,
|
|
1826
1900
|
providerId: state.providerId,
|
|
1827
1901
|
model: state.model,
|
|
1828
1902
|
reasoningEffort: state.reasoningEffort,
|
|
1829
1903
|
speedTier: state.speedTier,
|
|
1830
1904
|
autoRecompile: state.autoRecompile !== false,
|
|
1831
1905
|
requireReviewing: state.requireReviewing === true,
|
|
1832
|
-
focusFiles: getActiveFocusFiles(),
|
|
1833
|
-
selectionContext: selectionContextView.capture()
|
|
1906
|
+
focusFiles: compact ? [] : getActiveFocusFiles(),
|
|
1907
|
+
selectionContext: compact ? null : selectionContextView.capture()
|
|
1834
1908
|
};
|
|
1835
1909
|
const queuedContext = queuedInput
|
|
1836
1910
|
? RunInputQueue.normalizeSubmissionContext(queuedInput.payload?.submissionContext) : null;
|
|
1837
1911
|
const submittedCustomInstructions = queuedContext ? queuedContext.customInstructions : getCustomInstructionsForCurrentProject();
|
|
1838
1912
|
const submittedSkillLoadingSettings = queuedContext ? queuedContext.skillLoadingSettings : getSkillLoadingSettings();
|
|
1839
|
-
const submittedAttachments = queuedInput ? [] : getComposerAttachmentsForRun();
|
|
1840
|
-
const submittedSkillInvocation = queuedContext ? queuedContext.skillInvocation : getComposerSkillInvocationForRun();
|
|
1913
|
+
const submittedAttachments = queuedInput || compact ? [] : getComposerAttachmentsForRun();
|
|
1914
|
+
const submittedSkillInvocation = compact ? null : queuedContext ? queuedContext.skillInvocation : getComposerSkillInvocationForRun();
|
|
1841
1915
|
let submittedEnabledSkillIds = queuedContext ? queuedContext.enabledCodexOverleafSkillIds : getEnabledCodexOverleafSkillIds();
|
|
1842
1916
|
const submittedSkillEnabled = { ...state.codexOverleafSkillEnabled };
|
|
1843
1917
|
const submittedLocalSkillEnabled = { ...state.codexLocalSkillEnabled };
|
|
1844
1918
|
const submittedSelectionRevision = selectionContextView.getRevision?.();
|
|
1845
|
-
let submittedEnabledLocalSkillIds = queuedContext ? queuedContext.enabledCodexLocalSkillIds : [];
|
|
1846
|
-
|
|
1919
|
+
let submittedEnabledLocalSkillIds = queuedContext ? queuedContext.enabledCodexLocalSkillIds : compact ? getEnabledCodexLocalSkillIds() : [];
|
|
1920
|
+
// Capture submission first; a save must not adopt text typed while it waits.
|
|
1921
|
+
if (!queuedInput) await flushComposerDraft({ requireDurable: true });
|
|
1922
|
+
if (!compact && !queuedInput && submittedSkillLoadingSettings.loadCodexLocalSkills !== false) {
|
|
1847
1923
|
const localSkills = await primeCodexLocalSkillsCatalog({ required: true, force: submittedSkillInvocation?.scope === 'codex-local' });
|
|
1848
1924
|
submittedEnabledLocalSkillIds = localSkills.filter(skill => Object.hasOwn(submittedLocalSkillEnabled, skill.id)
|
|
1849
1925
|
? submittedLocalSkillEnabled[skill.id] !== false : skill.enabled !== false).map(skill => skill.id);
|
|
@@ -1853,7 +1929,7 @@
|
|
|
1853
1929
|
throw new Error(tx('The selected local Codex skill is unavailable or disabled. Select an enabled skill and try again.',
|
|
1854
1930
|
'所选 Codex 本地技能不可用或已关闭,请选择已启用的技能后重试。'));
|
|
1855
1931
|
}
|
|
1856
|
-
if (!queuedInput && submittedSkillLoadingSettings.loadCodexOverleafSkills !== false) {
|
|
1932
|
+
if (!compact && !queuedInput && submittedSkillLoadingSettings.loadCodexOverleafSkills !== false) {
|
|
1857
1933
|
const skills = await primeCodexOverleafSkillsCatalog({ required: true });
|
|
1858
1934
|
submittedEnabledSkillIds = skills.map(skill => String(skill?.id || '').trim())
|
|
1859
1935
|
.filter(id => id && submittedSkillEnabled[id] !== false);
|
|
@@ -1862,18 +1938,19 @@
|
|
|
1862
1938
|
const pendingMirror = getPendingMirrorRefresh?.();
|
|
1863
1939
|
if (pendingMirror) await pendingMirror.catch(() => {});
|
|
1864
1940
|
await providerSettingsCoordinator.ensureLoaded();
|
|
1865
|
-
if (!queuedInput) await writingStyleSettings.ensureLoaded(true);
|
|
1941
|
+
if (!queuedInput && !compact) await writingStyleSettings.ensureLoaded(true);
|
|
1866
1942
|
if (submittedWritingStyleScope.accountScopeId !== cachedAccountScopeId
|
|
1867
1943
|
|| submittedWritingStyleScope.projectId !== getCurrentProjectId()) {
|
|
1868
1944
|
throw new Error(tx('The active project changed before submission. Submit the task again.',
|
|
1869
1945
|
'提交前活动项目已改变,请重新提交任务。'));
|
|
1870
1946
|
}
|
|
1871
|
-
if (!findSessionById(submittedSessionId) || currentRunView
|
|
1947
|
+
if (!findSessionById(submittedSessionId) || currentRunView
|
|
1948
|
+
|| (compact && findSessionById(submittedSessionId)?.codexThreadId !== submittedThreadId)) {
|
|
1872
1949
|
throw new Error(tx('The submitted session changed before submission. Submit the task again.',
|
|
1873
1950
|
'提交所属会话已改变,请重新提交任务。'));
|
|
1874
1951
|
}
|
|
1875
|
-
submittedPanelState.writingStyle = queuedInput ? null : writingStyleSettings.capture();
|
|
1876
|
-
|
|
1952
|
+
submittedPanelState.writingStyle = queuedInput || compact ? null : writingStyleSettings.capture();
|
|
1953
|
+
let executionSnapshot = queuedInput
|
|
1877
1954
|
? RunExecutionSnapshot.resolveForExecution(
|
|
1878
1955
|
RunExecutionSnapshot.fromQueuePayload(queuedInput.payload),
|
|
1879
1956
|
{ catalog: providerSettingsCoordinator.getCatalog() }
|
|
@@ -1881,8 +1958,8 @@
|
|
|
1881
1958
|
: captureCurrentExecutionSnapshot(submittedPanelState);
|
|
1882
1959
|
// Freeze submitted run identity before the panel can change during the run.
|
|
1883
1960
|
const submittedMode = executionSnapshot.mode;
|
|
1884
|
-
|
|
1885
|
-
const replaceRunId = queuedInput ? '' : runController.resolveRetryReplacement({
|
|
1961
|
+
let submittedRequireReviewing = executionSnapshot.requireReviewing === true;
|
|
1962
|
+
const replaceRunId = queuedInput || compact ? '' : runController.resolveRetryReplacement({
|
|
1886
1963
|
candidate: submittedRetryReplacement,
|
|
1887
1964
|
activeSessionId: submittedSessionId,
|
|
1888
1965
|
runs: findSessionById(submittedSessionId)?.runs,
|
|
@@ -1892,26 +1969,78 @@
|
|
|
1892
1969
|
runCancellationRequested = false;
|
|
1893
1970
|
runCancellationController = new AbortController();
|
|
1894
1971
|
setRunning(true);
|
|
1895
|
-
|
|
1896
|
-
sessionId: submittedSessionId,
|
|
1897
|
-
sessionContext: submittedSession,
|
|
1898
|
-
codexThreadId: submittedThreadId,
|
|
1899
|
-
task,
|
|
1900
|
-
mode: submittedMode,
|
|
1901
|
-
model: executionSnapshot.model,
|
|
1902
|
-
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
1903
|
-
speedTier: executionSnapshot.speedTier,
|
|
1904
|
-
executionSnapshot,
|
|
1905
|
-
attachments: submittedAttachments,
|
|
1906
|
-
skillInvocation: submittedSkillInvocation,
|
|
1907
|
-
queueItemId: queuedInput?.id || '',
|
|
1908
|
-
replaceRunId
|
|
1909
|
-
});
|
|
1910
|
-
if (!queuedInput && pendingRetryReplacement === submittedRetryReplacement) pendingRetryReplacement = null;
|
|
1911
|
-
const runSessionId = currentRunView.sessionId;
|
|
1972
|
+
const runSessionId = submittedSessionId;
|
|
1912
1973
|
let runAuditDraft = null;
|
|
1913
1974
|
let localSkillRestorePoint = null;
|
|
1914
1975
|
try {
|
|
1976
|
+
currentRunView = startRunView({
|
|
1977
|
+
sessionId: submittedSessionId,
|
|
1978
|
+
sessionContext: submittedSession,
|
|
1979
|
+
codexThreadId: submittedThreadId,
|
|
1980
|
+
operation: compact ? 'compact' : undefined,
|
|
1981
|
+
task,
|
|
1982
|
+
mode: submittedMode,
|
|
1983
|
+
model: executionSnapshot.model,
|
|
1984
|
+
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
1985
|
+
speedTier: executionSnapshot.speedTier,
|
|
1986
|
+
executionSnapshot,
|
|
1987
|
+
attachments: submittedAttachments,
|
|
1988
|
+
skillInvocation: submittedSkillInvocation,
|
|
1989
|
+
queueItemId: queuedInput?.id || '',
|
|
1990
|
+
replaceRunId
|
|
1991
|
+
});
|
|
1992
|
+
// Preparation can await project/audit work before a Native request exists.
|
|
1993
|
+
// Give other pages a live owner to check throughout that interval.
|
|
1994
|
+
const bound = await awaitRunStep(activeTurnControl.bindBrowserRun({
|
|
1995
|
+
requestId: ActiveTurnControl.getStartupRequestId(runSessionId, currentRunView.recordId),
|
|
1996
|
+
projectKey: currentRunView.runProjectId, clientRunId: currentRunView.recordId,
|
|
1997
|
+
sessionId: runSessionId, ownerToken: crypto.randomUUID()
|
|
1998
|
+
}));
|
|
1999
|
+
throwIfRunCancellationRequested();
|
|
2000
|
+
if (!bound) throw new Error(tx('Could not start the task. Reload the page and try again.',
|
|
2001
|
+
'无法启动任务,请刷新页面后重试。'));
|
|
2002
|
+
renderContextUsage();
|
|
2003
|
+
if (!queuedInput && !compact && pendingRetryReplacement === submittedRetryReplacement) pendingRetryReplacement = null;
|
|
2004
|
+
if (compact) {
|
|
2005
|
+
announceCrossTabRunStart();
|
|
2006
|
+
// Only the slash-command text is consumed; a draft kept while compacting from the popover stays.
|
|
2007
|
+
if (options.consumeCompactDraft === true && state.activeSessionId === submittedSessionId
|
|
2008
|
+
&& /^\/[^\s/]*$/.test(String(submittedDraft || '').trim())
|
|
2009
|
+
&& panel?.querySelector('[data-task]')?.value === submittedDraft) {
|
|
2010
|
+
panel.querySelector('[data-task]').value = '';
|
|
2011
|
+
updateSessionById(submittedSessionId, { task: '' });
|
|
2012
|
+
recordSubmittedDraftConsumption(submittedSessionId, submittedDraft);
|
|
2013
|
+
composerDraftEditedHere = false;
|
|
2014
|
+
composerDraftRevision += 1;
|
|
2015
|
+
autosizeTaskTextarea();
|
|
2016
|
+
closeSlashMenu();
|
|
2017
|
+
syncComposerSendAvailability(true);
|
|
2018
|
+
}
|
|
2019
|
+
appendRunEvent({ title: tr('contextUsageCompacting'), status: 'running',
|
|
2020
|
+
activity: { v: 1, kind: 'lifecycle', state: 'running', target: 'context_compaction' } });
|
|
2021
|
+
renderContextUsage();
|
|
2022
|
+
const response = await awaitRunStep(sendTrackedCodexRun({
|
|
2023
|
+
operation: 'compact', task: '/compact', mode: 'ask',
|
|
2024
|
+
projectId: currentRunView.runProjectId, accountScopeId: currentRunView.runAccountScopeId,
|
|
2025
|
+
clientRunId: currentRunView.recordId, clientSessionId: runSessionId,
|
|
2026
|
+
threadId: submittedThreadId, model: executionSnapshot.model,
|
|
2027
|
+
reasoningEffort: executionSnapshot.reasoningEffort, speedTier: executionSnapshot.speedTier,
|
|
2028
|
+
providerSelection: RunExecutionSnapshot.toProviderSelection(executionSnapshot),
|
|
2029
|
+
locale: getLocale(), ...submittedSkillLoadingSettings,
|
|
2030
|
+
enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
|
|
2031
|
+
enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds
|
|
2032
|
+
}));
|
|
2033
|
+
if (!response?.ok) throw Object.assign(new Error(response?.error?.message || 'Compaction failed'), response?.error);
|
|
2034
|
+
if (response.result?.operation !== 'compact' || response.result?.status !== 'completed'
|
|
2035
|
+
|| response.result?.threadId !== submittedThreadId) {
|
|
2036
|
+
throw Object.assign(new Error('Compaction completion was not confirmed'), { code: 'codex_compact_unconfirmed' });
|
|
2037
|
+
}
|
|
2038
|
+
throwIfRunCancellationRequested();
|
|
2039
|
+
appendCompletionReport({ conclusion: tr('compactCompleted'), conclusionKey: 'context_compacted',
|
|
2040
|
+
status: 'completed', mode: 'ask', operations: [], applyResults: [] });
|
|
2041
|
+
await finishRunView(tr('compactCompleted'), 'completed');
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
1915
2044
|
if (queuedInput?.id) {
|
|
1916
2045
|
await runQueueScheduler?.markExecuting(runSessionId, queuedInput.id, currentRunView.recordId);
|
|
1917
2046
|
}
|
|
@@ -1990,7 +2119,30 @@
|
|
|
1990
2119
|
await awaitRunStep(pauseOtWarmMirror('run-start'));
|
|
1991
2120
|
const writeSafety = await awaitRunStep(preflightWriteSafety({
|
|
1992
2121
|
mode: submittedMode,
|
|
1993
|
-
requireReviewing: submittedRequireReviewing
|
|
2122
|
+
requireReviewing: submittedRequireReviewing,
|
|
2123
|
+
onUntracked: async choice => {
|
|
2124
|
+
submittedRequireReviewing = false;
|
|
2125
|
+
// Keep the effective policy identical in dispatch, recovery and Undo consumers.
|
|
2126
|
+
const view = currentRunView;
|
|
2127
|
+
const record = findRunRecord(view.recordId, view.sessionId);
|
|
2128
|
+
executionSnapshot = RunExecutionSnapshot.applyUntrackedDecision(view, record, choice);
|
|
2129
|
+
if (choice === 'disable') {
|
|
2130
|
+
updateSessionById(view.sessionId, { requireReviewing: false });
|
|
2131
|
+
if (state.activeSessionId === view.sessionId) {
|
|
2132
|
+
state.requireReviewing = false;
|
|
2133
|
+
panel.querySelector('[data-require-reviewing]').checked = false;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
appendRunEvent({ title: tr('reviewingUnavailableRunUntracked'), status: 'completed',
|
|
2137
|
+
technicalDetail: { code: 'reviewing_unavailable', submittedRequireReviewing: true,
|
|
2138
|
+
effectiveRequireReviewing: false, choice } });
|
|
2139
|
+
saveStateSoon(0);
|
|
2140
|
+
syncProbeStatus();
|
|
2141
|
+
runAuditDraft = await finalizeAuditDraftPartial(runAuditDraft, {
|
|
2142
|
+
requireReviewing: false, trackingDecision: choice
|
|
2143
|
+
});
|
|
2144
|
+
await flushQueuedSaveState({ requireDurable: true });
|
|
2145
|
+
}
|
|
1994
2146
|
}));
|
|
1995
2147
|
if (!writeSafety.ok) {
|
|
1996
2148
|
await finalizeAuditRecord(runAuditDraft, {
|
|
@@ -2052,7 +2204,7 @@
|
|
|
2052
2204
|
fileCount: (project?.files || []).length
|
|
2053
2205
|
}
|
|
2054
2206
|
});
|
|
2055
|
-
appendCompletionReport({
|
|
2207
|
+
appendCompletionReport({ conclusionKey: 'full_project_unread',
|
|
2056
2208
|
conclusion: tx('This run did not continue: the full Overleaf project was not read.', '这轮没有继续:没有读到完整的 Overleaf 项目内容。'),
|
|
2057
2209
|
status: 'blocked',
|
|
2058
2210
|
operations: [],
|
|
@@ -2239,12 +2391,17 @@
|
|
|
2239
2391
|
const userChoice = await showThreadResumeFailedPrompt();
|
|
2240
2392
|
if (userChoice === 'new') {
|
|
2241
2393
|
updateSessionById(runSessionId, { codexThreadId: '' });
|
|
2394
|
+
renderContextUsage();
|
|
2242
2395
|
const StorageDb = Modules.StorageDb;
|
|
2243
|
-
|
|
2244
|
-
|
|
2396
|
+
// A live session goes through the ordinary writer. An envelope
|
|
2397
|
+
// conflict there must not abort the run that is choosing a new thread.
|
|
2398
|
+
if (findSessionById(runSessionId)) {
|
|
2399
|
+
saveStateSoon();
|
|
2400
|
+
} else if (StorageDb) {
|
|
2401
|
+
const record = await StorageDb.getRecord('sessions', runSessionId, { adopt: false });
|
|
2245
2402
|
if (record) {
|
|
2246
2403
|
record.codexThreadId = '';
|
|
2247
|
-
await StorageDb.putRecord('sessions', record);
|
|
2404
|
+
await StorageDb.putRecord('sessions', record, { adopt: false });
|
|
2248
2405
|
}
|
|
2249
2406
|
}
|
|
2250
2407
|
appendRunEvent({ title: tx('Creating a new Codex conversation thread.', '正在新建 Codex 会话线程。'), status: 'running' });
|
|
@@ -2291,15 +2448,20 @@
|
|
|
2291
2448
|
// Structured FailureReason (§9.7): split bridge-availability from
|
|
2292
2449
|
// Codex-side errors. `native_connection_failed` / `native_unavailable`
|
|
2293
2450
|
// / `native_update_required` indicate the bridge itself is missing,
|
|
2294
|
-
// so emit `native_bridge_unavailable` (blocked, retryable).
|
|
2295
|
-
//
|
|
2296
|
-
// `codex_no_usable_result` (error, retryable).
|
|
2451
|
+
// so emit `native_bridge_unavailable` (blocked, retryable). Recognized
|
|
2452
|
+
// Codex failures retain their specific cause; only unmatched errors
|
|
2453
|
+
// fall back to `codex_no_usable_result` (error, retryable).
|
|
2297
2454
|
// Map native/codex failures onto their SPECIFIC catalog codes — the
|
|
2298
2455
|
// timeout/not-found/output-limit entries (and their bilingual next
|
|
2299
2456
|
// steps) existed but were never emitted, so everything collapsed into
|
|
2300
2457
|
// "no usable result" and pointed users at the wrong recovery.
|
|
2301
2458
|
const runErrorMessage = String(response.error?.message || '');
|
|
2302
|
-
const codexRunFailure =
|
|
2459
|
+
const codexRunFailure = translated.failureCode === 'codex_context_limit'
|
|
2460
|
+
? buildContentFailure('codex_context_limit', { path: 'codex.run' }, {
|
|
2461
|
+
technicalMessage: runErrorMessage,
|
|
2462
|
+
evidence: { errorCode: response.error?.code || '' }
|
|
2463
|
+
})
|
|
2464
|
+
: response.error?.code === 'codex_local_skill_unavailable'
|
|
2303
2465
|
? buildContentFailure('codex_local_skill_unavailable', { path: 'codex.run' }, { technicalMessage: runErrorMessage })
|
|
2304
2466
|
: translated.failureCode === 'codex_local_skills_failed'
|
|
2305
2467
|
? buildContentFailure('codex_local_skills_failed', { path: 'codex.run' }, { technicalMessage: runErrorMessage })
|
|
@@ -2360,7 +2522,7 @@
|
|
|
2360
2522
|
detail: response.error
|
|
2361
2523
|
});
|
|
2362
2524
|
appendCompletionReport({
|
|
2363
|
-
conclusion: translated.conclusion,
|
|
2525
|
+
conclusion: translated.conclusion, conclusionKey: translated.conclusionKey,
|
|
2364
2526
|
status: response.error?.code === 'project_locked' ? 'blocked' : 'failed',
|
|
2365
2527
|
operations: [],
|
|
2366
2528
|
applyResults: [],
|
|
@@ -2410,7 +2572,7 @@
|
|
|
2410
2572
|
status: 'failed',
|
|
2411
2573
|
failure: emptyFailure
|
|
2412
2574
|
});
|
|
2413
|
-
appendCompletionReport({
|
|
2575
|
+
appendCompletionReport({ conclusionKey: 'empty_result',
|
|
2414
2576
|
conclusion: emptyResultConclusion,
|
|
2415
2577
|
status: 'failed',
|
|
2416
2578
|
operations: [],
|
|
@@ -2514,12 +2676,14 @@
|
|
|
2514
2676
|
if (returnedThreadId && returnedThreadId !== codexThreadId) {
|
|
2515
2677
|
updateSessionById(runSessionId, { codexThreadId: returnedThreadId });
|
|
2516
2678
|
const StorageDb = Modules.StorageDb;
|
|
2517
|
-
|
|
2518
|
-
|
|
2679
|
+
// The live session is saved below with its latest draft. Writing an
|
|
2680
|
+
// older DB snapshot here can give stale text a newer timestamp.
|
|
2681
|
+
if (StorageDb && !findSessionById(runSessionId)) {
|
|
2682
|
+
const record = await StorageDb.getRecord('sessions', runSessionId, { adopt: false });
|
|
2519
2683
|
if (record) {
|
|
2520
2684
|
record.codexThreadId = returnedThreadId;
|
|
2521
2685
|
record.updatedAt = new Date().toISOString();
|
|
2522
|
-
await StorageDb.putRecord('sessions', record);
|
|
2686
|
+
await StorageDb.putRecord('sessions', record, { adopt: false });
|
|
2523
2687
|
}
|
|
2524
2688
|
}
|
|
2525
2689
|
}
|
|
@@ -2540,17 +2704,43 @@
|
|
|
2540
2704
|
}
|
|
2541
2705
|
} catch (error) {
|
|
2542
2706
|
if (runCancellationRequested || isRunCancellationError(error)) {
|
|
2543
|
-
|
|
2707
|
+
if (currentRunView?.reviewingUnavailablePreflight) {
|
|
2708
|
+
appendCompletionReport({ conclusionKey: 'launch_not_started',
|
|
2709
|
+
conclusion: tx('This task did not start: Codex did not run and no files were written.', '这轮任务没有开始:Codex 没有运行,也没有写入文件。'),
|
|
2710
|
+
status: 'blocked', operations: [], applyResults: [],
|
|
2711
|
+
failure: buildContentFailure('reviewing_unavailable', {}, {
|
|
2712
|
+
changedDocument: false, evidence: { declinedChoice: true }
|
|
2713
|
+
}),
|
|
2714
|
+
nextStep: tr('failureReason_reviewing_unavailable_cancelled_next') });
|
|
2715
|
+
} else appendRunCancelledReport();
|
|
2544
2716
|
await finishRunView(tx('Cancelled', '已取消'), 'cancelled');
|
|
2545
2717
|
void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
|
|
2546
2718
|
return;
|
|
2547
2719
|
}
|
|
2720
|
+
if (compact) {
|
|
2721
|
+
const key = error.code === 'codex_compact_unsupported' ? 'compactUnsupported'
|
|
2722
|
+
: ['project_locked', 'codex_compact_thread_busy'].includes(error.code) ? 'compactBusy' : 'compactFailed';
|
|
2723
|
+
const contextLimit = FailureReasons.isContextLimitError(error.message, error.code)
|
|
2724
|
+
? translateRawError(error.message, { locale: getLocale(), code: error.code }) : null;
|
|
2725
|
+
const failure = contextLimit ? buildContentFailure('codex_context_limit', { path: 'codex.run' }, {
|
|
2726
|
+
technicalMessage: error.message, evidence: { errorCode: error.code || '' }
|
|
2727
|
+
}) : undefined;
|
|
2728
|
+
const conclusion = contextLimit?.conclusion || tr(key);
|
|
2729
|
+
appendRunEvent({ title: conclusion, status: 'failed', failure,
|
|
2730
|
+
technicalDetail: { code: error.code || '', message: error.message } });
|
|
2731
|
+
appendCompletionReport({ conclusion, conclusionKey: contextLimit?.conclusionKey || (key === 'compactUnsupported'
|
|
2732
|
+
? 'compact_unsupported' : key === 'compactBusy' ? 'compact_busy' : 'compact_failed'),
|
|
2733
|
+
nextStep: contextLimit?.nextStep, failure,
|
|
2734
|
+
status: 'failed', mode: 'ask', operations: [], applyResults: [] });
|
|
2735
|
+
await finishRunView(conclusion, 'failed');
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2548
2738
|
// codexReturned: assistantMessage arrived on the stream before the
|
|
2549
2739
|
// exception escaped, so Codex's answer is preserved in the conversation
|
|
2550
2740
|
// — the fallback conclusion must reflect that instead of saying
|
|
2551
2741
|
// 'local Codex returned no usable result'.
|
|
2552
2742
|
const codexReturned = Boolean(getAssistantAnswerForCurrentRun());
|
|
2553
|
-
const translated = translateRawError(error.message, { mode: submittedMode, locale: getLocale(), codexReturned });
|
|
2743
|
+
const translated = translateRawError(error.message, { mode: submittedMode, locale: getLocale(), code: error.code, codexReturned });
|
|
2554
2744
|
// When translateRawError maps the raw error to a FailureReason code,
|
|
2555
2745
|
// attach the structured failure to the run-event and completion-report
|
|
2556
2746
|
// so the run record carries machine-readable failure data alongside
|
|
@@ -2578,7 +2768,7 @@
|
|
|
2578
2768
|
}
|
|
2579
2769
|
});
|
|
2580
2770
|
appendCompletionReport({
|
|
2581
|
-
conclusion: translated.conclusion,
|
|
2771
|
+
conclusion: translated.conclusion, conclusionKey: translated.conclusionKey,
|
|
2582
2772
|
status: 'failed',
|
|
2583
2773
|
operations: [],
|
|
2584
2774
|
applyResults: [],
|
|
@@ -2592,6 +2782,10 @@
|
|
|
2592
2782
|
blockedFiles: [{ path: 'task', reason: error.message }]
|
|
2593
2783
|
});
|
|
2594
2784
|
await finishRunView(tx('Task failed', '任务失败'), 'failed');
|
|
2785
|
+
// A failed initial card render must still leave a visible terminal record.
|
|
2786
|
+
if (currentRunView?.sessionId === state.activeSessionId && !getCurrentRunViewForRender()) {
|
|
2787
|
+
renderRunHistory({ preserveScroll: true });
|
|
2788
|
+
}
|
|
2595
2789
|
} finally {
|
|
2596
2790
|
const settledView = currentRunView;
|
|
2597
2791
|
const settledRecord = settledView ? findRunRecord(settledView.recordId, settledView.sessionId) : null;
|
|
@@ -2647,6 +2841,7 @@
|
|
|
2647
2841
|
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
2648
2842
|
speedTier: executionSnapshot.speedTier,
|
|
2649
2843
|
session: currentRunView?.sessionContext,
|
|
2844
|
+
locale: getLocale(),
|
|
2650
2845
|
clientRunId: currentRunView?.recordId || '',
|
|
2651
2846
|
clientSessionId: runSessionId,
|
|
2652
2847
|
accountScopeId: currentRunView?.runAccountScopeId || '',
|
|
@@ -2688,7 +2883,7 @@
|
|
|
2688
2883
|
failure: translatedFailure
|
|
2689
2884
|
});
|
|
2690
2885
|
appendCompletionReport({
|
|
2691
|
-
conclusion: translated.conclusion,
|
|
2886
|
+
conclusion: translated.conclusion, conclusionKey: translated.conclusionKey,
|
|
2692
2887
|
status: 'failed',
|
|
2693
2888
|
operations: [],
|
|
2694
2889
|
applyResults: [],
|
|
@@ -2706,7 +2901,7 @@
|
|
|
2706
2901
|
}
|
|
2707
2902
|
|
|
2708
2903
|
const assistantMessage = response.result?.assistantMessage || getAssistantAnswerForCurrentRun();
|
|
2709
|
-
appendCompletionReport({
|
|
2904
|
+
appendCompletionReport({ conclusionKey: !assistantMessage ? 'skill_installer_completed' : undefined,
|
|
2710
2905
|
conclusion: assistantMessage || tx('Skill installer completed.', 'Skill installer 已完成。'),
|
|
2711
2906
|
status: 'completed',
|
|
2712
2907
|
operations: [],
|
|
@@ -2740,11 +2935,14 @@
|
|
|
2740
2935
|
if (returnedThreadId && returnedThreadId !== codexThreadId) {
|
|
2741
2936
|
updateSessionById(runSessionId, { codexThreadId: returnedThreadId });
|
|
2742
2937
|
const StorageDb = Modules.StorageDb;
|
|
2743
|
-
|
|
2744
|
-
|
|
2938
|
+
// The installer already finished; a later envelope conflict only delays this save.
|
|
2939
|
+
if (findSessionById(runSessionId)) {
|
|
2940
|
+
saveStateSoon();
|
|
2941
|
+
} else if (StorageDb) {
|
|
2942
|
+
const record = await StorageDb.getRecord('sessions', runSessionId, { adopt: false });
|
|
2745
2943
|
if (record) {
|
|
2746
2944
|
record.codexThreadId = returnedThreadId;
|
|
2747
|
-
await StorageDb.putRecord('sessions', record);
|
|
2945
|
+
await StorageDb.putRecord('sessions', record, { adopt: false });
|
|
2748
2946
|
}
|
|
2749
2947
|
}
|
|
2750
2948
|
}
|
|
@@ -2758,8 +2956,6 @@
|
|
|
2758
2956
|
if (mode === 'ask') {
|
|
2759
2957
|
return { ok: true, skipped: true };
|
|
2760
2958
|
}
|
|
2761
|
-
const method = requireReviewing ? 'ensureReviewing' : 'ensureEditing';
|
|
2762
|
-
|
|
2763
2959
|
appendRunEvent({
|
|
2764
2960
|
title: requireReviewing
|
|
2765
2961
|
? tx('Checking Overleaf Reviewing/Track Changes before starting.', '正在确认 Overleaf 留痕状态。')
|
|
@@ -2767,30 +2963,67 @@
|
|
|
2767
2963
|
status: 'running'
|
|
2768
2964
|
});
|
|
2769
2965
|
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
}
|
|
2966
|
+
const owner = currentRunView;
|
|
2967
|
+
const signal = runCancellationController?.signal;
|
|
2968
|
+
const isCurrent = () => owner === currentRunView && !signal?.aborted && !runCancellationRequested
|
|
2969
|
+
&& owner?.runProjectId === getCurrentProjectId() && owner?.runAccountScopeId === cachedAccountScopeId;
|
|
2970
|
+
const assertCurrent = () => { if (!isCurrent()) throw createRunCancellationError(); };
|
|
2971
|
+
assertCurrent();
|
|
2972
|
+
if (requireReviewing) {
|
|
2973
|
+
// Another tab may have observed this project's capability since hydration.
|
|
2974
|
+
const prefs = await Modules.StorageMigration.loadPrefs(owner.runAccountScopeId, owner.runProjectId);
|
|
2975
|
+
assertCurrent();
|
|
2976
|
+
const incoming = prefs?.reviewingAvailabilityByProject?.[owner.runProjectId];
|
|
2977
|
+
const current = projectSettingsCoordinator.getReviewingAvailability();
|
|
2978
|
+
if (incoming && (!current || incoming.observedAt > current.observedAt
|
|
2979
|
+
|| (incoming.observedAt === current.observedAt && incoming.status === 'available'))) {
|
|
2980
|
+
state = { ...state, reviewingAvailabilityByProject: {
|
|
2981
|
+
...state.reviewingAvailabilityByProject, [owner.runProjectId]: incoming
|
|
2982
|
+
} };
|
|
2983
|
+
syncProbeStatus();
|
|
2984
|
+
}
|
|
2789
2985
|
}
|
|
2986
|
+
let result = await Modules.Reviewing.resolveWritePolicy({ requireReviewing,
|
|
2987
|
+
knownUnavailable: isReviewingKnownUnavailable() }, {
|
|
2988
|
+
assertCurrent,
|
|
2989
|
+
ensureMode: async required => {
|
|
2990
|
+
try {
|
|
2991
|
+
return await callPageBridge(required ? 'ensureReviewing' : 'ensureEditing', {
|
|
2992
|
+
waitMs: 1800, runProjectId: owner.runProjectId
|
|
2993
|
+
});
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
assertCurrent();
|
|
2996
|
+
return { ok: false, code: error?.code, reason: error?.message, reviewing: null };
|
|
2997
|
+
}
|
|
2998
|
+
},
|
|
2999
|
+
observe: async (status, evidence) => {
|
|
3000
|
+
projectSettingsCoordinator.observeReviewingAvailability(status, evidence);
|
|
3001
|
+
syncProbeStatus();
|
|
3002
|
+
// Publish a detected paywall before waiting for the user's decision.
|
|
3003
|
+
if (status === 'unavailable') await flushQueuedSaveState({ requireDurable: true });
|
|
3004
|
+
assertCurrent();
|
|
3005
|
+
},
|
|
3006
|
+
choose: () => {
|
|
3007
|
+
owner.reviewingUnavailablePreflight = true;
|
|
3008
|
+
return showPluginConfirm({
|
|
3009
|
+
title: tr('reviewingUnavailableTitle'),
|
|
3010
|
+
message: tr('reviewingUnavailableChoiceMessage'),
|
|
3011
|
+
choices: [
|
|
3012
|
+
{ value: 'continue', label: tr('reviewingUnavailableContinue') },
|
|
3013
|
+
{ value: 'disable', label: tr('reviewingUnavailableDisable'), secondary: true },
|
|
3014
|
+
{ value: 'cancel', label: tr('confirmDefaultCancel') }
|
|
3015
|
+
],
|
|
3016
|
+
signal, isCurrent, timeoutMs: REVIEWING_CONFIRM_TIMEOUT_MS
|
|
3017
|
+
});
|
|
3018
|
+
},
|
|
3019
|
+
applyUntracked: choice => { assertCurrent(); return options.onUntracked(choice); }
|
|
3020
|
+
});
|
|
3021
|
+
if (result.ok) owner.reviewingUnavailablePreflight = false;
|
|
3022
|
+
const effectiveRequireReviewing = result.requireReviewing ?? requireReviewing;
|
|
2790
3023
|
|
|
2791
3024
|
if (result?.ok) {
|
|
2792
3025
|
appendRunEvent({
|
|
2793
|
-
title:
|
|
3026
|
+
title: effectiveRequireReviewing
|
|
2794
3027
|
? (result.activated
|
|
2795
3028
|
? tx('Overleaf Reviewing/Track Changes is now on. Starting the task.', '已开启 Overleaf 留痕,开始处理任务。')
|
|
2796
3029
|
: tx('Overleaf Reviewing/Track Changes is already on. Starting the task.', 'Overleaf 留痕已经开启,开始处理任务。'))
|
|
@@ -2802,10 +3035,23 @@
|
|
|
2802
3035
|
return result;
|
|
2803
3036
|
}
|
|
2804
3037
|
|
|
2805
|
-
const
|
|
3038
|
+
const unavailable = result?.code === 'reviewing_unavailable';
|
|
3039
|
+
if (unavailable && !result.failure) result.failure = buildContentFailure('reviewing_unavailable', {}, {
|
|
3040
|
+
changedDocument: false, evidence: result.evidence
|
|
3041
|
+
});
|
|
3042
|
+
const reason = unavailable ? tr('failureReason_reviewing_unavailable_user') : localizeVisibleReason(result?.reason || (effectiveRequireReviewing
|
|
2806
3043
|
? tx('Overleaf did not return track changes status', 'Overleaf 没有返回留痕状态')
|
|
2807
3044
|
: tx('Overleaf did not return Editing mode status', 'Overleaf 没有返回 Editing 模式状态')));
|
|
2808
|
-
|
|
3045
|
+
// Affirmative choices retain trackingDecision even if the Editing check fails.
|
|
3046
|
+
const declinedReviewingChoice = owner.reviewingUnavailablePreflight === true && !result.trackingDecision;
|
|
3047
|
+
const reportFailure = unavailable && declinedReviewingChoice
|
|
3048
|
+
? buildContentFailure('reviewing_unavailable', {}, {
|
|
3049
|
+
...result.failure, changedDocument: false,
|
|
3050
|
+
evidence: { ...result.failure?.evidence, declinedChoice: true }
|
|
3051
|
+
}) : result?.failure;
|
|
3052
|
+
const nextStep = unavailable ? tr(declinedReviewingChoice
|
|
3053
|
+
? 'failureReason_reviewing_unavailable_cancelled_next'
|
|
3054
|
+
: 'failureReason_reviewing_unavailable_next') : effectiveRequireReviewing
|
|
2809
3055
|
? tx(
|
|
2810
3056
|
'You may not have permission, or this Overleaf page did not expose the Reviewing switch. Switch to Reviewing manually and retry, or turn off Track before writing.',
|
|
2811
3057
|
'你可能没有权限,或 Overleaf 当前页面没有暴露切换入口。可以手动切到 Reviewing 后重试,或关闭“留痕”再运行。'
|
|
@@ -2815,7 +3061,7 @@
|
|
|
2815
3061
|
'你可能没有权限,或 Overleaf 当前页面没有暴露切换入口。请在 Overleaf 手动切到 Editing 后重试。'
|
|
2816
3062
|
);
|
|
2817
3063
|
appendRunEvent({
|
|
2818
|
-
title:
|
|
3064
|
+
title: effectiveRequireReviewing
|
|
2819
3065
|
? tx('Task not started: could not enable Overleaf Reviewing/Track Changes.', '任务未开始:无法开启 Overleaf 留痕。')
|
|
2820
3066
|
: tx('Task not started: could not switch to Overleaf Editing.', '任务未开始:无法切换到 Overleaf Editing。'),
|
|
2821
3067
|
status: 'failed',
|
|
@@ -2824,24 +3070,25 @@
|
|
|
2824
3070
|
[tr('detailNext')]: nextStep
|
|
2825
3071
|
},
|
|
2826
3072
|
technicalDetail: {
|
|
2827
|
-
reason,
|
|
3073
|
+
reason, code: result?.code, evidence: result?.evidence, failure: result?.failure,
|
|
2828
3074
|
reviewing: result?.reviewing || null
|
|
2829
3075
|
}
|
|
2830
3076
|
});
|
|
2831
|
-
appendCompletionReport({
|
|
3077
|
+
appendCompletionReport({ conclusionKey: 'launch_not_started',
|
|
2832
3078
|
conclusion: tx('This task did not start: Codex did not run and no files were written.', '这轮任务没有开始:Codex 没有运行,也没有写入文件。'),
|
|
2833
3079
|
status: 'blocked',
|
|
2834
3080
|
operations: [],
|
|
2835
3081
|
applyResults: [],
|
|
2836
|
-
nextStep
|
|
3082
|
+
nextStep,
|
|
3083
|
+
reason: result?.code, failure: reportFailure
|
|
2837
3084
|
});
|
|
2838
|
-
const finishTitle =
|
|
3085
|
+
const finishTitle = effectiveRequireReviewing
|
|
2839
3086
|
? tx('Not started: could not enable Track Changes', '未开始:无法开启留痕')
|
|
2840
3087
|
: tx('Not started: could not switch to Editing', '未开始:无法切换到 Editing');
|
|
2841
3088
|
await finishRunView(finishTitle, 'failed');
|
|
2842
3089
|
return {
|
|
2843
|
-
ok: false,
|
|
2844
|
-
reason,
|
|
3090
|
+
...result, ok: false,
|
|
3091
|
+
reason: result?.code || reason,
|
|
2845
3092
|
reviewing: result?.reviewing || null
|
|
2846
3093
|
};
|
|
2847
3094
|
}
|
|
@@ -2854,17 +3101,25 @@
|
|
|
2854
3101
|
return runController.truncateSessionHistoryText(value, maxLength);
|
|
2855
3102
|
}
|
|
2856
3103
|
|
|
2857
|
-
function safeRunTask() {
|
|
3104
|
+
function safeRunTask(options = {}) {
|
|
3105
|
+
if (sessionManager.isTransitioning()) return;
|
|
2858
3106
|
if (updateIdleClient?.isApplying()) {
|
|
2859
3107
|
showPluginToast(tx('An update is being applied. Codex will be available again after reload.', '正在应用更新,重新加载后即可继续使用 Codex。'), { status: 'info' });
|
|
2860
3108
|
return;
|
|
2861
3109
|
}
|
|
3110
|
+
const submittedCompactCommand = options.operation !== 'compact'
|
|
3111
|
+
&& panel?.querySelector('[data-task]')?.value.trim() === '/compact';
|
|
3112
|
+
const compact = options.operation === 'compact' || submittedCompactCommand;
|
|
3113
|
+
if (compact && (currentRunView || runSubmissionPending)) {
|
|
3114
|
+
showPluginToast(tr('compactBusy'), { status: 'info' });
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
2862
3117
|
if (currentRunView) {
|
|
2863
3118
|
return queueComposerInput();
|
|
2864
3119
|
}
|
|
2865
3120
|
if (runSubmissionPending) return;
|
|
2866
3121
|
runSubmissionPending = true;
|
|
2867
|
-
runTask().catch(error => {
|
|
3122
|
+
runTask(compact ? { operation: 'compact', consumeCompactDraft: options.consumeCompactDraft === true || submittedCompactCommand } : {}).catch(error => {
|
|
2868
3123
|
if (!currentRunView) {
|
|
2869
3124
|
setRunning(false);
|
|
2870
3125
|
nativeChannel.clearActiveRequest();
|
|
@@ -2896,7 +3151,7 @@
|
|
|
2896
3151
|
getActiveSession: () => getActiveSession(state),
|
|
2897
3152
|
setQueue: (sessionId, pendingInputs) => updateSessionById(sessionId, { pendingInputs }),
|
|
2898
3153
|
getCurrentRun: () => currentRunView,
|
|
2899
|
-
canStart:
|
|
3154
|
+
canStart: canStartQueuedSession,
|
|
2900
3155
|
readInputs: readPanelInputs,
|
|
2901
3156
|
getTask: () => panel?.querySelector('[data-task]')?.value || '',
|
|
2902
3157
|
hasAttachments: () => getComposerAttachmentsForRun().length > 0,
|
|
@@ -2916,6 +3171,7 @@
|
|
|
2916
3171
|
}),
|
|
2917
3172
|
clearTask: () => {
|
|
2918
3173
|
panel.querySelector('[data-task]').value = '';
|
|
3174
|
+
composerDraftEditedHere = false;
|
|
2919
3175
|
selectionContextView.clear();
|
|
2920
3176
|
updateSessionById(state.activeSessionId, { task: '' });
|
|
2921
3177
|
autosizeTaskTextarea();
|
|
@@ -2926,6 +3182,8 @@
|
|
|
2926
3182
|
RunExecutionSnapshot.fromQueuePayload(item.payload),
|
|
2927
3183
|
{ catalog: providerSettingsCoordinator.getCatalog() }
|
|
2928
3184
|
);
|
|
3185
|
+
// Promotion uses its captured snapshot; this line still describes the current draft.
|
|
3186
|
+
syncProbeStatus();
|
|
2929
3187
|
},
|
|
2930
3188
|
run: item => runTask({ queuedInput: item }),
|
|
2931
3189
|
getGuideScope: () => ({ projectId: getCurrentProjectId(), accountScopeId: cachedAccountScopeId, generation: spaRouteGeneration }),
|
|
@@ -2978,7 +3236,17 @@
|
|
|
2978
3236
|
});
|
|
2979
3237
|
}
|
|
2980
3238
|
|
|
3239
|
+
function canStartQueuedSession(sessionId) {
|
|
3240
|
+
if (state?.activeSessionId !== sessionId || !isProjectEditorRoute(window.location)) return false;
|
|
3241
|
+
if (sessionManager.isTransitioning()) {
|
|
3242
|
+
deferredSessionStart = { sessionId, scope: getComposerDraftScope() };
|
|
3243
|
+
return false;
|
|
3244
|
+
}
|
|
3245
|
+
return true;
|
|
3246
|
+
}
|
|
3247
|
+
|
|
2981
3248
|
function queueComposerInput() {
|
|
3249
|
+
if (sessionManager.isTransitioning()) return;
|
|
2982
3250
|
if (getSkillLoadingSettings().loadCodexLocalSkills !== false && !Array.isArray(getCodexLocalSkillsCatalog())) {
|
|
2983
3251
|
primeCodexLocalSkillsCatalog({ required: true }).catch(() => {});
|
|
2984
3252
|
showPluginToast(tx('Local Codex skills are still loading. Try submitting again when loading finishes.',
|
|
@@ -3218,6 +3486,7 @@
|
|
|
3218
3486
|
const params = runController.buildCodexRunParams({
|
|
3219
3487
|
currentProjectId: getCurrentProjectId(),
|
|
3220
3488
|
state: submittedState,
|
|
3489
|
+
locale: getLocale(),
|
|
3221
3490
|
task,
|
|
3222
3491
|
project,
|
|
3223
3492
|
useExistingMirror,
|
|
@@ -3271,6 +3540,9 @@
|
|
|
3271
3540
|
if (!compatibilityGate.ok) {
|
|
3272
3541
|
return compatibilityGate.response;
|
|
3273
3542
|
}
|
|
3543
|
+
if (params.operation === 'compact' && compatibilityGate.compatibility?.native?.capabilities?.threadCompact !== true) {
|
|
3544
|
+
return { ok: false, error: { code: 'codex_compact_unsupported', message: 'This Native host does not support context compaction.' } };
|
|
3545
|
+
}
|
|
3274
3546
|
const tracked = nativeChannel.sendNativeTracked(nativeCompatibilityController.attachEvidence({
|
|
3275
3547
|
method: 'codex.run',
|
|
3276
3548
|
params
|
|
@@ -3298,58 +3570,44 @@
|
|
|
3298
3570
|
}
|
|
3299
3571
|
|
|
3300
3572
|
async function recoverInterruptedRunJournals() {
|
|
3301
|
-
const projectKey = getCurrentProjectId();
|
|
3573
|
+
const projectKey = getCurrentProjectId(), accountScopeId = cachedAccountScopeId;
|
|
3574
|
+
const generation = spaRouteGeneration;
|
|
3575
|
+
const isCurrent = () => generation === spaRouteGeneration
|
|
3576
|
+
&& projectKey === getCurrentProjectId() && accountScopeId === cachedAccountScopeId;
|
|
3302
3577
|
const recovery = await activeTurnControl.recoverJournals({
|
|
3303
3578
|
projectKey,
|
|
3304
|
-
accountScopeId
|
|
3579
|
+
accountScopeId,
|
|
3580
|
+
isCurrent,
|
|
3581
|
+
onRecheck: async () => {
|
|
3582
|
+
if (!isCurrent()) return;
|
|
3583
|
+
await recoverInterruptedRunJournals();
|
|
3584
|
+
if (isCurrent()) applyStateToPanel();
|
|
3585
|
+
},
|
|
3305
3586
|
requireOwnerLost: Modules.StorageDb.sharedSessionsEnabled?.() === true,
|
|
3306
3587
|
normalizeInterruptedRun: record => Object.assign(record, Modules.SessionState.normalizeRuns([record], {
|
|
3307
3588
|
restoreRunningRuns: true,
|
|
3308
3589
|
locale: getLocale()
|
|
3309
3590
|
})[0]),
|
|
3310
3591
|
findSession: findSessionById,
|
|
3592
|
+
listSessions: () => state?.sessions || [],
|
|
3593
|
+
isLiveRun: (record, session) => currentRunView?.recordId === record.id
|
|
3594
|
+
&& currentRunView.sessionId === session.id && currentRunView.runProjectId === projectKey,
|
|
3311
3595
|
getActiveSession: () => getActiveSession(state),
|
|
3312
|
-
createInterruptedRun: journal => (
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
status: 'interrupted',
|
|
3318
|
-
statusText: tr('restoredRunStoppedStatus'),
|
|
3319
|
-
runProjectId: projectKey,
|
|
3320
|
-
startedAt: journal.createdAt || new Date().toISOString(),
|
|
3321
|
-
finishedAt: journal.updatedAt || new Date().toISOString(),
|
|
3322
|
-
events: [],
|
|
3323
|
-
attachments: [],
|
|
3324
|
-
undoOperations: [],
|
|
3325
|
-
undoTrackedChanges: [],
|
|
3326
|
-
undoExpectedFiles: []
|
|
3596
|
+
createInterruptedRun: journal => ActiveTurnControl.createInterruptedRun(journal, projectKey, tr('restoredRunStoppedStatus')),
|
|
3597
|
+
onRawEvent: (raw, { record, session, sequence }) => storeContextUsage(raw, record, session, sequence),
|
|
3598
|
+
mapEvent: (raw, journal) => ActiveTurnControl.mapRecoveredEvent(raw, journal, {
|
|
3599
|
+
mapActivity: value => mapAgentEventToActivity(value, { locale: getLocale() }),
|
|
3600
|
+
sanitizeText: sanitizeAssistantVisibleText, sanitizeValue: sanitizeAssistantVisibleValue
|
|
3327
3601
|
}),
|
|
3328
|
-
mapEvent: (raw, journal) => {
|
|
3329
|
-
const activity = mapAgentEventToActivity(raw, { locale: getLocale() });
|
|
3330
|
-
if (!activity?.visible || activity.kind === 'technical') {
|
|
3331
|
-
return null;
|
|
3332
|
-
}
|
|
3333
|
-
return {
|
|
3334
|
-
kind: activity.kind || 'activity',
|
|
3335
|
-
title: sanitizeAssistantVisibleText(activity.title) || 'Event',
|
|
3336
|
-
status: activity.status || 'running',
|
|
3337
|
-
detail: sanitizeAssistantVisibleValue(activity.detail),
|
|
3338
|
-
timestamp: raw.timestamp || journal.updatedAt || new Date().toISOString(),
|
|
3339
|
-
streamKey: sanitizeAssistantVisibleText(activity.streamKey),
|
|
3340
|
-
streamRole: sanitizeAssistantVisibleText(activity.streamRole),
|
|
3341
|
-
appendText: sanitizeAssistantVisibleText(activity.appendText),
|
|
3342
|
-
replaceText: activity.replaceText
|
|
3343
|
-
};
|
|
3344
|
-
},
|
|
3345
3602
|
upsertStream: upsertRunStreamRecordEvent,
|
|
3346
3603
|
pauseSessionQueue: (session, reason) => {
|
|
3347
3604
|
session.pendingInputs = RunInputQueue.pauseAll(session.pendingInputs || [], reason);
|
|
3348
3605
|
},
|
|
3349
3606
|
maxEvents: MAX_RUN_EVENTS
|
|
3350
3607
|
});
|
|
3608
|
+
if (!isCurrent()) return;
|
|
3351
3609
|
if (recovery.changed) {
|
|
3352
|
-
if (await saveState({ recoveryRuns: recovery.recoveredRuns }) !== true) return;
|
|
3610
|
+
if (await saveState({ recoveryRuns: recovery.recoveredRuns }) !== true || !isCurrent()) return;
|
|
3353
3611
|
for (const requestId of recovery.acknowledgeIds) {
|
|
3354
3612
|
await activeTurnControl.acknowledge(requestId).catch(() => false);
|
|
3355
3613
|
}
|
|
@@ -3375,6 +3633,7 @@
|
|
|
3375
3633
|
sessionId: input.sessionId || '',
|
|
3376
3634
|
turnId: currentRunView?.recordId || '',
|
|
3377
3635
|
mode: input.mode || executionSnapshot.mode,
|
|
3636
|
+
requireReviewing: executionSnapshot.requireReviewing,
|
|
3378
3637
|
...providerSettingsCoordinator.getProviderSnapshot(executionSnapshot.providerId),
|
|
3379
3638
|
providerRevision: executionSnapshot.providerRevision,
|
|
3380
3639
|
model: executionSnapshot.model,
|
|
@@ -3453,7 +3712,7 @@
|
|
|
3453
3712
|
status: 'info',
|
|
3454
3713
|
activity: { v: 1, kind: 'lifecycle', state: 'cancelled', target: 'cancelled' }
|
|
3455
3714
|
});
|
|
3456
|
-
appendCompletionReport({
|
|
3715
|
+
appendCompletionReport({ conclusionKey: 'cancelled',
|
|
3457
3716
|
conclusion: tx('This run was cancelled. It did not continue syncing or writing to Overleaf.', '这轮已中断,没有继续同步或写入 Overleaf。'),
|
|
3458
3717
|
status: 'cancelled',
|
|
3459
3718
|
operations: [],
|
|
@@ -3482,7 +3741,8 @@
|
|
|
3482
3741
|
const running = typeof runningOverride === 'boolean'
|
|
3483
3742
|
? runningOverride
|
|
3484
3743
|
: Boolean(currentRunView);
|
|
3485
|
-
const
|
|
3744
|
+
const transitioning = sessionManager.isTransitioning();
|
|
3745
|
+
const action = running && (!hasInput || transitioning) ? 'cancel' : 'send';
|
|
3486
3746
|
const label = action === 'cancel'
|
|
3487
3747
|
? tr('cancelRun')
|
|
3488
3748
|
: running
|
|
@@ -3491,13 +3751,17 @@
|
|
|
3491
3751
|
runButton.dataset.action = action;
|
|
3492
3752
|
runButton.disabled = action === 'cancel'
|
|
3493
3753
|
? runCancellationRequested
|
|
3494
|
-
: !hasInput;
|
|
3754
|
+
: !hasInput || transitioning;
|
|
3495
3755
|
runButton.title = label;
|
|
3496
3756
|
runButton.setAttribute('aria-label', label);
|
|
3497
|
-
if (currentRunView?.writebackRetry && hasInput) runButton.disabled = true;
|
|
3757
|
+
if (currentRunView?.writebackRetry && hasInput && action !== 'cancel') runButton.disabled = true;
|
|
3498
3758
|
}
|
|
3499
3759
|
|
|
3500
3760
|
function handleTaskInputKeydown(event) {
|
|
3761
|
+
if (sessionManager.isTransitioning()) {
|
|
3762
|
+
if (event.key === 'Enter' && !event.isComposing && event.keyCode !== 229) event.preventDefault();
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3501
3765
|
if (handleSlashMenuKeydown(event)) {
|
|
3502
3766
|
return;
|
|
3503
3767
|
}
|
|
@@ -3525,9 +3789,12 @@
|
|
|
3525
3789
|
|
|
3526
3790
|
function getSlashCommands() {
|
|
3527
3791
|
const commands = [
|
|
3792
|
+
{ id: 'compact', kind: 'compact', group: 'commands', title: tr('slashCompactTitle'), slug: '/compact',
|
|
3793
|
+
subtitle: tr('slashCompactSubtitle') },
|
|
3528
3794
|
{
|
|
3529
3795
|
id: 'install-skill',
|
|
3530
3796
|
kind: 'installer',
|
|
3797
|
+
group: 'commands',
|
|
3531
3798
|
title: tr('slashInstallSkillTitle'),
|
|
3532
3799
|
subtitle: tr('slashInstallSkillSubtitle')
|
|
3533
3800
|
}
|
|
@@ -3546,6 +3813,7 @@
|
|
|
3546
3813
|
commands.push({
|
|
3547
3814
|
id: `skill:${id}`,
|
|
3548
3815
|
kind: 'codex-overleaf-skill',
|
|
3816
|
+
group: 'overleaf',
|
|
3549
3817
|
scope: 'codex-overleaf',
|
|
3550
3818
|
skillId: id,
|
|
3551
3819
|
title,
|
|
@@ -3556,11 +3824,13 @@
|
|
|
3556
3824
|
for (const skill of localSkills) {
|
|
3557
3825
|
if (!isSafeSkillId(skill.id) || !isCodexLocalSkillEnabled(skill.id, skill)) continue;
|
|
3558
3826
|
const sameName = localSkills.filter(other => String(other.title || other.name || other.id) === String(skill.title || skill.name || skill.id)).length > 1;
|
|
3559
|
-
|
|
3827
|
+
// Source and duplicate hints lead so a truncated preview cannot hide them.
|
|
3828
|
+
commands.push({ id: `local-skill:${skill.id}`, kind: 'codex-local-skill', group: 'local', scope: 'codex-local', skillId: skill.id,
|
|
3560
3829
|
title: String(skill.title || skill.name || skill.id).trim().slice(0, 80),
|
|
3561
3830
|
searchName: String(skill.name || ''),
|
|
3562
|
-
subtitle:
|
|
3563
|
-
|
|
3831
|
+
subtitle: [skill.sourceKind === 'plugin' && skill.sourceLabel ? skill.sourceLabel : '',
|
|
3832
|
+
sameName ? skill.sourceHint || skill.id.slice(-8) : '',
|
|
3833
|
+
String(skill.preview || '').trim() || tr('slashUseLocalSkillSubtitle')].filter(Boolean).join(' · ') });
|
|
3564
3834
|
}
|
|
3565
3835
|
return commands;
|
|
3566
3836
|
}
|
|
@@ -3607,7 +3877,7 @@
|
|
|
3607
3877
|
|| command.id.toLowerCase().includes(query)
|
|
3608
3878
|
|| String(command.searchName || '').toLowerCase().includes(query);
|
|
3609
3879
|
});
|
|
3610
|
-
renderSlashMenu(commands);
|
|
3880
|
+
renderSlashMenu(commands, { query });
|
|
3611
3881
|
return;
|
|
3612
3882
|
}
|
|
3613
3883
|
const atTrigger = getAtFileTrigger();
|
|
@@ -3705,7 +3975,7 @@
|
|
|
3705
3975
|
closeSlashMenu();
|
|
3706
3976
|
return;
|
|
3707
3977
|
}
|
|
3708
|
-
renderSlashMenu(entries);
|
|
3978
|
+
renderSlashMenu(entries, { query });
|
|
3709
3979
|
}
|
|
3710
3980
|
|
|
3711
3981
|
// Lazy one-shot load: the tray's file list is usually warm (panel open
|
|
@@ -3788,7 +4058,22 @@
|
|
|
3788
4058
|
};
|
|
3789
4059
|
}
|
|
3790
4060
|
|
|
3791
|
-
|
|
4061
|
+
const SLASH_MENU_GROUPS = { commands: 'slashGroupCommands', overleaf: 'slashGroupOverleafSkills', local: 'slashGroupLocalSkills' };
|
|
4062
|
+
const SLASH_MENU_ICONS = { compact: 'compact', installer: 'install', 'codex-overleaf-skill': 'book', 'codex-local-skill': 'terminal' };
|
|
4063
|
+
|
|
4064
|
+
function appendHighlightedText(node, value, query) {
|
|
4065
|
+
const text = String(value || '');
|
|
4066
|
+
const at = query ? text.toLowerCase().indexOf(query) : -1;
|
|
4067
|
+
if (at < 0) {
|
|
4068
|
+
node.append(text);
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
const mark = document.createElement('mark');
|
|
4072
|
+
mark.textContent = text.slice(at, at + query.length);
|
|
4073
|
+
node.append(text.slice(0, at), mark, text.slice(at + query.length));
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
function renderSlashMenu(commands = [], { query = '' } = {}) {
|
|
3792
4077
|
const menu = panel?.querySelector('[data-slash-menu]');
|
|
3793
4078
|
if (!menu) {
|
|
3794
4079
|
return;
|
|
@@ -3798,7 +4083,17 @@
|
|
|
3798
4083
|
menu.hidden = true;
|
|
3799
4084
|
return;
|
|
3800
4085
|
}
|
|
4086
|
+
const usage = commands.some(command => command.kind === 'compact') ? getContextUsageDisplay().usage : null;
|
|
4087
|
+
const percent = usage?.contextWindow > 0 ? usage.usedTokens / usage.contextWindow * 100 : null;
|
|
4088
|
+
let group = '';
|
|
3801
4089
|
commands.forEach((command, index) => {
|
|
4090
|
+
if (command.group && command.group !== group) {
|
|
4091
|
+
group = command.group;
|
|
4092
|
+
const label = document.createElement('div');
|
|
4093
|
+
label.className = 'codex-slash-menu-group';
|
|
4094
|
+
label.textContent = tr(SLASH_MENU_GROUPS[group]);
|
|
4095
|
+
menu.append(label);
|
|
4096
|
+
}
|
|
3802
4097
|
const button = document.createElement('button');
|
|
3803
4098
|
button.type = 'button';
|
|
3804
4099
|
button.dataset.slashCommand = command.id;
|
|
@@ -3810,13 +4105,44 @@
|
|
|
3810
4105
|
button.dataset.slashSkillScope = command.scope;
|
|
3811
4106
|
}
|
|
3812
4107
|
button.dataset.active = index === 0 ? 'true' : 'false';
|
|
4108
|
+
const icon = document.createElement('span');
|
|
4109
|
+
icon.className = 'codex-slash-menu-icon';
|
|
4110
|
+
icon.innerHTML = Modules.PanelIcons.svg(SLASH_MENU_ICONS[command.kind] || 'file');
|
|
4111
|
+
const text = document.createElement('span');
|
|
4112
|
+
text.className = 'codex-slash-menu-text';
|
|
3813
4113
|
const title = document.createElement('span');
|
|
3814
|
-
title.
|
|
4114
|
+
title.className = 'codex-slash-menu-title';
|
|
4115
|
+
appendHighlightedText(title, command.title, query);
|
|
4116
|
+
if (command.slug) {
|
|
4117
|
+
const slug = document.createElement('code');
|
|
4118
|
+
appendHighlightedText(slug, command.slug, query);
|
|
4119
|
+
title.append(slug);
|
|
4120
|
+
}
|
|
3815
4121
|
const subtitle = document.createElement('small');
|
|
3816
4122
|
subtitle.textContent = command.subtitle;
|
|
3817
|
-
|
|
4123
|
+
subtitle.title = command.subtitle;
|
|
4124
|
+
text.append(title, subtitle);
|
|
4125
|
+
button.append(icon, text);
|
|
4126
|
+
if (command.kind === 'compact' && percent !== null) {
|
|
4127
|
+
const meta = document.createElement('span');
|
|
4128
|
+
meta.className = 'codex-slash-menu-meta';
|
|
4129
|
+
meta.dataset.level = Modules.ContextUsage.level(percent);
|
|
4130
|
+
meta.title = tr('contextUsageTitle');
|
|
4131
|
+
meta.textContent = new Intl.NumberFormat(getLocale() === 'zh' ? 'zh-CN' : 'en-US', { maximumFractionDigits: 0 }).format(percent) + '%';
|
|
4132
|
+
button.append(meta);
|
|
4133
|
+
}
|
|
3818
4134
|
menu.append(button);
|
|
3819
4135
|
});
|
|
4136
|
+
const foot = document.createElement('div');
|
|
4137
|
+
foot.className = 'codex-slash-menu-foot';
|
|
4138
|
+
foot.dataset.slashMenuFoot = '';
|
|
4139
|
+
for (const [keys, label] of [[['↑', '↓'], 'slashKeyMove'], [['Enter'], 'slashKeyRun'], [['Esc'], 'slashKeyClose']]) {
|
|
4140
|
+
const hint = document.createElement('span');
|
|
4141
|
+
for (const key of keys) hint.append(Object.assign(document.createElement('kbd'), { textContent: key }));
|
|
4142
|
+
hint.append(tr(label));
|
|
4143
|
+
foot.append(hint);
|
|
4144
|
+
}
|
|
4145
|
+
menu.append(foot);
|
|
3820
4146
|
renderedSlashCommands = new Map(commands.map(command => [command.id, command]));
|
|
3821
4147
|
menu.dataset.activeIndex = '0';
|
|
3822
4148
|
menu.hidden = false;
|
|
@@ -3891,10 +4217,16 @@
|
|
|
3891
4217
|
});
|
|
3892
4218
|
const active = buttons[index];
|
|
3893
4219
|
if (!active || !menu.clientHeight) return;
|
|
4220
|
+
// Wrapping to the first item also reveals its group label.
|
|
4221
|
+
if (index === 0) {
|
|
4222
|
+
menu.scrollTop = 0;
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
3894
4225
|
const viewport = menu.getBoundingClientRect();
|
|
3895
4226
|
const item = active.getBoundingClientRect();
|
|
3896
4227
|
const top = viewport.top + menu.clientTop;
|
|
3897
|
-
|
|
4228
|
+
// The sticky key-hint footer covers the bottom of the scroll area.
|
|
4229
|
+
const bottom = top + menu.clientHeight - (menu.querySelector('[data-slash-menu-foot]')?.offsetHeight || 0);
|
|
3898
4230
|
// Scroll only this popup; scrollIntoView may move the chat or editor too.
|
|
3899
4231
|
if (item.height > menu.clientHeight || item.top < top) {
|
|
3900
4232
|
menu.scrollTop += item.top - top;
|
|
@@ -3904,6 +4236,7 @@
|
|
|
3904
4236
|
}
|
|
3905
4237
|
|
|
3906
4238
|
function selectSlashCommand(commandId) {
|
|
4239
|
+
if (sessionManager.isTransitioning()) return;
|
|
3907
4240
|
const command = renderedSlashCommands.get(commandId);
|
|
3908
4241
|
if (!command) {
|
|
3909
4242
|
closeSlashMenu();
|
|
@@ -3916,6 +4249,11 @@
|
|
|
3916
4249
|
applyAtFileSelection(command);
|
|
3917
4250
|
return;
|
|
3918
4251
|
}
|
|
4252
|
+
if (command.kind === 'compact') {
|
|
4253
|
+
closeSlashMenu();
|
|
4254
|
+
safeRunTask({ operation: 'compact', consumeCompactDraft: true });
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
3919
4257
|
clearSlashTriggerFromTaskInput();
|
|
3920
4258
|
closeSlashMenu();
|
|
3921
4259
|
if (command.kind === 'installer') {
|
|
@@ -4191,8 +4529,15 @@
|
|
|
4191
4529
|
Bridge: Modules.SharedSessionBridge, StorageDb: Modules.StorageDb,
|
|
4192
4530
|
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
|
|
4193
4531
|
getState: () => state, setState: next => { state = next; },
|
|
4194
|
-
|
|
4195
|
-
|
|
4532
|
+
// An in-flight Accept/Undo keeps its view until it settles; refresh applies afterwards.
|
|
4533
|
+
isBusy: () => Boolean(pendingDraftSave || readLiveRunViewForSaveStateGuard() || trackedChangeInFlight.size
|
|
4534
|
+
|| runReviewController?.hasActiveReview?.() || document.querySelector('dialog[open]')),
|
|
4535
|
+
hasLocalDraft: current => Boolean(current.task) && composerDraftEditedHere,
|
|
4536
|
+
normalize: normalizePanelState,
|
|
4537
|
+
render: () => {
|
|
4538
|
+
applyStateToPanel();
|
|
4539
|
+
composerDraftEditedHere = false;
|
|
4540
|
+
},
|
|
4196
4541
|
notice: error => appendStorageNoticeOnce('shared-history-refresh', tx(
|
|
4197
4542
|
`Could not refresh shared history: ${error.message}`,
|
|
4198
4543
|
`共享会话历史刷新失败:${error.message}`
|
|
@@ -4292,6 +4637,7 @@
|
|
|
4292
4637
|
storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
|
|
4293
4638
|
const reloaded = await loadStoredState();
|
|
4294
4639
|
state = normalizeLoadedPanelState(reloaded);
|
|
4640
|
+
composerDraftEditedHere = false;
|
|
4295
4641
|
await getRunReviewController().recoverInterruptedUndoStatuses();
|
|
4296
4642
|
applyStateToPanel();
|
|
4297
4643
|
} catch (_error) {
|
|
@@ -4354,6 +4700,7 @@
|
|
|
4354
4700
|
function leaveActiveProject(newId) {
|
|
4355
4701
|
const prevId = activeProjectId;
|
|
4356
4702
|
activeProjectId = newId; // null for non-project URLs (spec §5.7.1)
|
|
4703
|
+
if (prevId !== newId) composerDraftEditedHere = false;
|
|
4357
4704
|
getComposerDraftPersistence().syncScope();
|
|
4358
4705
|
if (!prevId || prevId === newId) {
|
|
4359
4706
|
return;
|
|
@@ -4380,7 +4727,15 @@
|
|
|
4380
4727
|
}
|
|
4381
4728
|
|
|
4382
4729
|
async function onSpaRouteChange() {
|
|
4730
|
+
// Snapshot the departing draft before hydration replaces state. The detached
|
|
4731
|
+
// writer retains its original account/project and never reads the new view's attachments.
|
|
4732
|
+
if (pendingDraftSave?.projectId === activeProjectId
|
|
4733
|
+
&& pendingDraftSave?.accountScopeId === cachedAccountScopeId) {
|
|
4734
|
+
void saveState({ draftScope: pendingDraftSave });
|
|
4735
|
+
}
|
|
4736
|
+
resetDraftSave();
|
|
4383
4737
|
const generation = ++spaRouteGeneration;
|
|
4738
|
+
activeTurnControl.cancelRecovery();
|
|
4384
4739
|
const url = new URL(window.location.href);
|
|
4385
4740
|
const pathname = url.pathname;
|
|
4386
4741
|
const previousProjectId = activeProjectId;
|
|
@@ -4604,7 +4959,7 @@
|
|
|
4604
4959
|
operations: [],
|
|
4605
4960
|
status: tr('modeAsk')
|
|
4606
4961
|
});
|
|
4607
|
-
appendCompletionReport({
|
|
4962
|
+
appendCompletionReport({ conclusionKey: !notes ? 'ask_inspected' : undefined,
|
|
4608
4963
|
conclusion: notes || tx('This run only inspected and explained; no files were written.', '这轮只做了检查和说明,没有写入文件。'),
|
|
4609
4964
|
status: tr('modeAsk'),
|
|
4610
4965
|
notes,
|
|
@@ -4646,7 +5001,7 @@
|
|
|
4646
5001
|
applyResults,
|
|
4647
5002
|
status: 'applied with delete plan'
|
|
4648
5003
|
});
|
|
4649
|
-
appendCompletionReport({
|
|
5004
|
+
appendCompletionReport({ conclusionKey: !notes ? 'delete_handled' : undefined,
|
|
4650
5005
|
conclusion: notes || tx('Changes were written, and delete items were handled after your confirmation.', '已写入修改,并按你的确认处理删除项。'),
|
|
4651
5006
|
status: 'applied with delete plan',
|
|
4652
5007
|
notes,
|
|
@@ -4669,7 +5024,7 @@
|
|
|
4669
5024
|
status: 'applied without deletes',
|
|
4670
5025
|
deletePlanRejected: true
|
|
4671
5026
|
});
|
|
4672
|
-
appendCompletionReport({
|
|
5027
|
+
appendCompletionReport({ conclusionKey: 'delete_rejected',
|
|
4673
5028
|
conclusion: tx('Non-delete changes were kept; delete items were not written.', '已保留非删除修改;删除项没有写入。'),
|
|
4674
5029
|
status: 'applied without deletes',
|
|
4675
5030
|
notes,
|
|
@@ -4699,7 +5054,7 @@
|
|
|
4699
5054
|
applyResults: [applied],
|
|
4700
5055
|
status: result.status || 'completed'
|
|
4701
5056
|
});
|
|
4702
|
-
appendCompletionReport({
|
|
5057
|
+
appendCompletionReport({ conclusionKey: !notes ? 'run_complete' : undefined,
|
|
4703
5058
|
conclusion: notes || tx('This run is complete.', '这轮任务已完成。'),
|
|
4704
5059
|
status: result.status || 'completed',
|
|
4705
5060
|
notes,
|
|
@@ -4781,10 +5136,19 @@
|
|
|
4781
5136
|
// Thread runProjectId so the page-side dispatcher gates the toggle. Same
|
|
4782
5137
|
// intent as the pre-flight check above: do not flip Track Changes /
|
|
4783
5138
|
// Editing in a different project than the run is bound to.
|
|
5139
|
+
const owner = currentRunView;
|
|
4784
5140
|
const result = await callPageBridge(method, {
|
|
4785
5141
|
waitMs: 1800,
|
|
4786
5142
|
runProjectId: currentRunView?.runProjectId || ''
|
|
4787
5143
|
});
|
|
5144
|
+
throwIfRunCancellationRequested();
|
|
5145
|
+
if (owner && (owner !== currentRunView || owner.runProjectId !== getCurrentProjectId()
|
|
5146
|
+
|| owner.runAccountScopeId !== cachedAccountScopeId)) throw createRunCancellationError();
|
|
5147
|
+
if (requireReviewing && (result?.ok || result?.code === 'reviewing_unavailable')) {
|
|
5148
|
+
projectSettingsCoordinator.observeReviewingAvailability(result.ok ? 'available' : 'unavailable',
|
|
5149
|
+
result.ok ? 'reviewing_confirmed' : 'upgrade_modal');
|
|
5150
|
+
syncProbeStatus();
|
|
5151
|
+
}
|
|
4788
5152
|
if (result?.ok) {
|
|
4789
5153
|
appendRunEvent({
|
|
4790
5154
|
title: requireReviewing
|
|
@@ -4816,6 +5180,7 @@
|
|
|
4816
5180
|
code: result?.code || (requireReviewing ? 'reviewing_not_enabled' : 'editing_not_confirmed'),
|
|
4817
5181
|
reason: result?.reason || (requireReviewing ? 'Reviewing/Track Changes was not enabled' : 'Editing mode was not confirmed'),
|
|
4818
5182
|
reviewing: result?.reviewing || null,
|
|
5183
|
+
evidence: result?.evidence, failure: result?.failure,
|
|
4819
5184
|
requireReviewing
|
|
4820
5185
|
};
|
|
4821
5186
|
}
|
|
@@ -4830,8 +5195,9 @@
|
|
|
4830
5195
|
operation,
|
|
4831
5196
|
result: {
|
|
4832
5197
|
ok: false,
|
|
4833
|
-
code: blockedByEditing ? 'editing_not_confirmed' : 'reviewing_not_enabled',
|
|
4834
|
-
|
|
5198
|
+
code: reviewing?.code || (blockedByEditing ? 'editing_not_confirmed' : 'reviewing_not_enabled'),
|
|
5199
|
+
evidence: reviewing?.evidence, failure: reviewing?.failure,
|
|
5200
|
+
reason: reviewing?.code === 'reviewing_unavailable' ? tr('failureReason_reviewing_unavailable_user') : blockedByEditing
|
|
4835
5201
|
? tx(
|
|
4836
5202
|
'Track is off, but Overleaf Editing mode was not verified before writing. Codex did not write this file.',
|
|
4837
5203
|
'已关闭“留痕”,但写入前没有确认 Overleaf 正在用 Editing 模式。Codex 没有写入这个文件。'
|
|
@@ -4885,13 +5251,13 @@
|
|
|
4885
5251
|
|
|
4886
5252
|
async function refreshProbe(options = {}) {
|
|
4887
5253
|
const projectId = getCurrentProjectId();
|
|
5254
|
+
const accountScopeId = cachedAccountScopeId;
|
|
4888
5255
|
const userInitiated = options.userInitiated === true;
|
|
4889
5256
|
if (userInitiated) {
|
|
4890
5257
|
setRefreshProbeLoading(true);
|
|
4891
5258
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4892
5259
|
if (status) {
|
|
4893
|
-
status
|
|
4894
|
-
status.dataset.ok = 'false';
|
|
5260
|
+
renderProbeStatusMessage(status, tr('refreshProbeLoading'));
|
|
4895
5261
|
status.dataset.refreshing = 'true';
|
|
4896
5262
|
}
|
|
4897
5263
|
}
|
|
@@ -4901,13 +5267,14 @@
|
|
|
4901
5267
|
manualOverride: state?.requireReviewing === false
|
|
4902
5268
|
});
|
|
4903
5269
|
if (projectId !== getCurrentProjectId()) return null;
|
|
5270
|
+
if (accountScopeId !== cachedAccountScopeId) return null;
|
|
5271
|
+
if (probe.capabilities?.reviewing?.confirmed === true) {
|
|
5272
|
+
projectSettingsCoordinator.observeReviewingAvailability('available', 'reviewing_confirmed');
|
|
5273
|
+
}
|
|
4904
5274
|
probeStatusSnapshot = { projectId, probe, userInitiated };
|
|
4905
5275
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4906
5276
|
if (status) {
|
|
4907
|
-
status
|
|
4908
|
-
? tr('refreshProbeDone', { status: formatProbeStatusBar(probe) })
|
|
4909
|
-
: formatProbeStatusBar(probe);
|
|
4910
|
-
status.dataset.ok = isProbeReadyForCurrentMode(probe) ? 'true' : 'false';
|
|
5277
|
+
renderProbeStatusLine(status, probe, { refreshed: userInitiated });
|
|
4911
5278
|
status.dataset.refreshing = 'false';
|
|
4912
5279
|
}
|
|
4913
5280
|
if (!options.quiet && !userInitiated) {
|
|
@@ -4924,8 +5291,7 @@
|
|
|
4924
5291
|
probeStatusSnapshot = { projectId, failed: true };
|
|
4925
5292
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4926
5293
|
if (status) {
|
|
4927
|
-
status
|
|
4928
|
-
status.dataset.ok = 'false';
|
|
5294
|
+
renderProbeStatusMessage(status, tr('refreshProbeFailed'), 'warn');
|
|
4929
5295
|
status.dataset.refreshing = 'false';
|
|
4930
5296
|
}
|
|
4931
5297
|
console.warn('[codex-overleaf] refresh probe failed', error);
|
|
@@ -4945,23 +5311,81 @@
|
|
|
4945
5311
|
button.dataset.loading = loading ? 'true' : 'false';
|
|
4946
5312
|
button.disabled = Boolean(loading);
|
|
4947
5313
|
button.setAttribute('aria-busy', loading ? 'true' : 'false');
|
|
5314
|
+
setElementTitleAndAria('[data-refresh]', tr(loading ? 'refreshProbeLoading' : 'refreshProbe'),
|
|
5315
|
+
tr(loading ? 'refreshProbeLoading' : 'refreshProbeTitle'));
|
|
4948
5316
|
}
|
|
4949
5317
|
|
|
4950
5318
|
function syncProbeStatus(mode = state?.mode) {
|
|
4951
5319
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4952
5320
|
if (!status) return;
|
|
4953
5321
|
if (status.dataset.refreshing === 'true') {
|
|
4954
|
-
status
|
|
5322
|
+
renderProbeStatusMessage(status, tr('refreshProbeLoading'));
|
|
4955
5323
|
return;
|
|
4956
5324
|
}
|
|
4957
5325
|
if (probeStatusSnapshot?.projectId !== getCurrentProjectId()) return;
|
|
4958
5326
|
const { failed, probe, userInitiated } = probeStatusSnapshot;
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
status.dataset.ok = !failed && isProbeReadyForCurrentMode(probe, mode) ? 'true' : 'false';
|
|
5327
|
+
if (failed) renderProbeStatusMessage(status, tr('refreshProbeFailed'), 'warn');
|
|
5328
|
+
else renderProbeStatusLine(status, probe, { refreshed: userInitiated });
|
|
4962
5329
|
if (!failed) updateExistingProbeNotice(probe, mode);
|
|
4963
5330
|
}
|
|
4964
5331
|
|
|
5332
|
+
// Keep the read scope visible; a known Track limitation adds its own warning segment.
|
|
5333
|
+
function getProbeScope(probe) {
|
|
5334
|
+
const selection = selectionContextView.capture();
|
|
5335
|
+
const focusFiles = getActiveFocusFiles();
|
|
5336
|
+
// Match submission priority: an edit selection, then explicit focus files,
|
|
5337
|
+
// then reference context, ahead of the editor's implicit current-file hint.
|
|
5338
|
+
if (selection?.mode === 'edit') return { icon: 'file', key: 'probeScopeSelection' };
|
|
5339
|
+
if (focusFiles.some(file => /\.tex$/i.test(file))) return { icon: 'file', key: 'probeScopeFiles' };
|
|
5340
|
+
if (focusFiles.length || selection) return { icon: 'attach', key: 'probeScopeContext' };
|
|
5341
|
+
if (getProbeRunReadiness(probe).editorOk) return { icon: 'file', key: 'probeScopeCurrentFile' };
|
|
5342
|
+
return { icon: 'folder', key: 'probeScopeProject' };
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
function isReviewingKnownUnavailable() {
|
|
5346
|
+
return Modules.Reviewing.knownUnavailable(projectSettingsCoordinator.getReviewingAvailability());
|
|
5347
|
+
}
|
|
5348
|
+
|
|
5349
|
+
function syncReviewingTooltip() {
|
|
5350
|
+
const toggle = panel?.querySelector('[data-require-reviewing]');
|
|
5351
|
+
const title = tr(isReviewingKnownUnavailable() ? 'reviewingUnavailableTooltip' : 'requireReviewingTitle');
|
|
5352
|
+
if (toggle) toggle.title = title;
|
|
5353
|
+
const label = toggle?.closest('.codex-review-toggle');
|
|
5354
|
+
if (label) label.title = title;
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
function renderProbeStatusLine(status, probe, options = {}) {
|
|
5358
|
+
const scope = getProbeScope(probe);
|
|
5359
|
+
const parts = [];
|
|
5360
|
+
if (options.refreshed) parts.push(tr('refreshProbeDone', { status: '' }).trim());
|
|
5361
|
+
const label = document.createElement('span');
|
|
5362
|
+
label.className = 'codex-probe-scope';
|
|
5363
|
+
label.innerHTML = Modules.PanelIcons?.svg?.(scope.icon) || ''; // Static, repository-owned icon path.
|
|
5364
|
+
label.append(tr(scope.key));
|
|
5365
|
+
parts.push(label);
|
|
5366
|
+
if (isExperimentalOtEnabled()) {
|
|
5367
|
+
const separator = document.createElement('span');
|
|
5368
|
+
separator.className = 'codex-probe-sep';
|
|
5369
|
+
separator.textContent = '·';
|
|
5370
|
+
parts.push(separator, 'OT ' + formatOtStatusLabel(getCurrentOtStatus()));
|
|
5371
|
+
}
|
|
5372
|
+
if (state.requireReviewing && isReviewingKnownUnavailable()) {
|
|
5373
|
+
const warning = document.createElement('span');
|
|
5374
|
+
warning.dataset.tone = 'warn';
|
|
5375
|
+
warning.textContent = ' · ' + tr('reviewingUnavailableStatus');
|
|
5376
|
+
parts.push(warning);
|
|
5377
|
+
}
|
|
5378
|
+
syncReviewingTooltip();
|
|
5379
|
+
status.replaceChildren(...parts);
|
|
5380
|
+
delete status.dataset.tone;
|
|
5381
|
+
}
|
|
5382
|
+
|
|
5383
|
+
function renderProbeStatusMessage(status, text, tone = '') {
|
|
5384
|
+
status.replaceChildren(text);
|
|
5385
|
+
if (tone) status.dataset.tone = tone;
|
|
5386
|
+
else delete status.dataset.tone;
|
|
5387
|
+
}
|
|
5388
|
+
|
|
4965
5389
|
function formatProbeStatusBar(probe, mode = state?.mode) {
|
|
4966
5390
|
const readiness = getProbeRunReadiness(probe);
|
|
4967
5391
|
const reviewingOk = readiness.reviewingOk;
|
|
@@ -4995,17 +5419,7 @@
|
|
|
4995
5419
|
if (!status || status.dataset.refreshing === 'true') {
|
|
4996
5420
|
return;
|
|
4997
5421
|
}
|
|
4998
|
-
|
|
4999
|
-
if (!text) {
|
|
5000
|
-
return;
|
|
5001
|
-
}
|
|
5002
|
-
const base = text.split(' · OT ')[0];
|
|
5003
|
-
status.textContent = appendOtStatusToProbeStatus(base);
|
|
5004
|
-
}
|
|
5005
|
-
|
|
5006
|
-
function isProbeReadyForCurrentMode(probe, mode = state?.mode) {
|
|
5007
|
-
const readiness = getProbeRunReadiness(probe);
|
|
5008
|
-
return mode === 'ask' || readiness.reviewingOk;
|
|
5422
|
+
syncProbeStatus();
|
|
5009
5423
|
}
|
|
5010
5424
|
|
|
5011
5425
|
function getProbeRunReadiness(probe) {
|
|
@@ -5110,8 +5524,8 @@
|
|
|
5110
5524
|
ready: true,
|
|
5111
5525
|
message: manualOverride
|
|
5112
5526
|
? tx(
|
|
5113
|
-
`Ready:
|
|
5114
|
-
|
|
5527
|
+
`Ready: Codex writes without tracked changes and ${readiness.contextLabel}.`,
|
|
5528
|
+
`可以运行:Codex 写入时不使用留痕,并且${readiness.contextLabel}。`
|
|
5115
5529
|
)
|
|
5116
5530
|
: tx(
|
|
5117
5531
|
`Ready: Codex verified Overleaf Reviewing/Track Changes is on and ${readiness.contextLabel}.`,
|
|
@@ -5120,21 +5534,13 @@
|
|
|
5120
5534
|
};
|
|
5121
5535
|
}
|
|
5122
5536
|
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
ready: false,
|
|
5126
|
-
message: tx(
|
|
5127
|
-
`Not safe to write yet: Codex ${readiness.contextLabel}, but Overleaf Reviewing/Track Changes was not verified. Turn on Reviewing first, or disable Track below.`,
|
|
5128
|
-
`还不能安全写入:Codex ${readiness.contextLabel},但没有确认 Overleaf 已开启 Reviewing/Track Changes。请先打开 Reviewing,或关闭下方的安全检查。`
|
|
5129
|
-
)
|
|
5130
|
-
};
|
|
5131
|
-
}
|
|
5132
|
-
|
|
5537
|
+
// ensureReviewingBeforeWrite switches Overleaf to Reviewing before the first
|
|
5538
|
+
// write and blocks the write only if that cannot be confirmed.
|
|
5133
5539
|
return {
|
|
5134
|
-
ready:
|
|
5540
|
+
ready: true,
|
|
5135
5541
|
message: tx(
|
|
5136
|
-
|
|
5137
|
-
|
|
5542
|
+
`Ready: Codex ${readiness.contextLabel} and will switch Overleaf to Reviewing/Track Changes before writing.`,
|
|
5543
|
+
`可以运行:Codex ${readiness.contextLabel},写入前会把 Overleaf 切到 Reviewing/Track Changes。`
|
|
5138
5544
|
)
|
|
5139
5545
|
};
|
|
5140
5546
|
}
|
|
@@ -5521,6 +5927,7 @@
|
|
|
5521
5927
|
codexLocalSkillEnabled: prefs.codexLocalSkillEnabled || {},
|
|
5522
5928
|
experimentalOtByProject: prefs.experimentalOtByProject || {},
|
|
5523
5929
|
customInstructionsByProject: prefs.customInstructionsByProject || {},
|
|
5930
|
+
reviewingAvailabilityByProject: prefs.reviewingAvailabilityByProject || {},
|
|
5524
5931
|
governanceRulesByProject: normalizeGovernanceRulesByProject(prefs.governanceRulesByProject),
|
|
5525
5932
|
panelWidth: prefs.panelWidth || PANEL_DEFAULT_WIDTH,
|
|
5526
5933
|
sessions: sessions.map(session => ({
|
|
@@ -5531,6 +5938,7 @@
|
|
|
5531
5938
|
focusFiles: session.focusFiles || [], selectionContext: session.selectionContext?.projectId === projectId ? session.selectionContext : null,
|
|
5532
5939
|
projectReferenceFiles: session.projectReferenceFiles || [],
|
|
5533
5940
|
codexThreadId: session.codexThreadId || '',
|
|
5941
|
+
contextUsage: session.contextUsage || null,
|
|
5534
5942
|
forkedFromSessionId: session.forkedFromSessionId || '',
|
|
5535
5943
|
forkedFromRunId: session.forkedFromRunId || '',
|
|
5536
5944
|
createdAt: session.createdAt,
|
|
@@ -5576,6 +5984,7 @@
|
|
|
5576
5984
|
codexLocalSkillEnabled: fallback.codexLocalSkillEnabled || {},
|
|
5577
5985
|
experimentalOtByProject: fallback.experimentalOtByProject || {},
|
|
5578
5986
|
customInstructionsByProject: fallback.customInstructionsByProject || {},
|
|
5987
|
+
reviewingAvailabilityByProject: fallback.reviewingAvailabilityByProject || {},
|
|
5579
5988
|
governanceRulesByProject: normalizeGovernanceRulesByProject(fallback.governanceRulesByProject),
|
|
5580
5989
|
panelWidth: fallback.panelWidth || PANEL_DEFAULT_WIDTH,
|
|
5581
5990
|
sessions: [],
|
|
@@ -5610,9 +6019,8 @@
|
|
|
5610
6019
|
// Settlement owns a run after navigation away from its project. Ordinary
|
|
5611
6020
|
// view saves must not write that run into the current route's project.
|
|
5612
6021
|
// An explicit override is reserved for callers that own a detached scope.
|
|
5613
|
-
const projectIdOverride = options && typeof options.projectIdOverride === 'string'
|
|
5614
|
-
? options.projectIdOverride
|
|
5615
|
-
: '';
|
|
6022
|
+
const projectIdOverride = options?.draftScope?.projectId || (options && typeof options.projectIdOverride === 'string'
|
|
6023
|
+
? options.projectIdOverride : '');
|
|
5616
6024
|
const urlProjectId = getCurrentProjectId();
|
|
5617
6025
|
const projectId = projectIdOverride || urlProjectId;
|
|
5618
6026
|
if (!projectId) {
|
|
@@ -5630,6 +6038,8 @@
|
|
|
5630
6038
|
));
|
|
5631
6039
|
if (options?.requireDurable || options?.reviewRunIds?.length) throw new Error('Persistence project changed.'); return;
|
|
5632
6040
|
}
|
|
6041
|
+
const savedDraft = !projectIdOverride && pendingDraftSave?.scope === getComposerDraftScope()
|
|
6042
|
+
? pendingDraftSave : null;
|
|
5633
6043
|
const compactState = prepareStateForStorage(state, { onAggressive: notifyAggressiveCompactionOnce });
|
|
5634
6044
|
compactState.autoRecompile = state.autoRecompile;
|
|
5635
6045
|
compactState.loadCodexLocalSkills = state.loadCodexLocalSkills !== false;
|
|
@@ -5638,10 +6048,17 @@
|
|
|
5638
6048
|
compactState.codexLocalSkillEnabled = getCodexLocalSkillEnabled();
|
|
5639
6049
|
compactState.experimentalOtByProject = state.experimentalOtByProject;
|
|
5640
6050
|
compactState.customInstructionsByProject = state.customInstructionsByProject;
|
|
6051
|
+
compactState.reviewingAvailabilityByProject = state.reviewingAvailabilityByProject;
|
|
5641
6052
|
compactState.governanceRulesByProject = state.governanceRulesByProject;
|
|
5642
6053
|
const stateSnapshot = typeof structuredClone === 'function'
|
|
5643
6054
|
? structuredClone(state)
|
|
5644
6055
|
: JSON.parse(JSON.stringify(state));
|
|
6056
|
+
// Keep consumption provenance on the live run owner across ordinary
|
|
6057
|
+
// session normalization; it is not a composer edit or a queued input.
|
|
6058
|
+
const consumedSession = liveRunView?.draftConsumption && liveRunView.runProjectId === projectId
|
|
6059
|
+
&& liveRunView.runAccountScopeId === cachedAccountScopeId
|
|
6060
|
+
? stateSnapshot.sessions?.find(session => session.id === liveRunView.sessionId) : null;
|
|
6061
|
+
if (consumedSession?.task === '') consumedSession.draftConsumption = { ...liveRunView.draftConsumption };
|
|
5645
6062
|
const compactStateSnapshot = typeof structuredClone === 'function'
|
|
5646
6063
|
? structuredClone(compactState)
|
|
5647
6064
|
: JSON.parse(JSON.stringify(compactState));
|
|
@@ -5662,7 +6079,9 @@
|
|
|
5662
6079
|
normalizeCustomInstructionsByProject,
|
|
5663
6080
|
persistenceContext
|
|
5664
6081
|
});
|
|
5665
|
-
|
|
6082
|
+
if (!options?.draftScope) {
|
|
6083
|
+
await getComposerDraftPersistence().save({ accountScopeId: persistenceContext.scope.accountScopeId, projectId });
|
|
6084
|
+
}
|
|
5666
6085
|
};
|
|
5667
6086
|
|
|
5668
6087
|
const persistenceCoordinator = getScopedPersistenceCoordinator();
|
|
@@ -5671,17 +6090,16 @@
|
|
|
5671
6090
|
code: 'scoped_persistence_unavailable'
|
|
5672
6091
|
});
|
|
5673
6092
|
} else {
|
|
5674
|
-
const
|
|
5675
|
-
projectId,
|
|
5676
|
-
{
|
|
6093
|
+
const commitOptions = {
|
|
5677
6094
|
detached: Boolean(projectIdOverride),
|
|
5678
6095
|
rebase: true,
|
|
5679
6096
|
queueMutation: options?.queueMutation,
|
|
5680
6097
|
queueMutations: options?.queueMutations,
|
|
5681
6098
|
sessionTombstones: options?.deletedSessionIds
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
6099
|
+
};
|
|
6100
|
+
const committed = options?.draftScope
|
|
6101
|
+
? await persistenceCoordinator.commitDetached(projectId, options.draftScope.accountScopeId, commitOptions, persistScopedState)
|
|
6102
|
+
: await persistenceCoordinator.commit(projectId, commitOptions, persistScopedState);
|
|
5685
6103
|
if (!committed.ok && (
|
|
5686
6104
|
options?.requireDurable || options?.reviewRunIds?.length || options?.recoveryRuns?.length || options?.queueMutation
|
|
5687
6105
|
|| options?.queueMutations?.length
|
|
@@ -5692,6 +6110,16 @@
|
|
|
5692
6110
|
});
|
|
5693
6111
|
}
|
|
5694
6112
|
if (committed.ok) {
|
|
6113
|
+
if (savedDraft && pendingDraftSave === savedDraft) {
|
|
6114
|
+
const capturedTask = stateSnapshot.sessions.find(item => item.id === stateSnapshot.activeSessionId)?.task;
|
|
6115
|
+
const storedTask = StorageDb.buildSessionRecord({ task: capturedTask }).task;
|
|
6116
|
+
if (!(written || []).some(session => session.id === stateSnapshot.activeSessionId && session.task === storedTask)) {
|
|
6117
|
+
throw Object.assign(new Error(tx('The latest draft could not be saved. It is still available in this page.',
|
|
6118
|
+
'最新草稿未能保存,内容仍保留在当前页面。')), { code: 'draft_state_not_persisted' });
|
|
6119
|
+
}
|
|
6120
|
+
// A newer input revision keeps its own timer, even if this save finishes later.
|
|
6121
|
+
resetDraftSave();
|
|
6122
|
+
}
|
|
5695
6123
|
const retentionUpdates = StorageDb.projectRetentionUpdates?.({ liveState: state, capturedState: stateSnapshot, written,
|
|
5696
6124
|
capturedScope: retentionScope, currentScope: { accountScopeId: typeof cachedAccountScopeId === 'string' ? cachedAccountScopeId : '',
|
|
5697
6125
|
projectId: getCurrentProjectId(), generation: typeof spaRouteGeneration === 'number' ? spaRouteGeneration : null } }) || [];
|
|
@@ -5707,6 +6135,8 @@
|
|
|
5707
6135
|
}
|
|
5708
6136
|
} catch (error) {
|
|
5709
6137
|
if (options?.requireDurable) throw error;
|
|
6138
|
+
// A departing draft must never fall back using the destination page's state/scope.
|
|
6139
|
+
if (options?.draftScope) return;
|
|
5710
6140
|
if (error?.code === 'recovery_scope_changed') return;
|
|
5711
6141
|
if (
|
|
5712
6142
|
error?.code === 'account_scope_unavailable' &&
|
|
@@ -5824,6 +6254,65 @@
|
|
|
5824
6254
|
}, delayMs);
|
|
5825
6255
|
}
|
|
5826
6256
|
|
|
6257
|
+
function getComposerDraftScope() {
|
|
6258
|
+
return JSON.stringify([cachedAccountScopeId, getCurrentProjectId(), spaRouteGeneration, state.activeSessionId]);
|
|
6259
|
+
}
|
|
6260
|
+
|
|
6261
|
+
function resetDraftSave() {
|
|
6262
|
+
if (draftSaveTimer) clearTimeout(draftSaveTimer);
|
|
6263
|
+
draftSaveTimer = null;
|
|
6264
|
+
pendingDraftSave = null;
|
|
6265
|
+
}
|
|
6266
|
+
|
|
6267
|
+
function captureComposerDraft() {
|
|
6268
|
+
const input = panel?.querySelector('[data-task]');
|
|
6269
|
+
if (!input || !state.activeSessionId) return;
|
|
6270
|
+
const next = updateActiveSessionDraft(state, input.value);
|
|
6271
|
+
if (next === state) return;
|
|
6272
|
+
if (next.task !== state.task) {
|
|
6273
|
+
composerDraftEditedHere = true;
|
|
6274
|
+
if (currentRunView?.sessionId === state.activeSessionId) delete currentRunView.draftConsumption;
|
|
6275
|
+
}
|
|
6276
|
+
state = next;
|
|
6277
|
+
const now = Date.now(), scope = getComposerDraftScope();
|
|
6278
|
+
pendingDraftSave = {
|
|
6279
|
+
scope,
|
|
6280
|
+
projectId: getCurrentProjectId(),
|
|
6281
|
+
accountScopeId: cachedAccountScopeId,
|
|
6282
|
+
firstAt: pendingDraftSave?.scope === scope ? pendingDraftSave.firstAt : now
|
|
6283
|
+
};
|
|
6284
|
+
if (draftSaveTimer) clearTimeout(draftSaveTimer);
|
|
6285
|
+
const delay = Math.max(0, Math.min(DRAFT_SAVE_DELAY_MS, pendingDraftSave.firstAt + DRAFT_SAVE_MAX_WAIT_MS - now));
|
|
6286
|
+
draftSaveTimer = setTimeout(() => {
|
|
6287
|
+
draftSaveTimer = null;
|
|
6288
|
+
if (pendingDraftSave?.scope !== getComposerDraftScope()) { resetDraftSave(); return; }
|
|
6289
|
+
pendingDraftSave.firstAt = Date.now();
|
|
6290
|
+
// Join the existing writer immediately; never reset its timer on every key.
|
|
6291
|
+
saveStateSoon(0);
|
|
6292
|
+
}, delay);
|
|
6293
|
+
}
|
|
6294
|
+
|
|
6295
|
+
async function flushComposerDraft({ requireDurable = false } = {}) {
|
|
6296
|
+
if (!pendingDraftSave) return;
|
|
6297
|
+
const scope = getComposerDraftScope();
|
|
6298
|
+
if (pendingDraftSave.scope !== scope) { resetDraftSave(); return; }
|
|
6299
|
+
if (draftSaveTimer) clearTimeout(draftSaveTimer);
|
|
6300
|
+
draftSaveTimer = null;
|
|
6301
|
+
pendingDraftSave.firstAt = Date.now();
|
|
6302
|
+
await flushQueuedSaveState(requireDurable ? { requireDurable: true } : null);
|
|
6303
|
+
if (requireDurable && scope !== getComposerDraftScope()) {
|
|
6304
|
+
throw new Error(tx('The active session changed while saving the draft. Try again.',
|
|
6305
|
+
'保存草稿时活动会话已改变,请重试。'));
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
|
|
6309
|
+
function flushComposerDraftOnLeave() {
|
|
6310
|
+
// Lifecycle events cannot await completion; periodic saves remain the primary protection.
|
|
6311
|
+
flushComposerDraft().catch(error => appendStorageNoticeOnce('save-failed', tx(
|
|
6312
|
+
`Failed to save session state: ${localizeVisibleReason(error.message)}`,
|
|
6313
|
+
`保存会话状态失败:${localizeVisibleReason(error.message)}`)));
|
|
6314
|
+
}
|
|
6315
|
+
|
|
5827
6316
|
function runQueuedSaveState() {
|
|
5828
6317
|
if (saveStateInFlightPromise) {
|
|
5829
6318
|
saveStateRunAfterFlight = true;
|
|
@@ -5924,13 +6413,11 @@
|
|
|
5924
6413
|
async function persistPanelInputs(event) {
|
|
5925
6414
|
const globalWrite = getGlobalPreferences().handleInput(event);
|
|
5926
6415
|
if (globalWrite) return globalWrite;
|
|
5927
|
-
//
|
|
5928
|
-
//
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
readPanelInputs();
|
|
5933
|
-
saveStateSoon();
|
|
6416
|
+
// Draft edits retain run/event identity. Composition updates also capture the
|
|
6417
|
+
// current text; compositionend captures its committed value if no final input fires.
|
|
6418
|
+
if (event?.target?.matches?.('[data-task]')) {
|
|
6419
|
+
captureComposerDraft();
|
|
6420
|
+
if (event.type === 'change') await flushComposerDraft();
|
|
5934
6421
|
return;
|
|
5935
6422
|
}
|
|
5936
6423
|
if (event?.target?.matches?.('[data-speed]')) {
|
|
@@ -5942,8 +6429,18 @@
|
|
|
5942
6429
|
}
|
|
5943
6430
|
renderSpeedOptions(getRenderedModelEntries());
|
|
5944
6431
|
readPanelInputs();
|
|
6432
|
+
// Only a user re-enabling Track requests another capability check. The
|
|
6433
|
+
// preceding input event and programmatic session renders must not reset it.
|
|
6434
|
+
if (event?.type === 'change' && event.isTrusted === true
|
|
6435
|
+
&& event.target?.matches?.('[data-require-reviewing]') && event.target.checked
|
|
6436
|
+
&& isReviewingKnownUnavailable()) {
|
|
6437
|
+
projectSettingsCoordinator.observeReviewingAvailability('unknown', 'user_retry');
|
|
6438
|
+
syncReviewingTooltip();
|
|
6439
|
+
syncProbeStatus();
|
|
6440
|
+
}
|
|
5945
6441
|
renderModelConfigChoices();
|
|
5946
6442
|
updateModelDisplay();
|
|
6443
|
+
renderContextUsage();
|
|
5947
6444
|
// Save feedback is per-card now (settingsPanel flashSaved); the global
|
|
5948
6445
|
// header chip and its Saving/Saved lifecycle are gone.
|
|
5949
6446
|
await saveState();
|
|
@@ -6073,6 +6570,7 @@
|
|
|
6073
6570
|
if (findSessionById(submission.sessionId)?.task !== submission.task) return false;
|
|
6074
6571
|
if (state.activeSessionId !== submission.sessionId) {
|
|
6075
6572
|
updateSessionById(submission.sessionId, { task: '' });
|
|
6573
|
+
recordSubmittedDraftConsumption(submission.sessionId, submission.task);
|
|
6076
6574
|
saveStateSoon();
|
|
6077
6575
|
return false;
|
|
6078
6576
|
}
|
|
@@ -6086,6 +6584,7 @@
|
|
|
6086
6584
|
if (taskInput) {
|
|
6087
6585
|
taskInput.value = '';
|
|
6088
6586
|
}
|
|
6587
|
+
composerDraftEditedHere = false;
|
|
6089
6588
|
autosizeTaskTextarea();
|
|
6090
6589
|
if (!keepAttachments) {
|
|
6091
6590
|
composerAttachmentController.clear();
|
|
@@ -6094,10 +6593,20 @@
|
|
|
6094
6593
|
renderComposerSkillInvocation();
|
|
6095
6594
|
selectionContextView.clear();
|
|
6096
6595
|
state = updateActiveSession(state, { task: '' });
|
|
6596
|
+
if (submission) recordSubmittedDraftConsumption(submission.sessionId, submission.task);
|
|
6597
|
+
else if (currentRunView?.sessionId === state.activeSessionId) delete currentRunView.draftConsumption;
|
|
6097
6598
|
saveStateSoon();
|
|
6098
6599
|
return true;
|
|
6099
6600
|
}
|
|
6100
6601
|
|
|
6602
|
+
function recordSubmittedDraftConsumption(sessionId, task) {
|
|
6603
|
+
if (task && currentRunView?.sessionId === sessionId
|
|
6604
|
+
&& currentRunView.runProjectId === getCurrentProjectId()
|
|
6605
|
+
&& currentRunView.runAccountScopeId === cachedAccountScopeId) {
|
|
6606
|
+
currentRunView.draftConsumption = { runId: currentRunView.recordId, task };
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
|
|
6101
6610
|
function restoreRejectedLocalSkillSubmission(submission, checkpoint) {
|
|
6102
6611
|
const input = panel?.querySelector('[data-task]');
|
|
6103
6612
|
if (!checkpoint || !input || submission.accountScopeId !== cachedAccountScopeId
|
|
@@ -6115,6 +6624,7 @@
|
|
|
6115
6624
|
state = updateActiveSession(state, { task: submission.task, selectionContext: submission.selectionContext });
|
|
6116
6625
|
renderComposerSkillInvocation();
|
|
6117
6626
|
selectionContextView.render();
|
|
6627
|
+
syncProbeStatus();
|
|
6118
6628
|
autosizeTaskTextarea();
|
|
6119
6629
|
syncComposerSendAvailability();
|
|
6120
6630
|
saveStateSoon();
|
|
@@ -6122,6 +6632,8 @@
|
|
|
6122
6632
|
}
|
|
6123
6633
|
|
|
6124
6634
|
function applyStateToPanel(options = {}) {
|
|
6635
|
+
if (pendingDraftSave && pendingDraftSave.scope !== getComposerDraftScope()) resetDraftSave();
|
|
6636
|
+
contextUsageView?.close();
|
|
6125
6637
|
selectionContextView.render();
|
|
6126
6638
|
writingStyleSettings.sync();
|
|
6127
6639
|
applyPanelTheme(getThemePreference());
|
|
@@ -6144,6 +6656,7 @@
|
|
|
6144
6656
|
autosizeTaskTextarea();
|
|
6145
6657
|
syncComposerSendAvailability();
|
|
6146
6658
|
panel.querySelector('[data-require-reviewing]').checked = state.requireReviewing;
|
|
6659
|
+
syncReviewingTooltip();
|
|
6147
6660
|
const recompileCheckbox = panel?.querySelector('[data-auto-recompile]');
|
|
6148
6661
|
if (recompileCheckbox) {
|
|
6149
6662
|
recompileCheckbox.checked = state.autoRecompile !== false;
|
|
@@ -6198,6 +6711,47 @@
|
|
|
6198
6711
|
if (record) configureWritebackButton(root, record, { running });
|
|
6199
6712
|
});
|
|
6200
6713
|
renderPendingInputs();
|
|
6714
|
+
renderContextUsage();
|
|
6715
|
+
}
|
|
6716
|
+
|
|
6717
|
+
function renderContextUsage() {
|
|
6718
|
+
if (!contextUsageView || !state) return;
|
|
6719
|
+
contextUsageView.setState(getContextUsageDisplay());
|
|
6720
|
+
}
|
|
6721
|
+
|
|
6722
|
+
function getContextUsageDisplay() {
|
|
6723
|
+
const session = getActiveSession(state);
|
|
6724
|
+
const run = currentRunView?.sessionId === session?.id
|
|
6725
|
+
? findRunRecord(currentRunView.recordId, currentRunView.sessionId) : null;
|
|
6726
|
+
const selection = providerSettingsCoordinator.getRunSelection(state.providerId);
|
|
6727
|
+
const runningSelection = run?.status === 'running' && run.model === state.model
|
|
6728
|
+
&& (run.providerId || 'builtin') === (state.providerId || 'builtin');
|
|
6729
|
+
const scope = {
|
|
6730
|
+
threadId: session?.codexThreadId || '',
|
|
6731
|
+
providerId: state.providerId || 'builtin',
|
|
6732
|
+
providerRevision: runningSelection ? Number(run.providerRevision || 0)
|
|
6733
|
+
: selection?.providerRevision ?? ((state.providerId || 'builtin') === 'builtin' ? 0 : null),
|
|
6734
|
+
model: state.model || ''
|
|
6735
|
+
};
|
|
6736
|
+
// The composer can select the next turn's model while this turn runs.
|
|
6737
|
+
// Only show statistics produced by the selected model and provider.
|
|
6738
|
+
const matchesRun = run && run.model === scope.model
|
|
6739
|
+
&& (run.providerId || 'builtin') === scope.providerId
|
|
6740
|
+
&& Number(run.providerRevision || 0) === scope.providerRevision;
|
|
6741
|
+
// Starting another turn must not revive statistics from before an automatic
|
|
6742
|
+
// compaction. Retain the watermark across this thread's saved normal runs.
|
|
6743
|
+
const compactedAt = (session?.runs || []).reduce((latest, candidate) => candidate && !candidate.forkSnapshot
|
|
6744
|
+
&& candidate.operation !== 'compact' && scope.threadId && candidate.codexThreadId === scope.threadId
|
|
6745
|
+
? Math.max(latest, Modules.RunActivityModel.getCompactedAt(candidate)) : latest, 0);
|
|
6746
|
+
const { usage, pending, compacted } = Modules.ContextUsage.status(session, { ...scope, compactedAt });
|
|
6747
|
+
const compacting = Boolean(matchesRun && Modules.RunActivityModel.project(run).compacting);
|
|
6748
|
+
return { usage, pending, compacted, compacting, compactingKind: compacting ? (run.operation === 'compact' ? 'manual' : 'auto') : '' };
|
|
6749
|
+
}
|
|
6750
|
+
|
|
6751
|
+
function getCompactAvailability() {
|
|
6752
|
+
if (sessionManager.isTransitioning() || updateIdleClient?.isApplying() || currentRunView || runSubmissionPending) return { ok: false, reason: 'compactBusy' };
|
|
6753
|
+
if (!getActiveSession(state)?.codexThreadId) return { ok: false, reason: 'compactNoThread' };
|
|
6754
|
+
return { ok: true };
|
|
6201
6755
|
}
|
|
6202
6756
|
|
|
6203
6757
|
function prepareRetryReplacement(run, task) {
|
|
@@ -6231,6 +6785,7 @@
|
|
|
6231
6785
|
sessionId = state.activeSessionId,
|
|
6232
6786
|
sessionContext = runController.captureSessionContext(state),
|
|
6233
6787
|
codexThreadId = '',
|
|
6788
|
+
operation,
|
|
6234
6789
|
task,
|
|
6235
6790
|
mode,
|
|
6236
6791
|
model,
|
|
@@ -6247,6 +6802,7 @@
|
|
|
6247
6802
|
resetAutoFollow();
|
|
6248
6803
|
const record = {
|
|
6249
6804
|
id: createRunId(),
|
|
6805
|
+
...(operation === 'compact' ? { operation } : {}),
|
|
6250
6806
|
task,
|
|
6251
6807
|
mode,
|
|
6252
6808
|
...providerSettingsCoordinator.getProviderSnapshot(executionSnapshot?.providerId || state?.providerId),
|
|
@@ -6276,12 +6832,20 @@
|
|
|
6276
6832
|
runAccountScopeId: cachedAccountScopeId || ''
|
|
6277
6833
|
};
|
|
6278
6834
|
const active = findSessionById(sessionId);
|
|
6279
|
-
const titlePatch = active?.titleSource !== 'manual'
|
|
6835
|
+
const titlePatch = operation !== 'compact' && active?.titleSource !== 'manual'
|
|
6280
6836
|
? {
|
|
6281
6837
|
title: deriveSessionTitle([], task),
|
|
6282
6838
|
titleSource: 'auto'
|
|
6283
6839
|
}
|
|
6284
6840
|
: {};
|
|
6841
|
+
// Establish the in-memory owner before publishing or rendering the record.
|
|
6842
|
+
// If presentation throws, runTask still has the identity needed to settle it.
|
|
6843
|
+
const view = currentRunView = {
|
|
6844
|
+
operation, sessionId, sessionContext, codexThreadId, recordId: record.id,
|
|
6845
|
+
runProjectId: record.runProjectId, runAccountScopeId: record.runAccountScopeId,
|
|
6846
|
+
executionSnapshot: record.executionSnapshot, startedAt: Date.now(), queueItemId,
|
|
6847
|
+
nativeRequestId: '', activeTurn: null
|
|
6848
|
+
};
|
|
6285
6849
|
updateSessionById(sessionId, {
|
|
6286
6850
|
runs: runController.replaceRunForRetry(active?.runs, replaceRunId, record),
|
|
6287
6851
|
...titlePatch
|
|
@@ -6290,6 +6854,16 @@
|
|
|
6290
6854
|
|
|
6291
6855
|
const log = panel.querySelector('[data-log]');
|
|
6292
6856
|
const root = renderRunCard(record);
|
|
6857
|
+
Object.assign(view, {
|
|
6858
|
+
root,
|
|
6859
|
+
runProcess: root.querySelector('[data-run-process]'),
|
|
6860
|
+
processLabel: root.querySelector('[data-run-process-summary]'),
|
|
6861
|
+
events: root.querySelector('[data-run-events]'),
|
|
6862
|
+
report: root.querySelector('[data-run-report]'),
|
|
6863
|
+
status: root.querySelector('[data-run-status]'),
|
|
6864
|
+
// Seed references before warm-mirror resolution produces its snapshot.
|
|
6865
|
+
projectFiles: getCurrentProjectReferenceFiles()
|
|
6866
|
+
});
|
|
6293
6867
|
if (state.activeSessionId === sessionId) {
|
|
6294
6868
|
removeEmptyRunsMessage(log);
|
|
6295
6869
|
if (replaceRunId) log.querySelector(`[data-run-id="${cssEscape(replaceRunId)}"]`)?.remove();
|
|
@@ -6300,32 +6874,7 @@
|
|
|
6300
6874
|
renderSessionList();
|
|
6301
6875
|
applySessionLabel();
|
|
6302
6876
|
|
|
6303
|
-
return
|
|
6304
|
-
sessionId,
|
|
6305
|
-
sessionContext,
|
|
6306
|
-
codexThreadId,
|
|
6307
|
-
recordId: record.id,
|
|
6308
|
-
// Mirror the immutable runProjectId on the view so writeback /
|
|
6309
|
-
// accept / undo dispatches don't need to re-look up the record.
|
|
6310
|
-
runProjectId: record.runProjectId,
|
|
6311
|
-
runAccountScopeId: record.runAccountScopeId,
|
|
6312
|
-
executionSnapshot: record.executionSnapshot,
|
|
6313
|
-
root,
|
|
6314
|
-
runProcess: root.querySelector('[data-run-process]'),
|
|
6315
|
-
processLabel: root.querySelector('[data-run-process-summary]'),
|
|
6316
|
-
events: root.querySelector('[data-run-events]'),
|
|
6317
|
-
report: root.querySelector('[data-run-report]'),
|
|
6318
|
-
status: root.querySelector('[data-run-status]'),
|
|
6319
|
-
// A run starts before warm-mirror resolution has produced its lightweight
|
|
6320
|
-
// project snapshot. Seed references from the already-prefetched project
|
|
6321
|
-
// inventory so early stream events and fast Ask responses can still
|
|
6322
|
-
// resolve project-relative locations.
|
|
6323
|
-
projectFiles: getCurrentProjectReferenceFiles(),
|
|
6324
|
-
startedAt: Date.now(),
|
|
6325
|
-
queueItemId,
|
|
6326
|
-
nativeRequestId: '',
|
|
6327
|
-
activeTurn: null
|
|
6328
|
-
};
|
|
6877
|
+
return view;
|
|
6329
6878
|
}
|
|
6330
6879
|
|
|
6331
6880
|
function touchSessionForTerminalRun(sessionId, finishedAt) {
|
|
@@ -6388,6 +6937,7 @@
|
|
|
6388
6937
|
].filter(Boolean).join(' · ');
|
|
6389
6938
|
collapseRunProcess(visibleView, statusText);
|
|
6390
6939
|
}
|
|
6940
|
+
renderContextUsage();
|
|
6391
6941
|
}
|
|
6392
6942
|
|
|
6393
6943
|
|
|
@@ -6402,6 +6952,11 @@
|
|
|
6402
6952
|
}
|
|
6403
6953
|
|
|
6404
6954
|
const owner = currentRunView && findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
6955
|
+
if (storeContextUsage(event, owner, findSessionById(currentRunView?.sessionId), journalSeq)) {
|
|
6956
|
+
scheduleRunStateSave('stream');
|
|
6957
|
+
renderContextUsage();
|
|
6958
|
+
return;
|
|
6959
|
+
}
|
|
6405
6960
|
if (owner && Modules.SubagentActivityModel.ingest(owner, event, {
|
|
6406
6961
|
locale: getLocale(), sanitize: sanitizeAssistantVisibleText, nativeEventSeq: journalSeq
|
|
6407
6962
|
})) {
|
|
@@ -6430,6 +6985,20 @@
|
|
|
6430
6985
|
replaceText: activity.replaceText,
|
|
6431
6986
|
nativeEventSeq: journalSeq
|
|
6432
6987
|
});
|
|
6988
|
+
if (activity.kind !== 'stream') renderContextUsage();
|
|
6989
|
+
}
|
|
6990
|
+
|
|
6991
|
+
function storeContextUsage(event, record, session, sequence) {
|
|
6992
|
+
if (!record) return false;
|
|
6993
|
+
const usage = Modules.ContextUsage.fromEvent(event, {
|
|
6994
|
+
...record, providerRevision: Number(record.providerRevision || 0), eventSeq: sequence
|
|
6995
|
+
});
|
|
6996
|
+
if (!usage || !Modules.ContextUsage.isNewer(usage, record.contextUsage)) return false;
|
|
6997
|
+
record.contextUsage = usage;
|
|
6998
|
+
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(sequence || 0));
|
|
6999
|
+
if (session?.codexThreadId === usage.threadId
|
|
7000
|
+
&& Modules.ContextUsage.isNewer(usage, session.contextUsage)) session.contextUsage = usage;
|
|
7001
|
+
return true;
|
|
6433
7002
|
}
|
|
6434
7003
|
|
|
6435
7004
|
function bindActiveTurn(detail = {}, journalSeq = 0) {
|
|
@@ -6438,16 +7007,24 @@
|
|
|
6438
7007
|
}
|
|
6439
7008
|
const threadId = String(detail.threadId || '');
|
|
6440
7009
|
const turnId = String(detail.turnId || '');
|
|
6441
|
-
currentRunView.activeTurn = { threadId, turnId };
|
|
7010
|
+
currentRunView.activeTurn = currentRunView.operation === 'compact' ? null : { threadId, turnId };
|
|
6442
7011
|
const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
6443
7012
|
if (record) {
|
|
6444
7013
|
record.codexThreadId = threadId; record.codexTurnId = turnId;
|
|
7014
|
+
if (record.operation === 'compact') {
|
|
7015
|
+
const session = findSessionById(currentRunView.sessionId);
|
|
7016
|
+
if (session?.contextUsage?.threadId === threadId
|
|
7017
|
+
&& Date.parse(session.contextUsage.observedAt) < Date.parse(record.startedAt)) {
|
|
7018
|
+
session.contextUsage = null;
|
|
7019
|
+
}
|
|
7020
|
+
}
|
|
6445
7021
|
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(journalSeq || 0));
|
|
6446
7022
|
}
|
|
6447
7023
|
if (threadId) {
|
|
6448
7024
|
updateSessionById(currentRunView.sessionId, { codexThreadId: threadId });
|
|
6449
7025
|
}
|
|
6450
7026
|
renderPendingInputs();
|
|
7027
|
+
renderContextUsage();
|
|
6451
7028
|
saveState().catch(() => {});
|
|
6452
7029
|
}
|
|
6453
7030
|
|
|
@@ -6468,7 +7045,7 @@
|
|
|
6468
7045
|
function appendRunEvent(input = {}) {
|
|
6469
7046
|
const { title, status = 'info', detail, timestamp } = input;
|
|
6470
7047
|
const safeTitle = sanitizeAssistantVisibleText(title) || 'Event';
|
|
6471
|
-
if (!currentRunView
|
|
7048
|
+
if (!currentRunView) {
|
|
6472
7049
|
appendPlainLog(safeTitle);
|
|
6473
7050
|
return;
|
|
6474
7051
|
}
|
|
@@ -6512,21 +7089,20 @@
|
|
|
6512
7089
|
})).filter(item => item.path)
|
|
6513
7090
|
: undefined
|
|
6514
7091
|
};
|
|
7092
|
+
// Keep the activity timestamp current without retaining a run reference
|
|
7093
|
+
// across session normalization, which replaces run and event objects.
|
|
7094
|
+
const nowMs = Date.now();
|
|
7095
|
+
if (nowMs - lastRunActivityBumpMs > 60000
|
|
7096
|
+
&& findRunRecord(currentRunView.recordId, currentRunView.sessionId)) {
|
|
7097
|
+
lastRunActivityBumpMs = nowMs;
|
|
7098
|
+
state = updateActiveSession(state, {});
|
|
7099
|
+
}
|
|
6515
7100
|
const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
6516
7101
|
let renderedEvent = event;
|
|
6517
7102
|
if (record) {
|
|
6518
7103
|
if (Number.isFinite(Number(input.nativeEventSeq))) {
|
|
6519
7104
|
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(input.nativeEventSeq || 0));
|
|
6520
7105
|
}
|
|
6521
|
-
// v1.8.1: keep the session's activity timestamp honest DURING a run —
|
|
6522
|
-
// it was pinned to run start, so the dashboard's 30-minute zombie
|
|
6523
|
-
// heuristic mislabeled any long live run as interrupted (fleet P1).
|
|
6524
|
-
// Throttled to once a minute; updateActiveSession stamps updatedAt.
|
|
6525
|
-
const nowMs = Date.now();
|
|
6526
|
-
if (nowMs - lastRunActivityBumpMs > 60000) {
|
|
6527
|
-
lastRunActivityBumpMs = nowMs;
|
|
6528
|
-
state = updateActiveSession(state, {});
|
|
6529
|
-
}
|
|
6530
7106
|
renderedEvent = event.kind === 'stream'
|
|
6531
7107
|
? upsertRunStreamRecordEvent(record, event)
|
|
6532
7108
|
: event;
|
|
@@ -6922,7 +7498,7 @@
|
|
|
6922
7498
|
}
|
|
6923
7499
|
await Modules.ScopedPersistenceCoordinator.applyReviewTransition({
|
|
6924
7500
|
getState: () => state, runId, transition: WritebackSettlement.applySettlementTransition(run, settlement),
|
|
6925
|
-
persist: flushQueuedSaveState, assertCurrent: options.assertCurrent
|
|
7501
|
+
persist: flushQueuedSaveState, assertCurrent: options.assertCurrent, SessionState: Modules.SessionState
|
|
6926
7502
|
});
|
|
6927
7503
|
}
|
|
6928
7504
|
|
|
@@ -6939,6 +7515,7 @@
|
|
|
6939
7515
|
if (!run) {
|
|
6940
7516
|
return 0;
|
|
6941
7517
|
}
|
|
7518
|
+
run = Modules.UndoOperations.withAvailableUndo(run);
|
|
6942
7519
|
const trackedEditorUndoCount = hasTrackedEditorUndo(run) ? 1 : 0;
|
|
6943
7520
|
if (trackedEditorUndoCount) {
|
|
6944
7521
|
return trackedEditorUndoCount + Modules.UndoOperations.getFileUndoOperations(run).length;
|
|
@@ -6990,16 +7567,20 @@
|
|
|
6990
7567
|
assertCurrent();
|
|
6991
7568
|
}
|
|
6992
7569
|
|
|
6993
|
-
async function settleWritebackRecovery(applied, project, verification, target) {
|
|
7570
|
+
async function settleWritebackRecovery(applied, project, verification, target, requireReviewing) {
|
|
6994
7571
|
const record = findRunRecord(target.recordId, target.sessionId);
|
|
6995
7572
|
if (!record) return;
|
|
7573
|
+
// retryRequireReviewing is in-memory only; a renormalized run copy loses it.
|
|
7574
|
+
const retryReviewing = typeof requireReviewing === 'boolean' ? requireReviewing
|
|
7575
|
+
: typeof record.retryRequireReviewing === 'boolean' ? record.retryRequireReviewing
|
|
7576
|
+
: record.executionSnapshot?.requireReviewing === true;
|
|
6996
7577
|
const owner = recoveryOwner(target);
|
|
6997
7578
|
const skippedPaths = new Set((applied.skipped || []).map(entry => entry.operation?.path));
|
|
6998
7579
|
const creates = (record.retryWriteback?.operations || []).filter(op => skippedPaths.has(op.path));
|
|
6999
7580
|
// Untouched edits (never opened / rejected) and deletes whose server check failed are safe to replay.
|
|
7000
7581
|
const edits = (applied.skipped || []).filter(({ operation: op, result }) => result?.failure?.changedDocument === false && ((op?.type === 'edit'
|
|
7001
7582
|
&& (result.failure.stage === 'navigation' || result.failure.code === 'write_operation_failed')) || (op?.type === 'delete' && result.failure.code === 'source_zip_unavailable'))).map(entry => entry.operation);
|
|
7002
|
-
const base = { ...(record.retryWriteback || { ...owner, id: crypto.randomUUID(), requireReviewing:
|
|
7583
|
+
const base = { ...(record.retryWriteback || { ...owner, id: crypto.randomUUID() }), requireReviewing: retryReviewing,
|
|
7003
7584
|
baseFiles: project?.files || [], requestId: applied.receiptUnconfirmed ? applied.requestId || '' : '' };
|
|
7004
7585
|
// An oversized edit pre-image must not cost the create retries (the intent has a 300 KiB ceiling).
|
|
7005
7586
|
const safeRetry = [[...creates, ...edits], creates].filter(ops => ops.length).map(ops => Modules.SessionState
|
|
@@ -7039,26 +7620,35 @@
|
|
|
7039
7620
|
let finished = false;
|
|
7040
7621
|
runCancellationRequested = false;
|
|
7041
7622
|
runCancellationController = new AbortController();
|
|
7042
|
-
|
|
7043
|
-
record.
|
|
7044
|
-
|
|
7045
|
-
const
|
|
7046
|
-
currentRunView = { root, recordId: runId, sessionId: session.id, runProjectId: owner.projectId,
|
|
7623
|
+
const generation = spaRouteGeneration;
|
|
7624
|
+
const requestId = record.nativeRequestId || `writeback:${owner.sessionId}:${runId}`;
|
|
7625
|
+
const ownerToken = crypto.randomUUID();
|
|
7626
|
+
const retryView = { recordId: runId, sessionId: session.id, runProjectId: owner.projectId,
|
|
7047
7627
|
runAccountScopeId: owner.accountScopeId, executionSnapshot: record.executionSnapshot,
|
|
7048
|
-
startedAt: Date.now(), writebackRetry: true
|
|
7049
|
-
|
|
7050
|
-
runProcess: root?.querySelector('[data-run-process]'), processLabel: root?.querySelector('[data-run-process-summary]') };
|
|
7628
|
+
startedAt: Date.now(), writebackRetry: true };
|
|
7629
|
+
currentRunView = retryView;
|
|
7051
7630
|
setRunning(true);
|
|
7052
7631
|
const assertOwner = () => {
|
|
7053
|
-
if (runCancellationRequested || currentRunView
|
|
7632
|
+
if (runCancellationRequested || currentRunView !== retryView) {
|
|
7054
7633
|
throw Object.assign(new Error('Save checking was cancelled.'), { code: 'codex_cancelled' });
|
|
7055
7634
|
}
|
|
7056
|
-
if (owner.projectId !== getCurrentProjectId() || owner.accountScopeId !== cachedAccountScopeId) {
|
|
7635
|
+
if (generation !== spaRouteGeneration || owner.projectId !== getCurrentProjectId() || owner.accountScopeId !== cachedAccountScopeId) {
|
|
7057
7636
|
throw Object.assign(new Error('The project or account changed.'), { code: 'aborted_project_changed' });
|
|
7058
7637
|
}
|
|
7059
7638
|
};
|
|
7060
7639
|
try {
|
|
7640
|
+
const bound = await activeTurnControl.bindBrowserRun({ requestId, projectKey: owner.projectId,
|
|
7641
|
+
clientRunId: runId, sessionId: session.id, ownerToken });
|
|
7061
7642
|
assertOwner();
|
|
7643
|
+
if (!bound) throw new Error(tx('Could not start sync. Reload the page and try again.', '无法开始同步,请刷新页面后重试。'));
|
|
7644
|
+
// Publish running only after another page can observe this browser owner.
|
|
7645
|
+
live().status = 'running';
|
|
7646
|
+
live().retryingWriteback = true;
|
|
7647
|
+
renderRunHistory({ preserveScroll: true });
|
|
7648
|
+
const root = panel.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
|
|
7649
|
+
Object.assign(retryView, { root, events: root?.querySelector('[data-run-events]'),
|
|
7650
|
+
report: root?.querySelector('[data-run-report]'), status: root?.querySelector('[data-run-status]'),
|
|
7651
|
+
runProcess: root?.querySelector('[data-run-process]'), processLabel: root?.querySelector('[data-run-process-summary]') });
|
|
7062
7652
|
if (intent?.requestId) {
|
|
7063
7653
|
const receipt = await callPageBridge('getWritebackReceipt', { requestId: intent.requestId, runProjectId: owner.projectId });
|
|
7064
7654
|
if (receipt?.state === 'running') throw new Error(tx('The previous upload is still finishing. Try again shortly.', '上一轮上传仍在收尾,请稍后再试。'));
|
|
@@ -7067,9 +7657,11 @@
|
|
|
7067
7657
|
// Edits replay against the run's original base files, so the stale guard still refuses changed documents.
|
|
7068
7658
|
const result = await applySyncChangesToOverleaf(intent.operations.map(op => op.type === 'edit'
|
|
7069
7659
|
? { type: 'write', path: op.path, patches: op.patches, content: op.replaceAll } : op.type === 'delete' ? { type: 'delete', path: op.path } : { ...op, type: 'create' }),
|
|
7070
|
-
|
|
7660
|
+
// The submitted snapshot owns the write mode; older intents may carry a lost value.
|
|
7661
|
+
{ id: owner.projectId, files: intent.baseFiles }, { mode: 'auto', requireReviewing: typeof record.executionSnapshot?.requireReviewing === 'boolean'
|
|
7662
|
+
? record.executionSnapshot.requireReviewing : intent.requireReviewing,
|
|
7071
7663
|
autoRecompile: retryAutoRecompile,
|
|
7072
|
-
retryCreates: true, assistantMessage: tx('Retried the remaining file transfers for this task.', '已重试本轮未完成的文件同步。') });
|
|
7664
|
+
retryCreates: true, assistantMessageKey: 'retry_sync', assistantMessage: tx('Retried the remaining file transfers for this task.', '已重试本轮未完成的文件同步。') });
|
|
7073
7665
|
assertOwner();
|
|
7074
7666
|
if (result.settlement?.facts) live().settlement = WritebackSettlement.mergeRetrySettlement(live().settlement, result.settlement.facts); // Keep files outside the retry.
|
|
7075
7667
|
await finishRunView(result.hasSkippedOperations ? tx('Sync incomplete', '同步未完成') : tx('Synced', '已同步'),
|
|
@@ -7094,11 +7686,19 @@
|
|
|
7094
7686
|
} finally {
|
|
7095
7687
|
// Only finishRunView settles a new terminal status; every other exit
|
|
7096
7688
|
// returns the card to the status it had before the retry.
|
|
7689
|
+
if (currentRunView !== retryView) {
|
|
7690
|
+
activeTurnControl.release(requestId, ownerToken);
|
|
7691
|
+
return;
|
|
7692
|
+
}
|
|
7097
7693
|
for (const copy of new Set([record, live()])) {
|
|
7098
7694
|
delete copy.retryingWriteback;
|
|
7099
7695
|
if (!finished && copy.status === 'running') copy.status = originalStatus;
|
|
7100
7696
|
}
|
|
7101
|
-
|
|
7697
|
+
saveStateSoon();
|
|
7698
|
+
await flushQueuedSaveState().catch(() => {});
|
|
7699
|
+
activeTurnControl.release(requestId, ownerToken);
|
|
7700
|
+
if (currentRunView !== retryView) return;
|
|
7701
|
+
currentRunView = null;
|
|
7102
7702
|
runCancellationRequested = false;
|
|
7103
7703
|
runCancellationController = null;
|
|
7104
7704
|
setRunning(false);
|
|
@@ -7206,11 +7806,18 @@
|
|
|
7206
7806
|
return;
|
|
7207
7807
|
}
|
|
7208
7808
|
|
|
7209
|
-
|
|
7809
|
+
let combinedAppliedOperations = [
|
|
7210
7810
|
...(Array.isArray(record.appliedOperations) ? record.appliedOperations : []),
|
|
7211
7811
|
...appliedOperations
|
|
7212
7812
|
];
|
|
7813
|
+
const checkpoint = buildUndoCheckpoint(project, combinedAppliedOperations);
|
|
7814
|
+
combinedAppliedOperations = checkpoint.appliedOperations;
|
|
7213
7815
|
record.appliedOperations = combinedAppliedOperations;
|
|
7816
|
+
if (checkpoint.undoUnavailable.length) {
|
|
7817
|
+
appendRunEvent({ title: tr('failureReason_snapshot_undo_unavailable_user'), status: 'warning',
|
|
7818
|
+
detail: checkpoint.undoUnavailable.map(item => ({ [tr('detailFile')]: item.path,
|
|
7819
|
+
[tr('detailReason')]: item.code, [tr('detailNext')]: tr('failureReason_snapshot_undo_unavailable_next') })) });
|
|
7820
|
+
}
|
|
7214
7821
|
const createdCheckpoint = buildCreatedFileUndoCheckpoint(project, combinedAppliedOperations, record);
|
|
7215
7822
|
|
|
7216
7823
|
if ((currentRunView?.executionSnapshot?.requireReviewing ?? state.requireReviewing) === true) {
|
|
@@ -7273,7 +7880,6 @@
|
|
|
7273
7880
|
return;
|
|
7274
7881
|
}
|
|
7275
7882
|
|
|
7276
|
-
const checkpoint = buildUndoCheckpoint(project, combinedAppliedOperations);
|
|
7277
7883
|
const createdPaths = new Set(createdCheckpoint.undoOperations.map(operation => operation.path));
|
|
7278
7884
|
checkpoint.undoOperations = [
|
|
7279
7885
|
...checkpoint.undoOperations.filter(operation => !createdPaths.has(operation.path)),
|
|
@@ -7283,10 +7889,7 @@
|
|
|
7283
7889
|
...checkpoint.undoBaseFiles.filter(file => !createdPaths.has(file.path)),
|
|
7284
7890
|
...createdCheckpoint.undoBaseFiles
|
|
7285
7891
|
];
|
|
7286
|
-
if (!checkpoint.undoOperations.length)
|
|
7287
|
-
return;
|
|
7288
|
-
}
|
|
7289
|
-
|
|
7892
|
+
if (!checkpoint.undoOperations.length && !checkpoint.undoUnavailable.length) return;
|
|
7290
7893
|
record.undoOperations = checkpoint.undoOperations;
|
|
7291
7894
|
record.undoBaseFiles = checkpoint.undoBaseFiles;
|
|
7292
7895
|
record.undoTrackedChanges = [];
|
|
@@ -7299,6 +7902,7 @@
|
|
|
7299
7902
|
record.undoStatus = '';
|
|
7300
7903
|
record.partialWriteback = skippedEntries.length > 0;
|
|
7301
7904
|
refreshRunCardControls(record.id);
|
|
7905
|
+
if (!checkpoint.undoOperations.length) return;
|
|
7302
7906
|
appendRunEvent({
|
|
7303
7907
|
title: tr('undoCheckpointPlain', { count: record.undoOperations.length }),
|
|
7304
7908
|
status: 'completed',
|
|
@@ -7361,6 +7965,7 @@
|
|
|
7361
7965
|
guidanceId: sanitizeAssistantVisibleText(event.guidanceId)
|
|
7362
7966
|
};
|
|
7363
7967
|
if (event.failure) normalized.failure = sanitizeAssistantVisibleValue(event.failure);
|
|
7968
|
+
if (event.detailStructured) normalized.detailStructured = sanitizeAssistantVisibleValue(event.detailStructured);
|
|
7364
7969
|
record.events = [...(record.events || []), normalized].slice(-MAX_RUN_EVENTS);
|
|
7365
7970
|
saveStateSoon();
|
|
7366
7971
|
|
|
@@ -7810,8 +8415,14 @@
|
|
|
7810
8415
|
return;
|
|
7811
8416
|
}
|
|
7812
8417
|
|
|
8418
|
+
const groupKey = String(options.groupKey || '');
|
|
8419
|
+
const grouped = groupKey && Array.from(region.children).find(item => item.dataset.groupKey === groupKey);
|
|
8420
|
+
if (grouped) {
|
|
8421
|
+
updateToastGroup(grouped);
|
|
8422
|
+
return;
|
|
8423
|
+
}
|
|
7813
8424
|
const last = region.lastElementChild;
|
|
7814
|
-
if (last?.classList?.contains('codex-toast') && last.dataset.baseText === value) {
|
|
8425
|
+
if (!groupKey && !last?.dataset.groupKey && last?.classList?.contains('codex-toast') && last.dataset.baseText === value) {
|
|
7815
8426
|
const repeatCount = Number(last.dataset.repeatCount || '1') + 1;
|
|
7816
8427
|
last.dataset.repeatCount = String(repeatCount);
|
|
7817
8428
|
const textNode = last.querySelector('[data-toast-text]');
|
|
@@ -7829,15 +8440,34 @@
|
|
|
7829
8440
|
|
|
7830
8441
|
const body = document.createElement('div');
|
|
7831
8442
|
body.className = 'codex-toast-body';
|
|
7832
|
-
|
|
7833
|
-
|
|
8443
|
+
if (options.title) {
|
|
8444
|
+
const title = document.createElement('div');
|
|
8445
|
+
title.className = 'codex-toast-title';
|
|
8446
|
+
title.textContent = options.title;
|
|
8447
|
+
body.append(title);
|
|
8448
|
+
}
|
|
8449
|
+
const textNode = document.createElement('div');
|
|
8450
|
+
textNode.dataset.toastText = 'true';
|
|
8451
|
+
textNode.textContent = value;
|
|
8452
|
+
body.append(textNode);
|
|
8453
|
+
if (groupKey) {
|
|
8454
|
+
item.dataset.groupKey = groupKey;
|
|
8455
|
+
const details = document.createElement('details');
|
|
8456
|
+
details.className = 'codex-toast-group';
|
|
8457
|
+
const summary = document.createElement('summary');
|
|
8458
|
+
const list = document.createElement('ul');
|
|
8459
|
+
list.dataset.toastGroupItems = 'true';
|
|
8460
|
+
details.append(summary, list);
|
|
8461
|
+
body.append(details);
|
|
8462
|
+
}
|
|
7834
8463
|
item.append(body);
|
|
8464
|
+
if (groupKey) updateToastGroup(item);
|
|
7835
8465
|
|
|
7836
8466
|
const close = document.createElement('button');
|
|
7837
8467
|
close.type = 'button';
|
|
7838
8468
|
close.className = 'codex-toast-close';
|
|
7839
8469
|
close.setAttribute('aria-label', tr('dismissNotification'));
|
|
7840
|
-
close.
|
|
8470
|
+
close.innerHTML = Modules.PanelIcons.svg('close', 'codex-close-glyph');
|
|
7841
8471
|
close.addEventListener('click', () => item.remove());
|
|
7842
8472
|
item.append(close);
|
|
7843
8473
|
|
|
@@ -7851,6 +8481,20 @@
|
|
|
7851
8481
|
item.remove();
|
|
7852
8482
|
}, 5200);
|
|
7853
8483
|
}
|
|
8484
|
+
|
|
8485
|
+
function updateToastGroup(toast) {
|
|
8486
|
+
const list = toast.querySelector('[data-toast-group-items]');
|
|
8487
|
+
const value = String(options.groupItem || '').trim();
|
|
8488
|
+
if (value && !Array.from(list.children).some(item => item.textContent === value)) {
|
|
8489
|
+
const item = document.createElement('li');
|
|
8490
|
+
const code = document.createElement('code');
|
|
8491
|
+
code.textContent = value;
|
|
8492
|
+
item.append(code);
|
|
8493
|
+
list.append(item);
|
|
8494
|
+
}
|
|
8495
|
+
toast.querySelector('.codex-toast-group summary').textContent =
|
|
8496
|
+
options.groupSummary?.(list.children.length) || String(list.children.length);
|
|
8497
|
+
}
|
|
7854
8498
|
}
|
|
7855
8499
|
|
|
7856
8500
|
function updateProbeNotice(text) {
|