codex-overleaf-link 2.5.1 → 2.6.5
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 +148 -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 +46 -10
- package/extension/src/content/composerPanel.js +92 -12
- package/extension/src/content/contentRuntime.js +1114 -321
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +147 -42
- package/extension/src/content/generated/content.bundle.js +5761 -1237
- 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 +6 -4
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +53 -14
- package/extension/src/content/modelPicker.js +29 -19
- package/extension/src/content/nativeCompatibilityController.js +52 -19
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +7 -7
- package/extension/src/content/panelMaintenance.js +10 -2
- package/extension/src/content/panelRenderer.js +55 -28
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectSettingsCoordinator.js +30 -6
- package/extension/src/content/providerSettingsDialog.js +28 -19
- package/extension/src/content/recentProjects.js +30 -19
- 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 +27 -13
- package/extension/src/content/runRecoveryActions.js +2 -1
- package/extension/src/content/runResultActions.js +73 -24
- package/extension/src/content/runReviewController.js +102 -37
- package/extension/src/content/runTimelineView.js +224 -25
- package/extension/src/content/scopedPersistencePanelState.js +124 -46
- package/extension/src/content/selectionContextView.js +30 -3
- 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 +13 -4
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateNotice.js +60 -6
- package/extension/src/content/writebackOrchestrator.js +301 -72
- package/extension/src/content/writingStyleSettings.js +70 -17
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/overleafEditor.js +12 -3
- package/extension/src/page/overleafProjectSnapshot.js +56 -3
- package/extension/src/page/snapshotRouter.js +75 -66
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +9 -1
- package/extension/src/page/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +172 -113
- package/extension/src/pageBridge.js +124 -73
- package/extension/src/popup.js +1 -1
- package/extension/src/shared/agentTranscript.js +372 -18
- package/extension/src/shared/auditRecords.js +3 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/compileAdapter.js +58 -17
- package/extension/src/shared/composerAttachmentDraft.js +27 -7
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/failureReasons.js +57 -3
- package/extension/src/shared/i18n.js +315 -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/projectFiles.js +2 -1
- 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/scopedPersistenceBrowserAdapter.js +34 -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 +130 -63
- package/extension/src/shared/storageDb.js +35 -3
- package/extension/src/shared/storageMigration.js +111 -2
- 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 +1293 -549
- 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 +127 -17
- package/native-host/src/debugLog.js +27 -3
- package/native-host/src/index.js +42 -11
- package/native-host/src/localSkills.js +1 -1
- package/native-host/src/mirrorWorkspace.js +47 -3
- package/native-host/src/nativeEnvironment.js +27 -5
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
- package/native-host/src/taskRunnerRuntime.js +28 -8
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/updateManager.js +144 -21
- package/native-host/src/writingStyleRuntime.js +1 -1
- 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';
|
|
@@ -667,6 +697,8 @@
|
|
|
667
697
|
showPluginConfirm, formatOperationFiles, getAppliedEntries, getSkippedEntries
|
|
668
698
|
});
|
|
669
699
|
const writebackOrchestrator = Modules.WritebackOrchestrator.create({
|
|
700
|
+
mirrorFenceStore: root.CodexOverleafModuleRegistry.resolve('ScopedPersistenceBrowserAdapter'),
|
|
701
|
+
getAccountScopeId: () => cachedAccountScopeId,
|
|
670
702
|
tr,
|
|
671
703
|
tx,
|
|
672
704
|
getLocale,
|
|
@@ -762,6 +794,7 @@
|
|
|
762
794
|
|| { providerId: state?.providerId || 'builtin', providerRevision: 0 },
|
|
763
795
|
readSelectedSpeedInput,
|
|
764
796
|
getRenderedModelEntries,
|
|
797
|
+
onSelectionRendered: renderContextUsage,
|
|
765
798
|
persistPanelInputs,
|
|
766
799
|
closeDiagnosticsMenu,
|
|
767
800
|
closeCustomInstructionsSettings,
|
|
@@ -813,20 +846,19 @@
|
|
|
813
846
|
});
|
|
814
847
|
// v1.8.1 D3: the dashboard was a one-shot render — another tab finishing a
|
|
815
848
|
// run, clearing history or creating sessions never showed up. Refresh it
|
|
816
|
-
// (debounced)
|
|
817
|
-
// arrives while the dashboard view is active.
|
|
849
|
+
// (debounced) on visibility, cross-tab run signals, and shared-history changes.
|
|
818
850
|
let dashboardRefreshTimer = null;
|
|
819
851
|
function refreshDashboardSoon() {
|
|
820
|
-
if (panel?.dataset.view !== 'recent-projects') {
|
|
852
|
+
if (panel?.dataset.view !== 'recent-projects' || document.visibilityState !== 'visible') {
|
|
821
853
|
return;
|
|
822
854
|
}
|
|
823
855
|
clearTimeout(dashboardRefreshTimer);
|
|
824
856
|
dashboardRefreshTimer = setTimeout(() => {
|
|
825
|
-
if (panel?.dataset.view !== 'recent-projects') {
|
|
857
|
+
if (panel?.dataset.view !== 'recent-projects' || document.visibilityState !== 'visible') {
|
|
826
858
|
return;
|
|
827
859
|
}
|
|
828
860
|
// Never clobber an in-progress inline rename (fleet P2) — retry later.
|
|
829
|
-
if (panel.querySelector('.recent-projects-
|
|
861
|
+
if (panel.querySelector('.recent-projects-session-rename-input')) {
|
|
830
862
|
refreshDashboardSoon();
|
|
831
863
|
return;
|
|
832
864
|
}
|
|
@@ -834,18 +866,17 @@
|
|
|
834
866
|
const expanded = Array.from(panel.querySelectorAll('[data-project-row-wrap][data-expanded="true"]'))
|
|
835
867
|
.map(wrap => wrap.getAttribute('data-project-row-wrap'))
|
|
836
868
|
.filter(Boolean);
|
|
837
|
-
|
|
838
|
-
recentProjects.renderRecentProjectsVariant({
|
|
839
|
-
restoreExpanded: expanded,
|
|
840
|
-
restoreScrollTop: rootEl ? rootEl.scrollTop : undefined
|
|
841
|
-
}).catch(() => { /* keep last render */ });
|
|
869
|
+
recentProjects.refreshDashboardView({ restoreExpanded: expanded }).catch(() => { /* keep last render */ });
|
|
842
870
|
}, 400);
|
|
843
871
|
}
|
|
844
872
|
document.addEventListener('visibilitychange', () => {
|
|
845
873
|
if (document.visibilityState === 'visible') {
|
|
846
874
|
refreshDashboardSoon();
|
|
875
|
+
} else {
|
|
876
|
+
flushComposerDraftOnLeave();
|
|
847
877
|
}
|
|
848
878
|
});
|
|
879
|
+
window.addEventListener('pagehide', flushComposerDraftOnLeave);
|
|
849
880
|
|
|
850
881
|
const recentProjects = Modules.RecentProjects.create({
|
|
851
882
|
ProjectSessionCleanup: Modules.ProjectSessionCleanup,
|
|
@@ -887,6 +918,7 @@
|
|
|
887
918
|
const MAX_ATTACHMENT_PREVIEW_DATA_URL_CHARS = 768 * 1024;
|
|
888
919
|
const composerAttachmentController = Modules.ComposerAttachments.createComposerAttachmentController({
|
|
889
920
|
getPanel: () => panel,
|
|
921
|
+
icons: Modules.PanelIcons,
|
|
890
922
|
tr,
|
|
891
923
|
tx,
|
|
892
924
|
appendPlainLog,
|
|
@@ -917,11 +949,14 @@
|
|
|
917
949
|
});
|
|
918
950
|
const contextTrayController = ContextTray.createContextTrayController({
|
|
919
951
|
root: window,
|
|
952
|
+
icons: Modules.PanelIcons,
|
|
920
953
|
document,
|
|
921
954
|
getPanel: () => panel,
|
|
922
955
|
getState: () => state,
|
|
923
956
|
setState: nextState => {
|
|
924
957
|
state = nextState;
|
|
958
|
+
// Paint focus changes before ContextTray waits for durable persistence.
|
|
959
|
+
syncProbeStatus();
|
|
925
960
|
},
|
|
926
961
|
saveState,
|
|
927
962
|
saveStateSoon,
|
|
@@ -985,6 +1020,7 @@
|
|
|
985
1020
|
let settingsPanelInstance = null;
|
|
986
1021
|
let diagnosticsPanelInstance = null;
|
|
987
1022
|
let composerPanelInstance = null;
|
|
1023
|
+
let contextUsageView = null;
|
|
988
1024
|
let state = null;
|
|
989
1025
|
let storageKey = LEGACY_STORAGE_KEY;
|
|
990
1026
|
let globalPreferencesController = null;
|
|
@@ -992,9 +1028,14 @@
|
|
|
992
1028
|
let runSubmissionPending = false;
|
|
993
1029
|
let pendingRetryReplacement = null;
|
|
994
1030
|
let runQueueScheduler = null;
|
|
1031
|
+
let deferredSessionStart = null;
|
|
995
1032
|
let runInputCoordinator = null;
|
|
996
1033
|
const activeTurnControl = ActiveTurnControl.create({ chrome });
|
|
997
1034
|
let saveStateTimer = null;
|
|
1035
|
+
const DRAFT_SAVE_DELAY_MS = 800;
|
|
1036
|
+
const DRAFT_SAVE_MAX_WAIT_MS = 4000;
|
|
1037
|
+
let draftSaveTimer = null;
|
|
1038
|
+
let pendingDraftSave = null;
|
|
998
1039
|
// Two-phase saveState scheduling. `saveStateInFlight` flips to true while
|
|
999
1040
|
// an async saveState() is actually writing; `saveStateRunAfterFlight` is
|
|
1000
1041
|
// set when a fresh saveStateSoon() fires during the in-flight phase. When
|
|
@@ -1011,6 +1052,8 @@
|
|
|
1011
1052
|
let storageNoticeKeys = new Set();
|
|
1012
1053
|
let composerSkillInvocation = null;
|
|
1013
1054
|
let composerDraftRevision = 0;
|
|
1055
|
+
// Page-local provenance only; the draft itself stays shared per conversation.
|
|
1056
|
+
let composerDraftEditedHere = false;
|
|
1014
1057
|
let slashCodexOverleafSkills = [];
|
|
1015
1058
|
let slashCodexOverleafSkillsLoaded = false;
|
|
1016
1059
|
let slashCodexOverleafSkillsLoading = false;
|
|
@@ -1033,6 +1076,7 @@
|
|
|
1033
1076
|
showPluginToast,
|
|
1034
1077
|
getPanel: () => panel,
|
|
1035
1078
|
ensurePanelOpen,
|
|
1079
|
+
getIconUrl: () => chrome.runtime.getURL('assets/icons/codex-overleaf-dialog-icon.png'),
|
|
1036
1080
|
getPanelRendererInstance: () => panelRendererInstance,
|
|
1037
1081
|
setBadge: (...args) => PanelRenderer.setBadge(...args),
|
|
1038
1082
|
setDiagnosticsHealth,
|
|
@@ -1051,7 +1095,7 @@
|
|
|
1051
1095
|
getBusyState: () => ({
|
|
1052
1096
|
run: Boolean(currentRunView),
|
|
1053
1097
|
cancelling: runCancellationRequested,
|
|
1054
|
-
storage: saveStateInFlight || Boolean(saveStateTimer),
|
|
1098
|
+
storage: saveStateInFlight || Boolean(saveStateTimer) || Boolean(pendingDraftSave),
|
|
1055
1099
|
reviewAction: trackedChangeInFlight.size > 0,
|
|
1056
1100
|
dialog: Boolean(pluginConfirmController?.isOpen())
|
|
1057
1101
|
}),
|
|
@@ -1081,7 +1125,7 @@
|
|
|
1081
1125
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
1082
1126
|
if (message?.type === 'codex-overleaf/runtime-health-probe') {
|
|
1083
1127
|
sendResponse?.({
|
|
1084
|
-
ok: true,
|
|
1128
|
+
ok: true, runOwnerProtocol: 2, runOwners: [activeTurnControl.getOwner()].filter(Boolean),
|
|
1085
1129
|
version: CodexOverleafCompatibility?.BUILD_TARGET_VERSION || chrome.runtime.getManifest().version
|
|
1086
1130
|
});
|
|
1087
1131
|
return false;
|
|
@@ -1146,6 +1190,7 @@
|
|
|
1146
1190
|
await refreshAccountScopeId();
|
|
1147
1191
|
storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
|
|
1148
1192
|
state = normalizeLoadedPanelState(await loadStoredState());
|
|
1193
|
+
composerDraftEditedHere = false;
|
|
1149
1194
|
await getRunReviewController().recoverInterruptedUndoStatuses();
|
|
1150
1195
|
initializeRunQueueScheduler();
|
|
1151
1196
|
recoverInterruptedRunJournals()
|
|
@@ -1251,7 +1296,6 @@
|
|
|
1251
1296
|
callbacks: {
|
|
1252
1297
|
onLauncherToggle: () => ensurePanelOpen(),
|
|
1253
1298
|
onClosePanel: () => closePanel(),
|
|
1254
|
-
onRefresh: () => refreshProbe({ userInitiated: true }),
|
|
1255
1299
|
onNewSession: () => startNewSession(),
|
|
1256
1300
|
onChangeHistory: () => openChangeHistory(),
|
|
1257
1301
|
onSettingsClick: () => toggleCustomInstructionsSettings(),
|
|
@@ -1273,7 +1317,9 @@
|
|
|
1273
1317
|
diagnosticsPanelInstance = DiagnosticsPanel.create({
|
|
1274
1318
|
container: panelRendererInstance.diagnosticsSlot,
|
|
1275
1319
|
i18n: { tr },
|
|
1320
|
+
icons: Modules.PanelIcons,
|
|
1276
1321
|
callbacks: {
|
|
1322
|
+
onRefresh: () => refreshProbe({ userInitiated: true }),
|
|
1277
1323
|
onBeforeOpen: () => {
|
|
1278
1324
|
closeModelConfigPopover();
|
|
1279
1325
|
closeContextTray();
|
|
@@ -1311,6 +1357,7 @@
|
|
|
1311
1357
|
projectId: getCurrentProjectId(),
|
|
1312
1358
|
i18n: { tr },
|
|
1313
1359
|
button: panel.querySelector('[data-custom-instructions-settings]'),
|
|
1360
|
+
getProjectName: () => { try { return recentProjects.readCurrentProjectNameFromDom(); } catch { return ''; } },
|
|
1314
1361
|
callbacks: {
|
|
1315
1362
|
onBack: () => closeCustomInstructionsSettings(),
|
|
1316
1363
|
onInputChange: event => persistPanelInputs(event),
|
|
@@ -1351,9 +1398,12 @@
|
|
|
1351
1398
|
composerPanelInstance = ComposerPanel.create({
|
|
1352
1399
|
container: panelRendererInstance.composerSlot,
|
|
1353
1400
|
i18n: { tr },
|
|
1401
|
+
icons: Modules.PanelIcons,
|
|
1354
1402
|
attachmentController: composerAttachmentController,
|
|
1355
1403
|
callbacks: {
|
|
1356
1404
|
onSubmit: () => safeRunTask(),
|
|
1405
|
+
getTaskScope: getComposerDraftScope,
|
|
1406
|
+
getTaskValue: () => state.task,
|
|
1357
1407
|
isRunning: () => Boolean(currentRunView),
|
|
1358
1408
|
onCancel: () => cancelActiveRun(),
|
|
1359
1409
|
onPaste: handleComposerPaste,
|
|
@@ -1369,7 +1419,7 @@
|
|
|
1369
1419
|
onAttachClick: () => panel.querySelector('[data-attach-input]')?.click(),
|
|
1370
1420
|
onAttachInput: handleAttachInputChange,
|
|
1371
1421
|
onContextRefresh: () => loadContextFiles({ force: true }),
|
|
1372
|
-
onModelConfigToggle: () => toggleModelConfigPopover(),
|
|
1422
|
+
onModelConfigToggle: () => { contextUsageView?.close(); toggleModelConfigPopover(); },
|
|
1373
1423
|
onModelConfigChoiceClick: event => {
|
|
1374
1424
|
handleModelConfigChoiceClick(event).catch(error => {
|
|
1375
1425
|
appendPlainLog(tx(`Failed to update model settings: ${error.message}`, `更新模型设置失败:${error.message}`));
|
|
@@ -1382,6 +1432,18 @@
|
|
|
1382
1432
|
}
|
|
1383
1433
|
});
|
|
1384
1434
|
|
|
1435
|
+
contextUsageView = Modules.ContextUsageView.create({
|
|
1436
|
+
getPanel: () => panel, getLocale, tr,
|
|
1437
|
+
onOpen: () => {
|
|
1438
|
+
closeModelConfigPopover();
|
|
1439
|
+
closeContextTray();
|
|
1440
|
+
closeSlashMenu();
|
|
1441
|
+
},
|
|
1442
|
+
getCompactAvailability,
|
|
1443
|
+
level: Modules.ContextUsage.level,
|
|
1444
|
+
onCompact: () => safeRunTask({ operation: 'compact' })
|
|
1445
|
+
});
|
|
1446
|
+
renderContextUsage();
|
|
1385
1447
|
installContextDismiss();
|
|
1386
1448
|
installModelConfigDismiss();
|
|
1387
1449
|
bindLogAutoFollow();
|
|
@@ -1393,7 +1455,7 @@
|
|
|
1393
1455
|
primeCodexLocalSkillsCatalog().catch(() => { /* retry through settings, slash, or submission */ });
|
|
1394
1456
|
}
|
|
1395
1457
|
|
|
1396
|
-
PanelRenderer.setVisible(
|
|
1458
|
+
PanelRenderer.setVisible(panelRendererInstance, true);
|
|
1397
1459
|
}
|
|
1398
1460
|
|
|
1399
1461
|
// Panel prefetch and first submission share the same official catalog read.
|
|
@@ -1559,7 +1621,7 @@
|
|
|
1559
1621
|
panelRendererInstance?.setLocale?.({ launcher: tx('Show or hide Codex panel', '显示或隐藏 Codex 边栏'), close: tx('Close Codex panel', '关闭 Codex 边栏') });
|
|
1560
1622
|
|
|
1561
1623
|
setElementTitleAndAria('[data-panel-resize-handle]', tr('resizePanel'), tr('resizePanel'));
|
|
1562
|
-
|
|
1624
|
+
setRefreshProbeLoading(panel?.querySelector('[data-refresh]')?.dataset.loading === 'true');
|
|
1563
1625
|
// State-dependent tooltip: re-derive from the dot's current health so a
|
|
1564
1626
|
// locale/session refresh doesn't clobber it back to the generic label.
|
|
1565
1627
|
setDiagnosticsHealth(panel.querySelector('[data-diagnostics-health-dot]')?.dataset.health || 'unknown');
|
|
@@ -1610,7 +1672,7 @@
|
|
|
1610
1672
|
}
|
|
1611
1673
|
const reviewToggle = panel.querySelector('.codex-review-toggle');
|
|
1612
1674
|
if (reviewToggle) {
|
|
1613
|
-
|
|
1675
|
+
syncReviewingTooltip();
|
|
1614
1676
|
}
|
|
1615
1677
|
const recompileToggle = panel.querySelector('.codex-recompile-toggle');
|
|
1616
1678
|
if (recompileToggle) {
|
|
@@ -1633,7 +1695,11 @@
|
|
|
1633
1695
|
updateExperimentalOtMenuStatus();
|
|
1634
1696
|
renderModelConfigChoices();
|
|
1635
1697
|
updateModelDisplay();
|
|
1698
|
+
// The header switcher's title and accessible name carry localized text too.
|
|
1699
|
+
applySessionLabel();
|
|
1636
1700
|
renderSessionList();
|
|
1701
|
+
renderContextUsage();
|
|
1702
|
+
if (panel.querySelector('[data-slash-menu]')?.hidden === false) updateSlashMenuForTaskInput();
|
|
1637
1703
|
renderContextSelection();
|
|
1638
1704
|
renderContextSummary();
|
|
1639
1705
|
renderPendingInputs();
|
|
@@ -1810,7 +1876,8 @@
|
|
|
1810
1876
|
// Capture the click before any preparation can yield to another draft/session.
|
|
1811
1877
|
readPanelInputs();
|
|
1812
1878
|
const queuedInput = options.queuedInput || null;
|
|
1813
|
-
const
|
|
1879
|
+
const compact = !queuedInput && options.operation === 'compact';
|
|
1880
|
+
const task = compact ? '/compact' : String(queuedInput?.text || state.task || '').trim();
|
|
1814
1881
|
if (!task) {
|
|
1815
1882
|
appendLog(tx('Enter a task first.', '请先输入任务。'));
|
|
1816
1883
|
return;
|
|
@@ -1818,32 +1885,38 @@
|
|
|
1818
1885
|
const submittedSessionId = state.activeSessionId;
|
|
1819
1886
|
const submittedSession = runController.captureSessionContext(state);
|
|
1820
1887
|
const submittedThreadId = getActiveSession(state)?.codexThreadId || '';
|
|
1888
|
+
if (compact && !submittedThreadId) {
|
|
1889
|
+
showPluginToast(tr('compactNoThread'), { status: 'info' });
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1821
1892
|
const submittedDraft = state.task;
|
|
1822
1893
|
const submittedRetryReplacement = pendingRetryReplacement;
|
|
1823
1894
|
const submittedWritingStyleScope = { accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId() };
|
|
1824
1895
|
const submittedPanelState = {
|
|
1825
|
-
mode: state.mode,
|
|
1896
|
+
mode: compact ? 'ask' : state.mode,
|
|
1826
1897
|
providerId: state.providerId,
|
|
1827
1898
|
model: state.model,
|
|
1828
1899
|
reasoningEffort: state.reasoningEffort,
|
|
1829
1900
|
speedTier: state.speedTier,
|
|
1830
1901
|
autoRecompile: state.autoRecompile !== false,
|
|
1831
1902
|
requireReviewing: state.requireReviewing === true,
|
|
1832
|
-
focusFiles: getActiveFocusFiles(),
|
|
1833
|
-
selectionContext: selectionContextView.capture()
|
|
1903
|
+
focusFiles: compact ? [] : getActiveFocusFiles(),
|
|
1904
|
+
selectionContext: compact ? null : selectionContextView.capture()
|
|
1834
1905
|
};
|
|
1835
1906
|
const queuedContext = queuedInput
|
|
1836
1907
|
? RunInputQueue.normalizeSubmissionContext(queuedInput.payload?.submissionContext) : null;
|
|
1837
1908
|
const submittedCustomInstructions = queuedContext ? queuedContext.customInstructions : getCustomInstructionsForCurrentProject();
|
|
1838
1909
|
const submittedSkillLoadingSettings = queuedContext ? queuedContext.skillLoadingSettings : getSkillLoadingSettings();
|
|
1839
|
-
const submittedAttachments = queuedInput ? [] : getComposerAttachmentsForRun();
|
|
1840
|
-
const submittedSkillInvocation = queuedContext ? queuedContext.skillInvocation : getComposerSkillInvocationForRun();
|
|
1910
|
+
const submittedAttachments = queuedInput || compact ? [] : getComposerAttachmentsForRun();
|
|
1911
|
+
const submittedSkillInvocation = compact ? null : queuedContext ? queuedContext.skillInvocation : getComposerSkillInvocationForRun();
|
|
1841
1912
|
let submittedEnabledSkillIds = queuedContext ? queuedContext.enabledCodexOverleafSkillIds : getEnabledCodexOverleafSkillIds();
|
|
1842
1913
|
const submittedSkillEnabled = { ...state.codexOverleafSkillEnabled };
|
|
1843
1914
|
const submittedLocalSkillEnabled = { ...state.codexLocalSkillEnabled };
|
|
1844
1915
|
const submittedSelectionRevision = selectionContextView.getRevision?.();
|
|
1845
|
-
let submittedEnabledLocalSkillIds = queuedContext ? queuedContext.enabledCodexLocalSkillIds : [];
|
|
1846
|
-
|
|
1916
|
+
let submittedEnabledLocalSkillIds = queuedContext ? queuedContext.enabledCodexLocalSkillIds : compact ? getEnabledCodexLocalSkillIds() : [];
|
|
1917
|
+
// Capture submission first; a save must not adopt text typed while it waits.
|
|
1918
|
+
if (!queuedInput) await flushComposerDraft({ requireDurable: true });
|
|
1919
|
+
if (!compact && !queuedInput && submittedSkillLoadingSettings.loadCodexLocalSkills !== false) {
|
|
1847
1920
|
const localSkills = await primeCodexLocalSkillsCatalog({ required: true, force: submittedSkillInvocation?.scope === 'codex-local' });
|
|
1848
1921
|
submittedEnabledLocalSkillIds = localSkills.filter(skill => Object.hasOwn(submittedLocalSkillEnabled, skill.id)
|
|
1849
1922
|
? submittedLocalSkillEnabled[skill.id] !== false : skill.enabled !== false).map(skill => skill.id);
|
|
@@ -1853,7 +1926,7 @@
|
|
|
1853
1926
|
throw new Error(tx('The selected local Codex skill is unavailable or disabled. Select an enabled skill and try again.',
|
|
1854
1927
|
'所选 Codex 本地技能不可用或已关闭,请选择已启用的技能后重试。'));
|
|
1855
1928
|
}
|
|
1856
|
-
if (!queuedInput && submittedSkillLoadingSettings.loadCodexOverleafSkills !== false) {
|
|
1929
|
+
if (!compact && !queuedInput && submittedSkillLoadingSettings.loadCodexOverleafSkills !== false) {
|
|
1857
1930
|
const skills = await primeCodexOverleafSkillsCatalog({ required: true });
|
|
1858
1931
|
submittedEnabledSkillIds = skills.map(skill => String(skill?.id || '').trim())
|
|
1859
1932
|
.filter(id => id && submittedSkillEnabled[id] !== false);
|
|
@@ -1862,18 +1935,19 @@
|
|
|
1862
1935
|
const pendingMirror = getPendingMirrorRefresh?.();
|
|
1863
1936
|
if (pendingMirror) await pendingMirror.catch(() => {});
|
|
1864
1937
|
await providerSettingsCoordinator.ensureLoaded();
|
|
1865
|
-
if (!queuedInput) await writingStyleSettings.ensureLoaded(true);
|
|
1938
|
+
if (!queuedInput && !compact) await writingStyleSettings.ensureLoaded(true);
|
|
1866
1939
|
if (submittedWritingStyleScope.accountScopeId !== cachedAccountScopeId
|
|
1867
1940
|
|| submittedWritingStyleScope.projectId !== getCurrentProjectId()) {
|
|
1868
1941
|
throw new Error(tx('The active project changed before submission. Submit the task again.',
|
|
1869
1942
|
'提交前活动项目已改变,请重新提交任务。'));
|
|
1870
1943
|
}
|
|
1871
|
-
if (!findSessionById(submittedSessionId) || currentRunView
|
|
1944
|
+
if (!findSessionById(submittedSessionId) || currentRunView
|
|
1945
|
+
|| (compact && findSessionById(submittedSessionId)?.codexThreadId !== submittedThreadId)) {
|
|
1872
1946
|
throw new Error(tx('The submitted session changed before submission. Submit the task again.',
|
|
1873
1947
|
'提交所属会话已改变,请重新提交任务。'));
|
|
1874
1948
|
}
|
|
1875
|
-
submittedPanelState.writingStyle = queuedInput ? null : writingStyleSettings.capture();
|
|
1876
|
-
|
|
1949
|
+
submittedPanelState.writingStyle = queuedInput || compact ? null : writingStyleSettings.capture();
|
|
1950
|
+
let executionSnapshot = queuedInput
|
|
1877
1951
|
? RunExecutionSnapshot.resolveForExecution(
|
|
1878
1952
|
RunExecutionSnapshot.fromQueuePayload(queuedInput.payload),
|
|
1879
1953
|
{ catalog: providerSettingsCoordinator.getCatalog() }
|
|
@@ -1881,8 +1955,8 @@
|
|
|
1881
1955
|
: captureCurrentExecutionSnapshot(submittedPanelState);
|
|
1882
1956
|
// Freeze submitted run identity before the panel can change during the run.
|
|
1883
1957
|
const submittedMode = executionSnapshot.mode;
|
|
1884
|
-
|
|
1885
|
-
const replaceRunId = queuedInput ? '' : runController.resolveRetryReplacement({
|
|
1958
|
+
let submittedRequireReviewing = executionSnapshot.requireReviewing === true;
|
|
1959
|
+
const replaceRunId = queuedInput || compact ? '' : runController.resolveRetryReplacement({
|
|
1886
1960
|
candidate: submittedRetryReplacement,
|
|
1887
1961
|
activeSessionId: submittedSessionId,
|
|
1888
1962
|
runs: findSessionById(submittedSessionId)?.runs,
|
|
@@ -1892,26 +1966,78 @@
|
|
|
1892
1966
|
runCancellationRequested = false;
|
|
1893
1967
|
runCancellationController = new AbortController();
|
|
1894
1968
|
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;
|
|
1969
|
+
const runSessionId = submittedSessionId;
|
|
1912
1970
|
let runAuditDraft = null;
|
|
1913
1971
|
let localSkillRestorePoint = null;
|
|
1914
1972
|
try {
|
|
1973
|
+
currentRunView = startRunView({
|
|
1974
|
+
sessionId: submittedSessionId,
|
|
1975
|
+
sessionContext: submittedSession,
|
|
1976
|
+
codexThreadId: submittedThreadId,
|
|
1977
|
+
operation: compact ? 'compact' : undefined,
|
|
1978
|
+
task,
|
|
1979
|
+
mode: submittedMode,
|
|
1980
|
+
model: executionSnapshot.model,
|
|
1981
|
+
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
1982
|
+
speedTier: executionSnapshot.speedTier,
|
|
1983
|
+
executionSnapshot,
|
|
1984
|
+
attachments: submittedAttachments,
|
|
1985
|
+
skillInvocation: submittedSkillInvocation,
|
|
1986
|
+
queueItemId: queuedInput?.id || '',
|
|
1987
|
+
replaceRunId
|
|
1988
|
+
});
|
|
1989
|
+
// Preparation can await project/audit work before a Native request exists.
|
|
1990
|
+
// Give other pages a live owner to check throughout that interval.
|
|
1991
|
+
const bound = await awaitRunStep(activeTurnControl.bindBrowserRun({
|
|
1992
|
+
requestId: ActiveTurnControl.getStartupRequestId(runSessionId, currentRunView.recordId),
|
|
1993
|
+
projectKey: currentRunView.runProjectId, clientRunId: currentRunView.recordId,
|
|
1994
|
+
sessionId: runSessionId, ownerToken: crypto.randomUUID()
|
|
1995
|
+
}));
|
|
1996
|
+
throwIfRunCancellationRequested();
|
|
1997
|
+
if (!bound) throw new Error(tx('Could not start the task. Reload the page and try again.',
|
|
1998
|
+
'无法启动任务,请刷新页面后重试。'));
|
|
1999
|
+
renderContextUsage();
|
|
2000
|
+
if (!queuedInput && !compact && pendingRetryReplacement === submittedRetryReplacement) pendingRetryReplacement = null;
|
|
2001
|
+
if (compact) {
|
|
2002
|
+
announceCrossTabRunStart();
|
|
2003
|
+
// Only the slash-command text is consumed; a draft kept while compacting from the popover stays.
|
|
2004
|
+
if (options.consumeCompactDraft === true && state.activeSessionId === submittedSessionId
|
|
2005
|
+
&& /^\/[^\s/]*$/.test(String(submittedDraft || '').trim())
|
|
2006
|
+
&& panel?.querySelector('[data-task]')?.value === submittedDraft) {
|
|
2007
|
+
panel.querySelector('[data-task]').value = '';
|
|
2008
|
+
updateSessionById(submittedSessionId, { task: '' });
|
|
2009
|
+
recordSubmittedDraftConsumption(submittedSessionId, submittedDraft);
|
|
2010
|
+
composerDraftEditedHere = false;
|
|
2011
|
+
composerDraftRevision += 1;
|
|
2012
|
+
autosizeTaskTextarea();
|
|
2013
|
+
closeSlashMenu();
|
|
2014
|
+
syncComposerSendAvailability(true);
|
|
2015
|
+
}
|
|
2016
|
+
appendRunEvent({ title: tr('contextUsageCompacting'), status: 'running',
|
|
2017
|
+
activity: { v: 1, kind: 'lifecycle', state: 'running', target: 'context_compaction' } });
|
|
2018
|
+
renderContextUsage();
|
|
2019
|
+
const response = await awaitRunStep(sendTrackedCodexRun({
|
|
2020
|
+
operation: 'compact', task: '/compact', mode: 'ask',
|
|
2021
|
+
projectId: currentRunView.runProjectId, accountScopeId: currentRunView.runAccountScopeId,
|
|
2022
|
+
clientRunId: currentRunView.recordId, clientSessionId: runSessionId,
|
|
2023
|
+
threadId: submittedThreadId, model: executionSnapshot.model,
|
|
2024
|
+
reasoningEffort: executionSnapshot.reasoningEffort, speedTier: executionSnapshot.speedTier,
|
|
2025
|
+
providerSelection: RunExecutionSnapshot.toProviderSelection(executionSnapshot),
|
|
2026
|
+
locale: getLocale(), ...submittedSkillLoadingSettings,
|
|
2027
|
+
enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
|
|
2028
|
+
enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds
|
|
2029
|
+
}));
|
|
2030
|
+
if (!response?.ok) throw Object.assign(new Error(response?.error?.message || 'Compaction failed'), response?.error);
|
|
2031
|
+
if (response.result?.operation !== 'compact' || response.result?.status !== 'completed'
|
|
2032
|
+
|| response.result?.threadId !== submittedThreadId) {
|
|
2033
|
+
throw Object.assign(new Error('Compaction completion was not confirmed'), { code: 'codex_compact_unconfirmed' });
|
|
2034
|
+
}
|
|
2035
|
+
throwIfRunCancellationRequested();
|
|
2036
|
+
appendCompletionReport({ conclusion: tr('compactCompleted'), conclusionKey: 'context_compacted',
|
|
2037
|
+
status: 'completed', mode: 'ask', operations: [], applyResults: [] });
|
|
2038
|
+
await finishRunView(tr('compactCompleted'), 'completed');
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
1915
2041
|
if (queuedInput?.id) {
|
|
1916
2042
|
await runQueueScheduler?.markExecuting(runSessionId, queuedInput.id, currentRunView.recordId);
|
|
1917
2043
|
}
|
|
@@ -1990,7 +2116,30 @@
|
|
|
1990
2116
|
await awaitRunStep(pauseOtWarmMirror('run-start'));
|
|
1991
2117
|
const writeSafety = await awaitRunStep(preflightWriteSafety({
|
|
1992
2118
|
mode: submittedMode,
|
|
1993
|
-
requireReviewing: submittedRequireReviewing
|
|
2119
|
+
requireReviewing: submittedRequireReviewing,
|
|
2120
|
+
onUntracked: async choice => {
|
|
2121
|
+
submittedRequireReviewing = false;
|
|
2122
|
+
// Keep the effective policy identical in dispatch, recovery and Undo consumers.
|
|
2123
|
+
const view = currentRunView;
|
|
2124
|
+
const record = findRunRecord(view.recordId, view.sessionId);
|
|
2125
|
+
executionSnapshot = RunExecutionSnapshot.applyUntrackedDecision(view, record, choice);
|
|
2126
|
+
if (choice === 'disable') {
|
|
2127
|
+
updateSessionById(view.sessionId, { requireReviewing: false });
|
|
2128
|
+
if (state.activeSessionId === view.sessionId) {
|
|
2129
|
+
state.requireReviewing = false;
|
|
2130
|
+
panel.querySelector('[data-require-reviewing]').checked = false;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
appendRunEvent({ title: tr('reviewingUnavailableRunUntracked'), status: 'completed',
|
|
2134
|
+
technicalDetail: { code: 'reviewing_unavailable', submittedRequireReviewing: true,
|
|
2135
|
+
effectiveRequireReviewing: false, choice } });
|
|
2136
|
+
saveStateSoon(0);
|
|
2137
|
+
syncProbeStatus();
|
|
2138
|
+
runAuditDraft = await finalizeAuditDraftPartial(runAuditDraft, {
|
|
2139
|
+
requireReviewing: false, trackingDecision: choice
|
|
2140
|
+
});
|
|
2141
|
+
await flushQueuedSaveState({ requireDurable: true });
|
|
2142
|
+
}
|
|
1994
2143
|
}));
|
|
1995
2144
|
if (!writeSafety.ok) {
|
|
1996
2145
|
await finalizeAuditRecord(runAuditDraft, {
|
|
@@ -2001,9 +2150,11 @@
|
|
|
2001
2150
|
}
|
|
2002
2151
|
|
|
2003
2152
|
let focusFiles = [...executionSnapshot.focusFiles];
|
|
2153
|
+
const mirrorOwner = currentRunView, mirrorRouteGeneration = spaRouteGeneration;
|
|
2004
2154
|
const warmStart = await awaitRunStep(resolveWarmRunStart({ focusFiles, mode: submittedMode }));
|
|
2155
|
+
const mirrorFence = await awaitRunStep(writebackOrchestrator.refreshMirrorReuseFence(mirrorOwner.runProjectId, mirrorOwner.runAccountScopeId));
|
|
2005
2156
|
let project = warmStart.project || null;
|
|
2006
|
-
let useExistingMirror = warmStart.useExistingMirror;
|
|
2157
|
+
let useExistingMirror = warmStart.useExistingMirror && !mirrorFence;
|
|
2007
2158
|
let fileOverlays = warmStart.fileOverlays || null;
|
|
2008
2159
|
let otWarmStart = warmStart.otWarmStart === true;
|
|
2009
2160
|
if (!useExistingMirror) {
|
|
@@ -2012,6 +2163,18 @@
|
|
|
2012
2163
|
status: 'running'
|
|
2013
2164
|
});
|
|
2014
2165
|
project = await awaitRunStep(getRunProjectSnapshot());
|
|
2166
|
+
if (mirrorFence) {
|
|
2167
|
+
const assertCurrent = () => {
|
|
2168
|
+
throwIfRunCancellationRequested();
|
|
2169
|
+
if (mirrorOwner !== currentRunView || mirrorRouteGeneration !== spaRouteGeneration
|
|
2170
|
+
|| mirrorOwner.runProjectId !== getCurrentProjectId()
|
|
2171
|
+
|| mirrorOwner.runAccountScopeId !== cachedAccountScopeId) throw createRunCancellationError();
|
|
2172
|
+
};
|
|
2173
|
+
await awaitRunStep(writebackOrchestrator.syncFencedMirror(mirrorOwner.runProjectId, project, {
|
|
2174
|
+
fence: mirrorFence, assertCurrent,
|
|
2175
|
+
sendNative: payload => nativeCompatibilityController.sendBackgroundNative(payload, { assertCurrent })
|
|
2176
|
+
}));
|
|
2177
|
+
}
|
|
2015
2178
|
}
|
|
2016
2179
|
throwIfRunCancellationRequested();
|
|
2017
2180
|
if (!useExistingMirror) {
|
|
@@ -2052,7 +2215,7 @@
|
|
|
2052
2215
|
fileCount: (project?.files || []).length
|
|
2053
2216
|
}
|
|
2054
2217
|
});
|
|
2055
|
-
appendCompletionReport({
|
|
2218
|
+
appendCompletionReport({ conclusionKey: 'full_project_unread',
|
|
2056
2219
|
conclusion: tx('This run did not continue: the full Overleaf project was not read.', '这轮没有继续:没有读到完整的 Overleaf 项目内容。'),
|
|
2057
2220
|
status: 'blocked',
|
|
2058
2221
|
operations: [],
|
|
@@ -2180,8 +2343,8 @@
|
|
|
2180
2343
|
submittedMode
|
|
2181
2344
|
})));
|
|
2182
2345
|
|
|
2183
|
-
//
|
|
2184
|
-
if (!response.ok && response.error?.code
|
|
2346
|
+
// A stale mirror or contradictory editor overlay requires a fresh read.
|
|
2347
|
+
if (!response.ok && ['mirror_stale', 'document_identity_mismatch'].includes(response.error?.code) && useExistingMirror) {
|
|
2185
2348
|
appendRunEvent({ title: tr('warmMirrorStaleRetryTitle'), status: 'running' });
|
|
2186
2349
|
const staleRetry = await awaitRunStep(prepareMirrorStaleRetry({
|
|
2187
2350
|
project: await awaitRunStep(getRunProjectSnapshot()),
|
|
@@ -2239,12 +2402,17 @@
|
|
|
2239
2402
|
const userChoice = await showThreadResumeFailedPrompt();
|
|
2240
2403
|
if (userChoice === 'new') {
|
|
2241
2404
|
updateSessionById(runSessionId, { codexThreadId: '' });
|
|
2405
|
+
renderContextUsage();
|
|
2242
2406
|
const StorageDb = Modules.StorageDb;
|
|
2243
|
-
|
|
2244
|
-
|
|
2407
|
+
// A live session goes through the ordinary writer. An envelope
|
|
2408
|
+
// conflict there must not abort the run that is choosing a new thread.
|
|
2409
|
+
if (findSessionById(runSessionId)) {
|
|
2410
|
+
saveStateSoon();
|
|
2411
|
+
} else if (StorageDb) {
|
|
2412
|
+
const record = await StorageDb.getRecord('sessions', runSessionId, { adopt: false });
|
|
2245
2413
|
if (record) {
|
|
2246
2414
|
record.codexThreadId = '';
|
|
2247
|
-
await StorageDb.putRecord('sessions', record);
|
|
2415
|
+
await StorageDb.putRecord('sessions', record, { adopt: false });
|
|
2248
2416
|
}
|
|
2249
2417
|
}
|
|
2250
2418
|
appendRunEvent({ title: tx('Creating a new Codex conversation thread.', '正在新建 Codex 会话线程。'), status: 'running' });
|
|
@@ -2291,15 +2459,20 @@
|
|
|
2291
2459
|
// Structured FailureReason (§9.7): split bridge-availability from
|
|
2292
2460
|
// Codex-side errors. `native_connection_failed` / `native_unavailable`
|
|
2293
2461
|
// / `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).
|
|
2462
|
+
// so emit `native_bridge_unavailable` (blocked, retryable). Recognized
|
|
2463
|
+
// Codex failures retain their specific cause; only unmatched errors
|
|
2464
|
+
// fall back to `codex_no_usable_result` (error, retryable).
|
|
2297
2465
|
// Map native/codex failures onto their SPECIFIC catalog codes — the
|
|
2298
2466
|
// timeout/not-found/output-limit entries (and their bilingual next
|
|
2299
2467
|
// steps) existed but were never emitted, so everything collapsed into
|
|
2300
2468
|
// "no usable result" and pointed users at the wrong recovery.
|
|
2301
2469
|
const runErrorMessage = String(response.error?.message || '');
|
|
2302
|
-
const codexRunFailure =
|
|
2470
|
+
const codexRunFailure = translated.failureCode === 'codex_context_limit'
|
|
2471
|
+
? buildContentFailure('codex_context_limit', { path: 'codex.run' }, {
|
|
2472
|
+
technicalMessage: runErrorMessage,
|
|
2473
|
+
evidence: { errorCode: response.error?.code || '' }
|
|
2474
|
+
})
|
|
2475
|
+
: response.error?.code === 'codex_local_skill_unavailable'
|
|
2303
2476
|
? buildContentFailure('codex_local_skill_unavailable', { path: 'codex.run' }, { technicalMessage: runErrorMessage })
|
|
2304
2477
|
: translated.failureCode === 'codex_local_skills_failed'
|
|
2305
2478
|
? buildContentFailure('codex_local_skills_failed', { path: 'codex.run' }, { technicalMessage: runErrorMessage })
|
|
@@ -2360,7 +2533,7 @@
|
|
|
2360
2533
|
detail: response.error
|
|
2361
2534
|
});
|
|
2362
2535
|
appendCompletionReport({
|
|
2363
|
-
conclusion: translated.conclusion,
|
|
2536
|
+
conclusion: translated.conclusion, conclusionKey: translated.conclusionKey,
|
|
2364
2537
|
status: response.error?.code === 'project_locked' ? 'blocked' : 'failed',
|
|
2365
2538
|
operations: [],
|
|
2366
2539
|
applyResults: [],
|
|
@@ -2410,7 +2583,7 @@
|
|
|
2410
2583
|
status: 'failed',
|
|
2411
2584
|
failure: emptyFailure
|
|
2412
2585
|
});
|
|
2413
|
-
appendCompletionReport({
|
|
2586
|
+
appendCompletionReport({ conclusionKey: 'empty_result',
|
|
2414
2587
|
conclusion: emptyResultConclusion,
|
|
2415
2588
|
status: 'failed',
|
|
2416
2589
|
operations: [],
|
|
@@ -2439,7 +2612,7 @@
|
|
|
2439
2612
|
});
|
|
2440
2613
|
runSettlementPersistence.applyToCurrentRun(syncOutcome.settlement);
|
|
2441
2614
|
if (runCancellationRequested) {
|
|
2442
|
-
throw createRunCancellationError();
|
|
2615
|
+
throw Object.assign(createRunCancellationError(), { writebackOutcome: syncOutcome });
|
|
2443
2616
|
}
|
|
2444
2617
|
await finalizeAuditRecord(runAuditDraft, {
|
|
2445
2618
|
...(syncOutcome.audit || {}),
|
|
@@ -2514,12 +2687,14 @@
|
|
|
2514
2687
|
if (returnedThreadId && returnedThreadId !== codexThreadId) {
|
|
2515
2688
|
updateSessionById(runSessionId, { codexThreadId: returnedThreadId });
|
|
2516
2689
|
const StorageDb = Modules.StorageDb;
|
|
2517
|
-
|
|
2518
|
-
|
|
2690
|
+
// The live session is saved below with its latest draft. Writing an
|
|
2691
|
+
// older DB snapshot here can give stale text a newer timestamp.
|
|
2692
|
+
if (StorageDb && !findSessionById(runSessionId)) {
|
|
2693
|
+
const record = await StorageDb.getRecord('sessions', runSessionId, { adopt: false });
|
|
2519
2694
|
if (record) {
|
|
2520
2695
|
record.codexThreadId = returnedThreadId;
|
|
2521
2696
|
record.updatedAt = new Date().toISOString();
|
|
2522
|
-
await StorageDb.putRecord('sessions', record);
|
|
2697
|
+
await StorageDb.putRecord('sessions', record, { adopt: false });
|
|
2523
2698
|
}
|
|
2524
2699
|
}
|
|
2525
2700
|
}
|
|
@@ -2540,17 +2715,46 @@
|
|
|
2540
2715
|
}
|
|
2541
2716
|
} catch (error) {
|
|
2542
2717
|
if (runCancellationRequested || isRunCancellationError(error)) {
|
|
2543
|
-
|
|
2718
|
+
if (currentRunView?.reviewingUnavailablePreflight) {
|
|
2719
|
+
appendCompletionReport({ conclusionKey: 'launch_not_started',
|
|
2720
|
+
conclusion: tx('This task did not start: Codex did not run and no files were written.', '这轮任务没有开始:Codex 没有运行,也没有写入文件。'),
|
|
2721
|
+
status: 'blocked', operations: [], applyResults: [],
|
|
2722
|
+
failure: buildContentFailure('reviewing_unavailable', {}, {
|
|
2723
|
+
changedDocument: false, evidence: { declinedChoice: true }
|
|
2724
|
+
}),
|
|
2725
|
+
nextStep: tr('failureReason_reviewing_unavailable_cancelled_next') });
|
|
2726
|
+
} else {
|
|
2727
|
+
runSettlementPersistence.applyToCurrentRun(error.writebackOutcome?.settlement);
|
|
2728
|
+
appendRunCancelledReport(error.writebackOutcome);
|
|
2729
|
+
}
|
|
2544
2730
|
await finishRunView(tx('Cancelled', '已取消'), 'cancelled');
|
|
2545
2731
|
void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
|
|
2546
2732
|
return;
|
|
2547
2733
|
}
|
|
2734
|
+
if (compact) {
|
|
2735
|
+
const key = error.code === 'codex_compact_unsupported' ? 'compactUnsupported'
|
|
2736
|
+
: ['project_locked', 'codex_compact_thread_busy'].includes(error.code) ? 'compactBusy' : 'compactFailed';
|
|
2737
|
+
const contextLimit = FailureReasons.isContextLimitError(error.message, error.code)
|
|
2738
|
+
? translateRawError(error.message, { locale: getLocale(), code: error.code }) : null;
|
|
2739
|
+
const failure = contextLimit ? buildContentFailure('codex_context_limit', { path: 'codex.run' }, {
|
|
2740
|
+
technicalMessage: error.message, evidence: { errorCode: error.code || '' }
|
|
2741
|
+
}) : undefined;
|
|
2742
|
+
const conclusion = contextLimit?.conclusion || tr(key);
|
|
2743
|
+
appendRunEvent({ title: conclusion, status: 'failed', failure,
|
|
2744
|
+
technicalDetail: { code: error.code || '', message: error.message } });
|
|
2745
|
+
appendCompletionReport({ conclusion, conclusionKey: contextLimit?.conclusionKey || (key === 'compactUnsupported'
|
|
2746
|
+
? 'compact_unsupported' : key === 'compactBusy' ? 'compact_busy' : 'compact_failed'),
|
|
2747
|
+
nextStep: contextLimit?.nextStep, failure,
|
|
2748
|
+
status: 'failed', mode: 'ask', operations: [], applyResults: [] });
|
|
2749
|
+
await finishRunView(conclusion, 'failed');
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2548
2752
|
// codexReturned: assistantMessage arrived on the stream before the
|
|
2549
2753
|
// exception escaped, so Codex's answer is preserved in the conversation
|
|
2550
2754
|
// — the fallback conclusion must reflect that instead of saying
|
|
2551
2755
|
// 'local Codex returned no usable result'.
|
|
2552
2756
|
const codexReturned = Boolean(getAssistantAnswerForCurrentRun());
|
|
2553
|
-
const translated = translateRawError(error.message, { mode: submittedMode, locale: getLocale(), codexReturned });
|
|
2757
|
+
const translated = translateRawError(error.message, { mode: submittedMode, locale: getLocale(), code: error.code, codexReturned });
|
|
2554
2758
|
// When translateRawError maps the raw error to a FailureReason code,
|
|
2555
2759
|
// attach the structured failure to the run-event and completion-report
|
|
2556
2760
|
// so the run record carries machine-readable failure data alongside
|
|
@@ -2578,7 +2782,7 @@
|
|
|
2578
2782
|
}
|
|
2579
2783
|
});
|
|
2580
2784
|
appendCompletionReport({
|
|
2581
|
-
conclusion: translated.conclusion,
|
|
2785
|
+
conclusion: translated.conclusion, conclusionKey: translated.conclusionKey,
|
|
2582
2786
|
status: 'failed',
|
|
2583
2787
|
operations: [],
|
|
2584
2788
|
applyResults: [],
|
|
@@ -2592,6 +2796,10 @@
|
|
|
2592
2796
|
blockedFiles: [{ path: 'task', reason: error.message }]
|
|
2593
2797
|
});
|
|
2594
2798
|
await finishRunView(tx('Task failed', '任务失败'), 'failed');
|
|
2799
|
+
// A failed initial card render must still leave a visible terminal record.
|
|
2800
|
+
if (currentRunView?.sessionId === state.activeSessionId && !getCurrentRunViewForRender()) {
|
|
2801
|
+
renderRunHistory({ preserveScroll: true });
|
|
2802
|
+
}
|
|
2595
2803
|
} finally {
|
|
2596
2804
|
const settledView = currentRunView;
|
|
2597
2805
|
const settledRecord = settledView ? findRunRecord(settledView.recordId, settledView.sessionId) : null;
|
|
@@ -2606,9 +2814,15 @@
|
|
|
2606
2814
|
nativeChannel.clearActiveRequest();
|
|
2607
2815
|
stopRunElapsedTick();
|
|
2608
2816
|
await flushQueuedSaveState().catch(() => {});
|
|
2817
|
+
// The card is already settled. Retain the owner while Native stops so
|
|
2818
|
+
// neither ordinary promotion nor a deferred session start races its lock.
|
|
2819
|
+
if (settlement.continueQueue) {
|
|
2820
|
+
settlement.continueQueue = await waitForCancelledNativeRun(settledView);
|
|
2821
|
+
}
|
|
2609
2822
|
currentRunView = null;
|
|
2610
2823
|
runCancellationRequested = false;
|
|
2611
2824
|
runCancellationController = null;
|
|
2825
|
+
syncComposerSendAvailability();
|
|
2612
2826
|
activeTurnControl.release(settlement.requestId);
|
|
2613
2827
|
if (isExperimentalOtEnabled()) {
|
|
2614
2828
|
try { await resumeOtWarmMirror('run-settled'); }
|
|
@@ -2647,6 +2861,7 @@
|
|
|
2647
2861
|
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
2648
2862
|
speedTier: executionSnapshot.speedTier,
|
|
2649
2863
|
session: currentRunView?.sessionContext,
|
|
2864
|
+
locale: getLocale(),
|
|
2650
2865
|
clientRunId: currentRunView?.recordId || '',
|
|
2651
2866
|
clientSessionId: runSessionId,
|
|
2652
2867
|
accountScopeId: currentRunView?.runAccountScopeId || '',
|
|
@@ -2688,7 +2903,7 @@
|
|
|
2688
2903
|
failure: translatedFailure
|
|
2689
2904
|
});
|
|
2690
2905
|
appendCompletionReport({
|
|
2691
|
-
conclusion: translated.conclusion,
|
|
2906
|
+
conclusion: translated.conclusion, conclusionKey: translated.conclusionKey,
|
|
2692
2907
|
status: 'failed',
|
|
2693
2908
|
operations: [],
|
|
2694
2909
|
applyResults: [],
|
|
@@ -2706,7 +2921,7 @@
|
|
|
2706
2921
|
}
|
|
2707
2922
|
|
|
2708
2923
|
const assistantMessage = response.result?.assistantMessage || getAssistantAnswerForCurrentRun();
|
|
2709
|
-
appendCompletionReport({
|
|
2924
|
+
appendCompletionReport({ conclusionKey: !assistantMessage ? 'skill_installer_completed' : undefined,
|
|
2710
2925
|
conclusion: assistantMessage || tx('Skill installer completed.', 'Skill installer 已完成。'),
|
|
2711
2926
|
status: 'completed',
|
|
2712
2927
|
operations: [],
|
|
@@ -2740,11 +2955,14 @@
|
|
|
2740
2955
|
if (returnedThreadId && returnedThreadId !== codexThreadId) {
|
|
2741
2956
|
updateSessionById(runSessionId, { codexThreadId: returnedThreadId });
|
|
2742
2957
|
const StorageDb = Modules.StorageDb;
|
|
2743
|
-
|
|
2744
|
-
|
|
2958
|
+
// The installer already finished; a later envelope conflict only delays this save.
|
|
2959
|
+
if (findSessionById(runSessionId)) {
|
|
2960
|
+
saveStateSoon();
|
|
2961
|
+
} else if (StorageDb) {
|
|
2962
|
+
const record = await StorageDb.getRecord('sessions', runSessionId, { adopt: false });
|
|
2745
2963
|
if (record) {
|
|
2746
2964
|
record.codexThreadId = returnedThreadId;
|
|
2747
|
-
await StorageDb.putRecord('sessions', record);
|
|
2965
|
+
await StorageDb.putRecord('sessions', record, { adopt: false });
|
|
2748
2966
|
}
|
|
2749
2967
|
}
|
|
2750
2968
|
}
|
|
@@ -2758,8 +2976,6 @@
|
|
|
2758
2976
|
if (mode === 'ask') {
|
|
2759
2977
|
return { ok: true, skipped: true };
|
|
2760
2978
|
}
|
|
2761
|
-
const method = requireReviewing ? 'ensureReviewing' : 'ensureEditing';
|
|
2762
|
-
|
|
2763
2979
|
appendRunEvent({
|
|
2764
2980
|
title: requireReviewing
|
|
2765
2981
|
? tx('Checking Overleaf Reviewing/Track Changes before starting.', '正在确认 Overleaf 留痕状态。')
|
|
@@ -2767,30 +2983,67 @@
|
|
|
2767
2983
|
status: 'running'
|
|
2768
2984
|
});
|
|
2769
2985
|
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
}
|
|
2986
|
+
const owner = currentRunView;
|
|
2987
|
+
const signal = runCancellationController?.signal;
|
|
2988
|
+
const isCurrent = () => owner === currentRunView && !signal?.aborted && !runCancellationRequested
|
|
2989
|
+
&& owner?.runProjectId === getCurrentProjectId() && owner?.runAccountScopeId === cachedAccountScopeId;
|
|
2990
|
+
const assertCurrent = () => { if (!isCurrent()) throw createRunCancellationError(); };
|
|
2991
|
+
assertCurrent();
|
|
2992
|
+
if (requireReviewing) {
|
|
2993
|
+
// Another tab may have observed this project's capability since hydration.
|
|
2994
|
+
const prefs = await Modules.StorageMigration.loadPrefs(owner.runAccountScopeId, owner.runProjectId);
|
|
2995
|
+
assertCurrent();
|
|
2996
|
+
const incoming = prefs?.reviewingAvailabilityByProject?.[owner.runProjectId];
|
|
2997
|
+
const current = projectSettingsCoordinator.getReviewingAvailability();
|
|
2998
|
+
if (incoming && (!current || incoming.observedAt > current.observedAt
|
|
2999
|
+
|| (incoming.observedAt === current.observedAt && incoming.status === 'available'))) {
|
|
3000
|
+
state = { ...state, reviewingAvailabilityByProject: {
|
|
3001
|
+
...state.reviewingAvailabilityByProject, [owner.runProjectId]: incoming
|
|
3002
|
+
} };
|
|
3003
|
+
syncProbeStatus();
|
|
3004
|
+
}
|
|
2789
3005
|
}
|
|
3006
|
+
let result = await Modules.Reviewing.resolveWritePolicy({ requireReviewing,
|
|
3007
|
+
knownUnavailable: isReviewingKnownUnavailable() }, {
|
|
3008
|
+
assertCurrent,
|
|
3009
|
+
ensureMode: async required => {
|
|
3010
|
+
try {
|
|
3011
|
+
return await callPageBridge(required ? 'ensureReviewing' : 'ensureEditing', {
|
|
3012
|
+
waitMs: 1800, runProjectId: owner.runProjectId
|
|
3013
|
+
});
|
|
3014
|
+
} catch (error) {
|
|
3015
|
+
assertCurrent();
|
|
3016
|
+
return { ok: false, code: error?.code, reason: error?.message, reviewing: null };
|
|
3017
|
+
}
|
|
3018
|
+
},
|
|
3019
|
+
observe: async (status, evidence) => {
|
|
3020
|
+
projectSettingsCoordinator.observeReviewingAvailability(status, evidence);
|
|
3021
|
+
syncProbeStatus();
|
|
3022
|
+
// Publish a detected paywall before waiting for the user's decision.
|
|
3023
|
+
if (status === 'unavailable') await flushQueuedSaveState({ requireDurable: true });
|
|
3024
|
+
assertCurrent();
|
|
3025
|
+
},
|
|
3026
|
+
choose: () => {
|
|
3027
|
+
owner.reviewingUnavailablePreflight = true;
|
|
3028
|
+
return showPluginConfirm({
|
|
3029
|
+
title: tr('reviewingUnavailableTitle'),
|
|
3030
|
+
message: tr('reviewingUnavailableChoiceMessage'),
|
|
3031
|
+
choices: [
|
|
3032
|
+
{ value: 'continue', label: tr('reviewingUnavailableContinue') },
|
|
3033
|
+
{ value: 'disable', label: tr('reviewingUnavailableDisable'), secondary: true },
|
|
3034
|
+
{ value: 'cancel', label: tr('confirmDefaultCancel') }
|
|
3035
|
+
],
|
|
3036
|
+
signal, isCurrent, timeoutMs: REVIEWING_CONFIRM_TIMEOUT_MS
|
|
3037
|
+
});
|
|
3038
|
+
},
|
|
3039
|
+
applyUntracked: choice => { assertCurrent(); return options.onUntracked(choice); }
|
|
3040
|
+
});
|
|
3041
|
+
if (result.ok) owner.reviewingUnavailablePreflight = false;
|
|
3042
|
+
const effectiveRequireReviewing = result.requireReviewing ?? requireReviewing;
|
|
2790
3043
|
|
|
2791
3044
|
if (result?.ok) {
|
|
2792
3045
|
appendRunEvent({
|
|
2793
|
-
title:
|
|
3046
|
+
title: effectiveRequireReviewing
|
|
2794
3047
|
? (result.activated
|
|
2795
3048
|
? tx('Overleaf Reviewing/Track Changes is now on. Starting the task.', '已开启 Overleaf 留痕,开始处理任务。')
|
|
2796
3049
|
: tx('Overleaf Reviewing/Track Changes is already on. Starting the task.', 'Overleaf 留痕已经开启,开始处理任务。'))
|
|
@@ -2802,10 +3055,23 @@
|
|
|
2802
3055
|
return result;
|
|
2803
3056
|
}
|
|
2804
3057
|
|
|
2805
|
-
const
|
|
3058
|
+
const unavailable = result?.code === 'reviewing_unavailable';
|
|
3059
|
+
if (unavailable && !result.failure) result.failure = buildContentFailure('reviewing_unavailable', {}, {
|
|
3060
|
+
changedDocument: false, evidence: result.evidence
|
|
3061
|
+
});
|
|
3062
|
+
const reason = unavailable ? tr('failureReason_reviewing_unavailable_user') : localizeVisibleReason(result?.reason || (effectiveRequireReviewing
|
|
2806
3063
|
? tx('Overleaf did not return track changes status', 'Overleaf 没有返回留痕状态')
|
|
2807
3064
|
: tx('Overleaf did not return Editing mode status', 'Overleaf 没有返回 Editing 模式状态')));
|
|
2808
|
-
|
|
3065
|
+
// Affirmative choices retain trackingDecision even if the Editing check fails.
|
|
3066
|
+
const declinedReviewingChoice = owner.reviewingUnavailablePreflight === true && !result.trackingDecision;
|
|
3067
|
+
const reportFailure = unavailable && declinedReviewingChoice
|
|
3068
|
+
? buildContentFailure('reviewing_unavailable', {}, {
|
|
3069
|
+
...result.failure, changedDocument: false,
|
|
3070
|
+
evidence: { ...result.failure?.evidence, declinedChoice: true }
|
|
3071
|
+
}) : result?.failure;
|
|
3072
|
+
const nextStep = unavailable ? tr(declinedReviewingChoice
|
|
3073
|
+
? 'failureReason_reviewing_unavailable_cancelled_next'
|
|
3074
|
+
: 'failureReason_reviewing_unavailable_next') : effectiveRequireReviewing
|
|
2809
3075
|
? tx(
|
|
2810
3076
|
'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
3077
|
'你可能没有权限,或 Overleaf 当前页面没有暴露切换入口。可以手动切到 Reviewing 后重试,或关闭“留痕”再运行。'
|
|
@@ -2815,7 +3081,7 @@
|
|
|
2815
3081
|
'你可能没有权限,或 Overleaf 当前页面没有暴露切换入口。请在 Overleaf 手动切到 Editing 后重试。'
|
|
2816
3082
|
);
|
|
2817
3083
|
appendRunEvent({
|
|
2818
|
-
title:
|
|
3084
|
+
title: effectiveRequireReviewing
|
|
2819
3085
|
? tx('Task not started: could not enable Overleaf Reviewing/Track Changes.', '任务未开始:无法开启 Overleaf 留痕。')
|
|
2820
3086
|
: tx('Task not started: could not switch to Overleaf Editing.', '任务未开始:无法切换到 Overleaf Editing。'),
|
|
2821
3087
|
status: 'failed',
|
|
@@ -2824,24 +3090,25 @@
|
|
|
2824
3090
|
[tr('detailNext')]: nextStep
|
|
2825
3091
|
},
|
|
2826
3092
|
technicalDetail: {
|
|
2827
|
-
reason,
|
|
3093
|
+
reason, code: result?.code, evidence: result?.evidence, failure: result?.failure,
|
|
2828
3094
|
reviewing: result?.reviewing || null
|
|
2829
3095
|
}
|
|
2830
3096
|
});
|
|
2831
|
-
appendCompletionReport({
|
|
3097
|
+
appendCompletionReport({ conclusionKey: 'launch_not_started',
|
|
2832
3098
|
conclusion: tx('This task did not start: Codex did not run and no files were written.', '这轮任务没有开始:Codex 没有运行,也没有写入文件。'),
|
|
2833
3099
|
status: 'blocked',
|
|
2834
3100
|
operations: [],
|
|
2835
3101
|
applyResults: [],
|
|
2836
|
-
nextStep
|
|
3102
|
+
nextStep,
|
|
3103
|
+
reason: result?.code, failure: reportFailure
|
|
2837
3104
|
});
|
|
2838
|
-
const finishTitle =
|
|
3105
|
+
const finishTitle = effectiveRequireReviewing
|
|
2839
3106
|
? tx('Not started: could not enable Track Changes', '未开始:无法开启留痕')
|
|
2840
3107
|
: tx('Not started: could not switch to Editing', '未开始:无法切换到 Editing');
|
|
2841
3108
|
await finishRunView(finishTitle, 'failed');
|
|
2842
3109
|
return {
|
|
2843
|
-
ok: false,
|
|
2844
|
-
reason,
|
|
3110
|
+
...result, ok: false,
|
|
3111
|
+
reason: result?.code || reason,
|
|
2845
3112
|
reviewing: result?.reviewing || null
|
|
2846
3113
|
};
|
|
2847
3114
|
}
|
|
@@ -2854,17 +3121,25 @@
|
|
|
2854
3121
|
return runController.truncateSessionHistoryText(value, maxLength);
|
|
2855
3122
|
}
|
|
2856
3123
|
|
|
2857
|
-
function safeRunTask() {
|
|
3124
|
+
function safeRunTask(options = {}) {
|
|
3125
|
+
if (sessionManager.isTransitioning()) return;
|
|
2858
3126
|
if (updateIdleClient?.isApplying()) {
|
|
2859
3127
|
showPluginToast(tx('An update is being applied. Codex will be available again after reload.', '正在应用更新,重新加载后即可继续使用 Codex。'), { status: 'info' });
|
|
2860
3128
|
return;
|
|
2861
3129
|
}
|
|
3130
|
+
const submittedCompactCommand = options.operation !== 'compact'
|
|
3131
|
+
&& panel?.querySelector('[data-task]')?.value.trim() === '/compact';
|
|
3132
|
+
const compact = options.operation === 'compact' || submittedCompactCommand;
|
|
3133
|
+
if (compact && (currentRunView || runSubmissionPending)) {
|
|
3134
|
+
showPluginToast(tr('compactBusy'), { status: 'info' });
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
2862
3137
|
if (currentRunView) {
|
|
2863
3138
|
return queueComposerInput();
|
|
2864
3139
|
}
|
|
2865
3140
|
if (runSubmissionPending) return;
|
|
2866
3141
|
runSubmissionPending = true;
|
|
2867
|
-
runTask().catch(error => {
|
|
3142
|
+
runTask(compact ? { operation: 'compact', consumeCompactDraft: options.consumeCompactDraft === true || submittedCompactCommand } : {}).catch(error => {
|
|
2868
3143
|
if (!currentRunView) {
|
|
2869
3144
|
setRunning(false);
|
|
2870
3145
|
nativeChannel.clearActiveRequest();
|
|
@@ -2896,7 +3171,7 @@
|
|
|
2896
3171
|
getActiveSession: () => getActiveSession(state),
|
|
2897
3172
|
setQueue: (sessionId, pendingInputs) => updateSessionById(sessionId, { pendingInputs }),
|
|
2898
3173
|
getCurrentRun: () => currentRunView,
|
|
2899
|
-
canStart:
|
|
3174
|
+
canStart: canStartQueuedSession,
|
|
2900
3175
|
readInputs: readPanelInputs,
|
|
2901
3176
|
getTask: () => panel?.querySelector('[data-task]')?.value || '',
|
|
2902
3177
|
hasAttachments: () => getComposerAttachmentsForRun().length > 0,
|
|
@@ -2916,6 +3191,7 @@
|
|
|
2916
3191
|
}),
|
|
2917
3192
|
clearTask: () => {
|
|
2918
3193
|
panel.querySelector('[data-task]').value = '';
|
|
3194
|
+
composerDraftEditedHere = false;
|
|
2919
3195
|
selectionContextView.clear();
|
|
2920
3196
|
updateSessionById(state.activeSessionId, { task: '' });
|
|
2921
3197
|
autosizeTaskTextarea();
|
|
@@ -2926,6 +3202,8 @@
|
|
|
2926
3202
|
RunExecutionSnapshot.fromQueuePayload(item.payload),
|
|
2927
3203
|
{ catalog: providerSettingsCoordinator.getCatalog() }
|
|
2928
3204
|
);
|
|
3205
|
+
// Promotion uses its captured snapshot; this line still describes the current draft.
|
|
3206
|
+
syncProbeStatus();
|
|
2929
3207
|
},
|
|
2930
3208
|
run: item => runTask({ queuedInput: item }),
|
|
2931
3209
|
getGuideScope: () => ({ projectId: getCurrentProjectId(), accountScopeId: cachedAccountScopeId, generation: spaRouteGeneration }),
|
|
@@ -2978,7 +3256,17 @@
|
|
|
2978
3256
|
});
|
|
2979
3257
|
}
|
|
2980
3258
|
|
|
3259
|
+
function canStartQueuedSession(sessionId) {
|
|
3260
|
+
if (state?.activeSessionId !== sessionId || !isProjectEditorRoute(window.location)) return false;
|
|
3261
|
+
if (sessionManager.isTransitioning()) {
|
|
3262
|
+
deferredSessionStart = { sessionId, scope: getComposerDraftScope() };
|
|
3263
|
+
return 'session_transition';
|
|
3264
|
+
}
|
|
3265
|
+
return true;
|
|
3266
|
+
}
|
|
3267
|
+
|
|
2981
3268
|
function queueComposerInput() {
|
|
3269
|
+
if (sessionManager.isTransitioning()) return;
|
|
2982
3270
|
if (getSkillLoadingSettings().loadCodexLocalSkills !== false && !Array.isArray(getCodexLocalSkillsCatalog())) {
|
|
2983
3271
|
primeCodexLocalSkillsCatalog({ required: true }).catch(() => {});
|
|
2984
3272
|
showPluginToast(tx('Local Codex skills are still loading. Try submitting again when loading finishes.',
|
|
@@ -3018,18 +3306,21 @@
|
|
|
3018
3306
|
// codex_cancelled instead of grinding through the rest of the queue.
|
|
3019
3307
|
// Without the page-side signal, a cancel during writeback waits up to
|
|
3020
3308
|
// the applyOperations content-side timeout (30s) before the run settles.
|
|
3021
|
-
const activeRequestId =
|
|
3309
|
+
const activeRequestId = currentRunView.nativeRequestId;
|
|
3022
3310
|
const projectKey = currentRunView?.runProjectId || getCurrentProjectId() || '';
|
|
3023
3311
|
|
|
3024
3312
|
const cancelTargets = [];
|
|
3025
|
-
if (activeRequestId
|
|
3313
|
+
if (activeRequestId != null && activeRequestId !== '') {
|
|
3026
3314
|
cancelTargets.push(sendBackgroundNative({
|
|
3027
3315
|
method: 'codex.cancel',
|
|
3028
3316
|
params: {
|
|
3029
|
-
requestId: activeRequestId
|
|
3317
|
+
requestId: activeRequestId,
|
|
3030
3318
|
projectKey: projectKey || undefined
|
|
3031
3319
|
}
|
|
3032
3320
|
}));
|
|
3321
|
+
} else {
|
|
3322
|
+
// Preparation has no Native run to cancel; the local signal prevents dispatch.
|
|
3323
|
+
cancelTargets.push(Promise.resolve(null));
|
|
3033
3324
|
}
|
|
3034
3325
|
// The page-side cancel is best-effort — it only matters if a writeback
|
|
3035
3326
|
// is currently in flight. Swallow errors / unavailable bridge so the
|
|
@@ -3116,6 +3407,23 @@
|
|
|
3116
3407
|
});
|
|
3117
3408
|
}
|
|
3118
3409
|
|
|
3410
|
+
async function waitForCancelledNativeRun(runView) {
|
|
3411
|
+
const pending = runView?.pendingNativeRun;
|
|
3412
|
+
if (!pending) return true;
|
|
3413
|
+
const CANCELLED_NATIVE_SETTLEMENT_TIMEOUT_MS = 20000;
|
|
3414
|
+
let timer;
|
|
3415
|
+
try {
|
|
3416
|
+
return await Promise.race([
|
|
3417
|
+
pending.then(() => true, () => true),
|
|
3418
|
+
new Promise(resolve => {
|
|
3419
|
+
timer = setTimeout(() => resolve(false), CANCELLED_NATIVE_SETTLEMENT_TIMEOUT_MS);
|
|
3420
|
+
})
|
|
3421
|
+
]);
|
|
3422
|
+
} finally {
|
|
3423
|
+
clearTimeout(timer);
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3119
3427
|
function isRunCancellationError(error = {}) {
|
|
3120
3428
|
return error.code === 'codex_cancelled' || /cancelled by the user|was cancelled/i.test(error.message || '');
|
|
3121
3429
|
}
|
|
@@ -3218,6 +3526,7 @@
|
|
|
3218
3526
|
const params = runController.buildCodexRunParams({
|
|
3219
3527
|
currentProjectId: getCurrentProjectId(),
|
|
3220
3528
|
state: submittedState,
|
|
3529
|
+
locale: getLocale(),
|
|
3221
3530
|
task,
|
|
3222
3531
|
project,
|
|
3223
3532
|
useExistingMirror,
|
|
@@ -3271,25 +3580,33 @@
|
|
|
3271
3580
|
if (!compatibilityGate.ok) {
|
|
3272
3581
|
return compatibilityGate.response;
|
|
3273
3582
|
}
|
|
3583
|
+
if (params.operation === 'compact' && compatibilityGate.compatibility?.native?.capabilities?.threadCompact !== true) {
|
|
3584
|
+
return { ok: false, error: { code: 'codex_compact_unsupported', message: 'This Native host does not support context compaction.' } };
|
|
3585
|
+
}
|
|
3274
3586
|
const tracked = nativeChannel.sendNativeTracked(nativeCompatibilityController.attachEvidence({
|
|
3275
3587
|
method: 'codex.run',
|
|
3276
3588
|
params
|
|
3277
3589
|
}, compatibilityGate.compatibility));
|
|
3278
|
-
if (
|
|
3279
|
-
|
|
3280
|
-
|
|
3590
|
+
if (owner) {
|
|
3591
|
+
owner.nativeRequestId = tracked.id;
|
|
3592
|
+
// Keep the actual response separate from awaitRunStep's local abort race.
|
|
3593
|
+
owner.pendingNativeRun = tracked.promise;
|
|
3594
|
+
const record = findRunRecord(owner.recordId, owner.sessionId);
|
|
3281
3595
|
if (record) {
|
|
3282
3596
|
record.nativeRequestId = tracked.id;
|
|
3283
3597
|
}
|
|
3284
3598
|
activeTurnControl.bind({
|
|
3285
3599
|
requestId: tracked.id,
|
|
3286
|
-
projectKey:
|
|
3287
|
-
clientRunId:
|
|
3288
|
-
sessionId:
|
|
3600
|
+
projectKey: owner.runProjectId,
|
|
3601
|
+
clientRunId: owner.recordId,
|
|
3602
|
+
sessionId: owner.sessionId
|
|
3289
3603
|
});
|
|
3290
3604
|
saveStateSoon();
|
|
3291
3605
|
}
|
|
3292
3606
|
return tracked.promise.finally(() => {
|
|
3607
|
+
if (owner?.pendingNativeRun === tracked.promise) {
|
|
3608
|
+
owner.pendingNativeRun = null;
|
|
3609
|
+
}
|
|
3293
3610
|
if (currentRunView?.nativeRequestId === tracked.id) {
|
|
3294
3611
|
currentRunView.activeTurn = null;
|
|
3295
3612
|
renderPendingInputs();
|
|
@@ -3298,58 +3615,44 @@
|
|
|
3298
3615
|
}
|
|
3299
3616
|
|
|
3300
3617
|
async function recoverInterruptedRunJournals() {
|
|
3301
|
-
const projectKey = getCurrentProjectId();
|
|
3618
|
+
const projectKey = getCurrentProjectId(), accountScopeId = cachedAccountScopeId;
|
|
3619
|
+
const generation = spaRouteGeneration;
|
|
3620
|
+
const isCurrent = () => generation === spaRouteGeneration
|
|
3621
|
+
&& projectKey === getCurrentProjectId() && accountScopeId === cachedAccountScopeId;
|
|
3302
3622
|
const recovery = await activeTurnControl.recoverJournals({
|
|
3303
3623
|
projectKey,
|
|
3304
|
-
accountScopeId
|
|
3624
|
+
accountScopeId,
|
|
3625
|
+
isCurrent,
|
|
3626
|
+
onRecheck: async () => {
|
|
3627
|
+
if (!isCurrent()) return;
|
|
3628
|
+
await recoverInterruptedRunJournals();
|
|
3629
|
+
if (isCurrent()) applyStateToPanel();
|
|
3630
|
+
},
|
|
3305
3631
|
requireOwnerLost: Modules.StorageDb.sharedSessionsEnabled?.() === true,
|
|
3306
3632
|
normalizeInterruptedRun: record => Object.assign(record, Modules.SessionState.normalizeRuns([record], {
|
|
3307
3633
|
restoreRunningRuns: true,
|
|
3308
3634
|
locale: getLocale()
|
|
3309
3635
|
})[0]),
|
|
3310
3636
|
findSession: findSessionById,
|
|
3637
|
+
listSessions: () => state?.sessions || [],
|
|
3638
|
+
isLiveRun: (record, session) => currentRunView?.recordId === record.id
|
|
3639
|
+
&& currentRunView.sessionId === session.id && currentRunView.runProjectId === projectKey,
|
|
3311
3640
|
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: []
|
|
3641
|
+
createInterruptedRun: journal => ActiveTurnControl.createInterruptedRun(journal, projectKey, tr('restoredRunStoppedStatus')),
|
|
3642
|
+
onRawEvent: (raw, { record, session, sequence }) => storeContextUsage(raw, record, session, sequence),
|
|
3643
|
+
mapEvent: (raw, journal) => ActiveTurnControl.mapRecoveredEvent(raw, journal, {
|
|
3644
|
+
mapActivity: value => mapAgentEventToActivity(value, { locale: getLocale() }),
|
|
3645
|
+
sanitizeText: sanitizeAssistantVisibleText, sanitizeValue: sanitizeAssistantVisibleValue
|
|
3327
3646
|
}),
|
|
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
3647
|
upsertStream: upsertRunStreamRecordEvent,
|
|
3346
3648
|
pauseSessionQueue: (session, reason) => {
|
|
3347
3649
|
session.pendingInputs = RunInputQueue.pauseAll(session.pendingInputs || [], reason);
|
|
3348
3650
|
},
|
|
3349
3651
|
maxEvents: MAX_RUN_EVENTS
|
|
3350
3652
|
});
|
|
3653
|
+
if (!isCurrent()) return;
|
|
3351
3654
|
if (recovery.changed) {
|
|
3352
|
-
if (await saveState({ recoveryRuns: recovery.recoveredRuns }) !== true) return;
|
|
3655
|
+
if (await saveState({ recoveryRuns: recovery.recoveredRuns }) !== true || !isCurrent()) return;
|
|
3353
3656
|
for (const requestId of recovery.acknowledgeIds) {
|
|
3354
3657
|
await activeTurnControl.acknowledge(requestId).catch(() => false);
|
|
3355
3658
|
}
|
|
@@ -3375,6 +3678,7 @@
|
|
|
3375
3678
|
sessionId: input.sessionId || '',
|
|
3376
3679
|
turnId: currentRunView?.recordId || '',
|
|
3377
3680
|
mode: input.mode || executionSnapshot.mode,
|
|
3681
|
+
requireReviewing: executionSnapshot.requireReviewing,
|
|
3378
3682
|
...providerSettingsCoordinator.getProviderSnapshot(executionSnapshot.providerId),
|
|
3379
3683
|
providerRevision: executionSnapshot.providerRevision,
|
|
3380
3684
|
model: executionSnapshot.model,
|
|
@@ -3445,19 +3749,20 @@
|
|
|
3445
3749
|
}).run(options);
|
|
3446
3750
|
}
|
|
3447
3751
|
|
|
3448
|
-
function appendRunCancelledReport() {
|
|
3449
|
-
const hasQueuedFollowUp = (getActiveSession(state)?.pendingInputs || [])
|
|
3752
|
+
function appendRunCancelledReport(writebackOutcome = {}, continueQueue = true) {
|
|
3753
|
+
const hasQueuedFollowUp = continueQueue && (getActiveSession(state)?.pendingInputs || [])
|
|
3450
3754
|
.some(item => item?.status === 'queued');
|
|
3451
3755
|
appendRunEvent({
|
|
3452
3756
|
title: tx('Current Codex task was cancelled.', '已中断当前 Codex 任务。'),
|
|
3453
3757
|
status: 'info',
|
|
3454
3758
|
activity: { v: 1, kind: 'lifecycle', state: 'cancelled', target: 'cancelled' }
|
|
3455
3759
|
});
|
|
3456
|
-
appendCompletionReport({
|
|
3760
|
+
appendCompletionReport({ conclusionKey: 'cancelled',
|
|
3457
3761
|
conclusion: tx('This run was cancelled. It did not continue syncing or writing to Overleaf.', '这轮已中断,没有继续同步或写入 Overleaf。'),
|
|
3458
3762
|
status: 'cancelled',
|
|
3459
|
-
operations: [],
|
|
3460
|
-
applyResults: [],
|
|
3763
|
+
operations: writebackOutcome.operations || [],
|
|
3764
|
+
applyResults: writebackOutcome.applied ? [writebackOutcome.applied] : [],
|
|
3765
|
+
saveVerification: writebackOutcome.saveVerification,
|
|
3461
3766
|
nextStep: hasQueuedFollowUp
|
|
3462
3767
|
? tx('The first queued follow-up will start next.', '接下来会自动启动队首的后续任务。')
|
|
3463
3768
|
: tx('Edit the task and run again.', '可以修改任务后重新运行。')
|
|
@@ -3482,22 +3787,36 @@
|
|
|
3482
3787
|
const running = typeof runningOverride === 'boolean'
|
|
3483
3788
|
? runningOverride
|
|
3484
3789
|
: Boolean(currentRunView);
|
|
3485
|
-
const
|
|
3790
|
+
const runningInActiveSession = running && (!currentRunView || currentRunView.sessionId === state?.activeSessionId);
|
|
3791
|
+
const transitioning = sessionManager.isTransitioning();
|
|
3792
|
+
const action = runningInActiveSession && (!hasInput || transitioning) ? 'cancel' : 'send';
|
|
3793
|
+
const attachmentsLoading = composerAttachmentController.hasPendingReads();
|
|
3486
3794
|
const label = action === 'cancel'
|
|
3487
3795
|
? tr('cancelRun')
|
|
3488
|
-
:
|
|
3489
|
-
? tr('
|
|
3490
|
-
:
|
|
3796
|
+
: attachmentsLoading
|
|
3797
|
+
? tr('attachmentsLoading')
|
|
3798
|
+
: running && hasInput
|
|
3799
|
+
? tr('queueNextInput')
|
|
3800
|
+
: tr('send');
|
|
3491
3801
|
runButton.dataset.action = action;
|
|
3492
3802
|
runButton.disabled = action === 'cancel'
|
|
3493
3803
|
? runCancellationRequested
|
|
3494
|
-
: !hasInput;
|
|
3804
|
+
: !hasInput || transitioning || attachmentsLoading;
|
|
3495
3805
|
runButton.title = label;
|
|
3496
3806
|
runButton.setAttribute('aria-label', label);
|
|
3497
|
-
if (currentRunView?.writebackRetry && hasInput) runButton.disabled = true;
|
|
3807
|
+
if (currentRunView?.writebackRetry && hasInput && action !== 'cancel') runButton.disabled = true;
|
|
3498
3808
|
}
|
|
3499
3809
|
|
|
3500
3810
|
function handleTaskInputKeydown(event) {
|
|
3811
|
+
if (sessionManager.isTransitioning()) {
|
|
3812
|
+
if (event.key === 'Enter' && !event.isComposing && event.keyCode !== 229) event.preventDefault();
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
if (composerAttachmentController.hasPendingReads() && event.key === 'Enter'
|
|
3816
|
+
&& !event.shiftKey && !event.isComposing && event.keyCode !== 229) {
|
|
3817
|
+
event.preventDefault();
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3501
3820
|
if (handleSlashMenuKeydown(event)) {
|
|
3502
3821
|
return;
|
|
3503
3822
|
}
|
|
@@ -3525,9 +3844,12 @@
|
|
|
3525
3844
|
|
|
3526
3845
|
function getSlashCommands() {
|
|
3527
3846
|
const commands = [
|
|
3847
|
+
{ id: 'compact', kind: 'compact', group: 'commands', title: tr('slashCompactTitle'), slug: '/compact',
|
|
3848
|
+
subtitle: tr('slashCompactSubtitle') },
|
|
3528
3849
|
{
|
|
3529
3850
|
id: 'install-skill',
|
|
3530
3851
|
kind: 'installer',
|
|
3852
|
+
group: 'commands',
|
|
3531
3853
|
title: tr('slashInstallSkillTitle'),
|
|
3532
3854
|
subtitle: tr('slashInstallSkillSubtitle')
|
|
3533
3855
|
}
|
|
@@ -3542,10 +3864,12 @@
|
|
|
3542
3864
|
if (!isCodexOverleafSkillEnabled(id)) {
|
|
3543
3865
|
continue;
|
|
3544
3866
|
}
|
|
3545
|
-
const title =
|
|
3867
|
+
const title = id === 'reviewer-response' ? tx('Reviewer response', '审稿回复')
|
|
3868
|
+
: String(skill?.title || skill?.name || id).trim().slice(0, 80) || id;
|
|
3546
3869
|
commands.push({
|
|
3547
3870
|
id: `skill:${id}`,
|
|
3548
3871
|
kind: 'codex-overleaf-skill',
|
|
3872
|
+
group: 'overleaf',
|
|
3549
3873
|
scope: 'codex-overleaf',
|
|
3550
3874
|
skillId: id,
|
|
3551
3875
|
title,
|
|
@@ -3556,11 +3880,13 @@
|
|
|
3556
3880
|
for (const skill of localSkills) {
|
|
3557
3881
|
if (!isSafeSkillId(skill.id) || !isCodexLocalSkillEnabled(skill.id, skill)) continue;
|
|
3558
3882
|
const sameName = localSkills.filter(other => String(other.title || other.name || other.id) === String(skill.title || skill.name || skill.id)).length > 1;
|
|
3559
|
-
|
|
3883
|
+
// Source and duplicate hints lead so a truncated preview cannot hide them.
|
|
3884
|
+
commands.push({ id: `local-skill:${skill.id}`, kind: 'codex-local-skill', group: 'local', scope: 'codex-local', skillId: skill.id,
|
|
3560
3885
|
title: String(skill.title || skill.name || skill.id).trim().slice(0, 80),
|
|
3561
3886
|
searchName: String(skill.name || ''),
|
|
3562
|
-
subtitle:
|
|
3563
|
-
|
|
3887
|
+
subtitle: [skill.sourceKind === 'plugin' && skill.sourceLabel ? skill.sourceLabel : '',
|
|
3888
|
+
sameName ? skill.sourceHint || skill.id.slice(-8) : '',
|
|
3889
|
+
String(skill.preview || '').trim() || tr('slashUseLocalSkillSubtitle')].filter(Boolean).join(' · ') });
|
|
3564
3890
|
}
|
|
3565
3891
|
return commands;
|
|
3566
3892
|
}
|
|
@@ -3607,7 +3933,7 @@
|
|
|
3607
3933
|
|| command.id.toLowerCase().includes(query)
|
|
3608
3934
|
|| String(command.searchName || '').toLowerCase().includes(query);
|
|
3609
3935
|
});
|
|
3610
|
-
renderSlashMenu(commands);
|
|
3936
|
+
renderSlashMenu(commands, { query });
|
|
3611
3937
|
return;
|
|
3612
3938
|
}
|
|
3613
3939
|
const atTrigger = getAtFileTrigger();
|
|
@@ -3705,7 +4031,7 @@
|
|
|
3705
4031
|
closeSlashMenu();
|
|
3706
4032
|
return;
|
|
3707
4033
|
}
|
|
3708
|
-
renderSlashMenu(entries);
|
|
4034
|
+
renderSlashMenu(entries, { query });
|
|
3709
4035
|
}
|
|
3710
4036
|
|
|
3711
4037
|
// Lazy one-shot load: the tray's file list is usually warm (panel open
|
|
@@ -3788,7 +4114,22 @@
|
|
|
3788
4114
|
};
|
|
3789
4115
|
}
|
|
3790
4116
|
|
|
3791
|
-
|
|
4117
|
+
const SLASH_MENU_GROUPS = { commands: 'slashGroupCommands', overleaf: 'slashGroupOverleafSkills', local: 'slashGroupLocalSkills' };
|
|
4118
|
+
const SLASH_MENU_ICONS = { compact: 'compact', installer: 'install', 'codex-overleaf-skill': 'book', 'codex-local-skill': 'terminal' };
|
|
4119
|
+
|
|
4120
|
+
function appendHighlightedText(node, value, query) {
|
|
4121
|
+
const text = String(value || '');
|
|
4122
|
+
const at = query ? text.toLowerCase().indexOf(query) : -1;
|
|
4123
|
+
if (at < 0) {
|
|
4124
|
+
node.append(text);
|
|
4125
|
+
return;
|
|
4126
|
+
}
|
|
4127
|
+
const mark = document.createElement('mark');
|
|
4128
|
+
mark.textContent = text.slice(at, at + query.length);
|
|
4129
|
+
node.append(text.slice(0, at), mark, text.slice(at + query.length));
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
function renderSlashMenu(commands = [], { query = '' } = {}) {
|
|
3792
4133
|
const menu = panel?.querySelector('[data-slash-menu]');
|
|
3793
4134
|
if (!menu) {
|
|
3794
4135
|
return;
|
|
@@ -3798,7 +4139,17 @@
|
|
|
3798
4139
|
menu.hidden = true;
|
|
3799
4140
|
return;
|
|
3800
4141
|
}
|
|
4142
|
+
const usage = commands.some(command => command.kind === 'compact') ? getContextUsageDisplay().usage : null;
|
|
4143
|
+
const percent = usage?.contextWindow > 0 ? usage.usedTokens / usage.contextWindow * 100 : null;
|
|
4144
|
+
let group = '';
|
|
3801
4145
|
commands.forEach((command, index) => {
|
|
4146
|
+
if (command.group && command.group !== group) {
|
|
4147
|
+
group = command.group;
|
|
4148
|
+
const label = document.createElement('div');
|
|
4149
|
+
label.className = 'codex-slash-menu-group';
|
|
4150
|
+
label.textContent = tr(SLASH_MENU_GROUPS[group]);
|
|
4151
|
+
menu.append(label);
|
|
4152
|
+
}
|
|
3802
4153
|
const button = document.createElement('button');
|
|
3803
4154
|
button.type = 'button';
|
|
3804
4155
|
button.dataset.slashCommand = command.id;
|
|
@@ -3810,13 +4161,44 @@
|
|
|
3810
4161
|
button.dataset.slashSkillScope = command.scope;
|
|
3811
4162
|
}
|
|
3812
4163
|
button.dataset.active = index === 0 ? 'true' : 'false';
|
|
4164
|
+
const icon = document.createElement('span');
|
|
4165
|
+
icon.className = 'codex-slash-menu-icon';
|
|
4166
|
+
icon.innerHTML = Modules.PanelIcons.svg(SLASH_MENU_ICONS[command.kind] || 'file');
|
|
4167
|
+
const text = document.createElement('span');
|
|
4168
|
+
text.className = 'codex-slash-menu-text';
|
|
3813
4169
|
const title = document.createElement('span');
|
|
3814
|
-
title.
|
|
4170
|
+
title.className = 'codex-slash-menu-title';
|
|
4171
|
+
appendHighlightedText(title, command.title, query);
|
|
4172
|
+
if (command.slug) {
|
|
4173
|
+
const slug = document.createElement('code');
|
|
4174
|
+
appendHighlightedText(slug, command.slug, query);
|
|
4175
|
+
title.append(slug);
|
|
4176
|
+
}
|
|
3815
4177
|
const subtitle = document.createElement('small');
|
|
3816
4178
|
subtitle.textContent = command.subtitle;
|
|
3817
|
-
|
|
4179
|
+
subtitle.title = command.subtitle;
|
|
4180
|
+
text.append(title, subtitle);
|
|
4181
|
+
button.append(icon, text);
|
|
4182
|
+
if (command.kind === 'compact' && percent !== null) {
|
|
4183
|
+
const meta = document.createElement('span');
|
|
4184
|
+
meta.className = 'codex-slash-menu-meta';
|
|
4185
|
+
meta.dataset.level = Modules.ContextUsage.level(percent);
|
|
4186
|
+
meta.title = tr('contextUsageTitle');
|
|
4187
|
+
meta.textContent = new Intl.NumberFormat(getLocale() === 'zh' ? 'zh-CN' : 'en-US', { maximumFractionDigits: 0 }).format(percent) + '%';
|
|
4188
|
+
button.append(meta);
|
|
4189
|
+
}
|
|
3818
4190
|
menu.append(button);
|
|
3819
4191
|
});
|
|
4192
|
+
const foot = document.createElement('div');
|
|
4193
|
+
foot.className = 'codex-slash-menu-foot';
|
|
4194
|
+
foot.dataset.slashMenuFoot = '';
|
|
4195
|
+
for (const [keys, label] of [[['↑', '↓'], 'slashKeyMove'], [['Enter'], 'slashKeyRun'], [['Esc'], 'slashKeyClose']]) {
|
|
4196
|
+
const hint = document.createElement('span');
|
|
4197
|
+
for (const key of keys) hint.append(Object.assign(document.createElement('kbd'), { textContent: key }));
|
|
4198
|
+
hint.append(tr(label));
|
|
4199
|
+
foot.append(hint);
|
|
4200
|
+
}
|
|
4201
|
+
menu.append(foot);
|
|
3820
4202
|
renderedSlashCommands = new Map(commands.map(command => [command.id, command]));
|
|
3821
4203
|
menu.dataset.activeIndex = '0';
|
|
3822
4204
|
menu.hidden = false;
|
|
@@ -3891,10 +4273,16 @@
|
|
|
3891
4273
|
});
|
|
3892
4274
|
const active = buttons[index];
|
|
3893
4275
|
if (!active || !menu.clientHeight) return;
|
|
4276
|
+
// Wrapping to the first item also reveals its group label.
|
|
4277
|
+
if (index === 0) {
|
|
4278
|
+
menu.scrollTop = 0;
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
3894
4281
|
const viewport = menu.getBoundingClientRect();
|
|
3895
4282
|
const item = active.getBoundingClientRect();
|
|
3896
4283
|
const top = viewport.top + menu.clientTop;
|
|
3897
|
-
|
|
4284
|
+
// The sticky key-hint footer covers the bottom of the scroll area.
|
|
4285
|
+
const bottom = top + menu.clientHeight - (menu.querySelector('[data-slash-menu-foot]')?.offsetHeight || 0);
|
|
3898
4286
|
// Scroll only this popup; scrollIntoView may move the chat or editor too.
|
|
3899
4287
|
if (item.height > menu.clientHeight || item.top < top) {
|
|
3900
4288
|
menu.scrollTop += item.top - top;
|
|
@@ -3904,6 +4292,7 @@
|
|
|
3904
4292
|
}
|
|
3905
4293
|
|
|
3906
4294
|
function selectSlashCommand(commandId) {
|
|
4295
|
+
if (sessionManager.isTransitioning()) return;
|
|
3907
4296
|
const command = renderedSlashCommands.get(commandId);
|
|
3908
4297
|
if (!command) {
|
|
3909
4298
|
closeSlashMenu();
|
|
@@ -3916,6 +4305,11 @@
|
|
|
3916
4305
|
applyAtFileSelection(command);
|
|
3917
4306
|
return;
|
|
3918
4307
|
}
|
|
4308
|
+
if (command.kind === 'compact') {
|
|
4309
|
+
closeSlashMenu();
|
|
4310
|
+
safeRunTask({ operation: 'compact', consumeCompactDraft: true });
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
3919
4313
|
clearSlashTriggerFromTaskInput();
|
|
3920
4314
|
closeSlashMenu();
|
|
3921
4315
|
if (command.kind === 'installer') {
|
|
@@ -4191,8 +4585,17 @@
|
|
|
4191
4585
|
Bridge: Modules.SharedSessionBridge, StorageDb: Modules.StorageDb,
|
|
4192
4586
|
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
|
|
4193
4587
|
getState: () => state, setState: next => { state = next; },
|
|
4194
|
-
|
|
4195
|
-
|
|
4588
|
+
isDashboard: () => panel?.dataset.view === 'recent-projects',
|
|
4589
|
+
refreshDashboard: refreshDashboardSoon,
|
|
4590
|
+
// An in-flight Accept/Undo keeps its view until it settles; refresh applies afterwards.
|
|
4591
|
+
isBusy: () => Boolean(pendingDraftSave || readLiveRunViewForSaveStateGuard() || trackedChangeInFlight.size
|
|
4592
|
+
|| runReviewController?.hasActiveReview?.() || document.querySelector('dialog[open]')),
|
|
4593
|
+
hasLocalDraft: current => Boolean(current.task) && composerDraftEditedHere,
|
|
4594
|
+
normalize: normalizePanelState,
|
|
4595
|
+
render: () => {
|
|
4596
|
+
applyStateToPanel();
|
|
4597
|
+
composerDraftEditedHere = false;
|
|
4598
|
+
},
|
|
4196
4599
|
notice: error => appendStorageNoticeOnce('shared-history-refresh', tx(
|
|
4197
4600
|
`Could not refresh shared history: ${error.message}`,
|
|
4198
4601
|
`共享会话历史刷新失败:${error.message}`
|
|
@@ -4292,6 +4695,7 @@
|
|
|
4292
4695
|
storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
|
|
4293
4696
|
const reloaded = await loadStoredState();
|
|
4294
4697
|
state = normalizeLoadedPanelState(reloaded);
|
|
4698
|
+
composerDraftEditedHere = false;
|
|
4295
4699
|
await getRunReviewController().recoverInterruptedUndoStatuses();
|
|
4296
4700
|
applyStateToPanel();
|
|
4297
4701
|
} catch (_error) {
|
|
@@ -4354,6 +4758,7 @@
|
|
|
4354
4758
|
function leaveActiveProject(newId) {
|
|
4355
4759
|
const prevId = activeProjectId;
|
|
4356
4760
|
activeProjectId = newId; // null for non-project URLs (spec §5.7.1)
|
|
4761
|
+
if (prevId !== newId) composerDraftEditedHere = false;
|
|
4357
4762
|
getComposerDraftPersistence().syncScope();
|
|
4358
4763
|
if (!prevId || prevId === newId) {
|
|
4359
4764
|
return;
|
|
@@ -4380,7 +4785,15 @@
|
|
|
4380
4785
|
}
|
|
4381
4786
|
|
|
4382
4787
|
async function onSpaRouteChange() {
|
|
4788
|
+
// Snapshot the departing draft before hydration replaces state. The detached
|
|
4789
|
+
// writer retains its original account/project and never reads the new view's attachments.
|
|
4790
|
+
if (pendingDraftSave?.projectId === activeProjectId
|
|
4791
|
+
&& pendingDraftSave?.accountScopeId === cachedAccountScopeId) {
|
|
4792
|
+
void saveState({ draftScope: pendingDraftSave });
|
|
4793
|
+
}
|
|
4794
|
+
resetDraftSave();
|
|
4383
4795
|
const generation = ++spaRouteGeneration;
|
|
4796
|
+
activeTurnControl.cancelRecovery();
|
|
4384
4797
|
const url = new URL(window.location.href);
|
|
4385
4798
|
const pathname = url.pathname;
|
|
4386
4799
|
const previousProjectId = activeProjectId;
|
|
@@ -4604,7 +5017,7 @@
|
|
|
4604
5017
|
operations: [],
|
|
4605
5018
|
status: tr('modeAsk')
|
|
4606
5019
|
});
|
|
4607
|
-
appendCompletionReport({
|
|
5020
|
+
appendCompletionReport({ conclusionKey: !notes ? 'ask_inspected' : undefined,
|
|
4608
5021
|
conclusion: notes || tx('This run only inspected and explained; no files were written.', '这轮只做了检查和说明,没有写入文件。'),
|
|
4609
5022
|
status: tr('modeAsk'),
|
|
4610
5023
|
notes,
|
|
@@ -4646,7 +5059,7 @@
|
|
|
4646
5059
|
applyResults,
|
|
4647
5060
|
status: 'applied with delete plan'
|
|
4648
5061
|
});
|
|
4649
|
-
appendCompletionReport({
|
|
5062
|
+
appendCompletionReport({ conclusionKey: !notes ? 'delete_handled' : undefined,
|
|
4650
5063
|
conclusion: notes || tx('Changes were written, and delete items were handled after your confirmation.', '已写入修改,并按你的确认处理删除项。'),
|
|
4651
5064
|
status: 'applied with delete plan',
|
|
4652
5065
|
notes,
|
|
@@ -4669,7 +5082,7 @@
|
|
|
4669
5082
|
status: 'applied without deletes',
|
|
4670
5083
|
deletePlanRejected: true
|
|
4671
5084
|
});
|
|
4672
|
-
appendCompletionReport({
|
|
5085
|
+
appendCompletionReport({ conclusionKey: 'delete_rejected',
|
|
4673
5086
|
conclusion: tx('Non-delete changes were kept; delete items were not written.', '已保留非删除修改;删除项没有写入。'),
|
|
4674
5087
|
status: 'applied without deletes',
|
|
4675
5088
|
notes,
|
|
@@ -4699,7 +5112,7 @@
|
|
|
4699
5112
|
applyResults: [applied],
|
|
4700
5113
|
status: result.status || 'completed'
|
|
4701
5114
|
});
|
|
4702
|
-
appendCompletionReport({
|
|
5115
|
+
appendCompletionReport({ conclusionKey: !notes ? 'run_complete' : undefined,
|
|
4703
5116
|
conclusion: notes || tx('This run is complete.', '这轮任务已完成。'),
|
|
4704
5117
|
status: result.status || 'completed',
|
|
4705
5118
|
notes,
|
|
@@ -4781,10 +5194,19 @@
|
|
|
4781
5194
|
// Thread runProjectId so the page-side dispatcher gates the toggle. Same
|
|
4782
5195
|
// intent as the pre-flight check above: do not flip Track Changes /
|
|
4783
5196
|
// Editing in a different project than the run is bound to.
|
|
5197
|
+
const owner = currentRunView;
|
|
4784
5198
|
const result = await callPageBridge(method, {
|
|
4785
5199
|
waitMs: 1800,
|
|
4786
5200
|
runProjectId: currentRunView?.runProjectId || ''
|
|
4787
5201
|
});
|
|
5202
|
+
throwIfRunCancellationRequested();
|
|
5203
|
+
if (owner && (owner !== currentRunView || owner.runProjectId !== getCurrentProjectId()
|
|
5204
|
+
|| owner.runAccountScopeId !== cachedAccountScopeId)) throw createRunCancellationError();
|
|
5205
|
+
if (requireReviewing && (result?.ok || result?.code === 'reviewing_unavailable')) {
|
|
5206
|
+
projectSettingsCoordinator.observeReviewingAvailability(result.ok ? 'available' : 'unavailable',
|
|
5207
|
+
result.ok ? 'reviewing_confirmed' : 'upgrade_modal');
|
|
5208
|
+
syncProbeStatus();
|
|
5209
|
+
}
|
|
4788
5210
|
if (result?.ok) {
|
|
4789
5211
|
appendRunEvent({
|
|
4790
5212
|
title: requireReviewing
|
|
@@ -4816,6 +5238,7 @@
|
|
|
4816
5238
|
code: result?.code || (requireReviewing ? 'reviewing_not_enabled' : 'editing_not_confirmed'),
|
|
4817
5239
|
reason: result?.reason || (requireReviewing ? 'Reviewing/Track Changes was not enabled' : 'Editing mode was not confirmed'),
|
|
4818
5240
|
reviewing: result?.reviewing || null,
|
|
5241
|
+
evidence: result?.evidence, failure: result?.failure,
|
|
4819
5242
|
requireReviewing
|
|
4820
5243
|
};
|
|
4821
5244
|
}
|
|
@@ -4830,8 +5253,9 @@
|
|
|
4830
5253
|
operation,
|
|
4831
5254
|
result: {
|
|
4832
5255
|
ok: false,
|
|
4833
|
-
code: blockedByEditing ? 'editing_not_confirmed' : 'reviewing_not_enabled',
|
|
4834
|
-
|
|
5256
|
+
code: reviewing?.code || (blockedByEditing ? 'editing_not_confirmed' : 'reviewing_not_enabled'),
|
|
5257
|
+
evidence: reviewing?.evidence, failure: reviewing?.failure,
|
|
5258
|
+
reason: reviewing?.code === 'reviewing_unavailable' ? tr('failureReason_reviewing_unavailable_user') : blockedByEditing
|
|
4835
5259
|
? tx(
|
|
4836
5260
|
'Track is off, but Overleaf Editing mode was not verified before writing. Codex did not write this file.',
|
|
4837
5261
|
'已关闭“留痕”,但写入前没有确认 Overleaf 正在用 Editing 模式。Codex 没有写入这个文件。'
|
|
@@ -4885,13 +5309,13 @@
|
|
|
4885
5309
|
|
|
4886
5310
|
async function refreshProbe(options = {}) {
|
|
4887
5311
|
const projectId = getCurrentProjectId();
|
|
5312
|
+
const accountScopeId = cachedAccountScopeId;
|
|
4888
5313
|
const userInitiated = options.userInitiated === true;
|
|
4889
5314
|
if (userInitiated) {
|
|
4890
5315
|
setRefreshProbeLoading(true);
|
|
4891
5316
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4892
5317
|
if (status) {
|
|
4893
|
-
status
|
|
4894
|
-
status.dataset.ok = 'false';
|
|
5318
|
+
renderProbeStatusMessage(status, tr('refreshProbeLoading'));
|
|
4895
5319
|
status.dataset.refreshing = 'true';
|
|
4896
5320
|
}
|
|
4897
5321
|
}
|
|
@@ -4901,13 +5325,14 @@
|
|
|
4901
5325
|
manualOverride: state?.requireReviewing === false
|
|
4902
5326
|
});
|
|
4903
5327
|
if (projectId !== getCurrentProjectId()) return null;
|
|
5328
|
+
if (accountScopeId !== cachedAccountScopeId) return null;
|
|
5329
|
+
if (probe.capabilities?.reviewing?.confirmed === true) {
|
|
5330
|
+
projectSettingsCoordinator.observeReviewingAvailability('available', 'reviewing_confirmed');
|
|
5331
|
+
}
|
|
4904
5332
|
probeStatusSnapshot = { projectId, probe, userInitiated };
|
|
4905
5333
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4906
5334
|
if (status) {
|
|
4907
|
-
status
|
|
4908
|
-
? tr('refreshProbeDone', { status: formatProbeStatusBar(probe) })
|
|
4909
|
-
: formatProbeStatusBar(probe);
|
|
4910
|
-
status.dataset.ok = isProbeReadyForCurrentMode(probe) ? 'true' : 'false';
|
|
5335
|
+
renderProbeStatusLine(status, probe, { refreshed: userInitiated });
|
|
4911
5336
|
status.dataset.refreshing = 'false';
|
|
4912
5337
|
}
|
|
4913
5338
|
if (!options.quiet && !userInitiated) {
|
|
@@ -4924,8 +5349,7 @@
|
|
|
4924
5349
|
probeStatusSnapshot = { projectId, failed: true };
|
|
4925
5350
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4926
5351
|
if (status) {
|
|
4927
|
-
status
|
|
4928
|
-
status.dataset.ok = 'false';
|
|
5352
|
+
renderProbeStatusMessage(status, tr('refreshProbeFailed'), 'warn');
|
|
4929
5353
|
status.dataset.refreshing = 'false';
|
|
4930
5354
|
}
|
|
4931
5355
|
console.warn('[codex-overleaf] refresh probe failed', error);
|
|
@@ -4945,23 +5369,81 @@
|
|
|
4945
5369
|
button.dataset.loading = loading ? 'true' : 'false';
|
|
4946
5370
|
button.disabled = Boolean(loading);
|
|
4947
5371
|
button.setAttribute('aria-busy', loading ? 'true' : 'false');
|
|
5372
|
+
setElementTitleAndAria('[data-refresh]', tr(loading ? 'refreshProbeLoading' : 'refreshProbe'),
|
|
5373
|
+
tr(loading ? 'refreshProbeLoading' : 'refreshProbeTitle'));
|
|
4948
5374
|
}
|
|
4949
5375
|
|
|
4950
5376
|
function syncProbeStatus(mode = state?.mode) {
|
|
4951
5377
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4952
5378
|
if (!status) return;
|
|
4953
5379
|
if (status.dataset.refreshing === 'true') {
|
|
4954
|
-
status
|
|
5380
|
+
renderProbeStatusMessage(status, tr('refreshProbeLoading'));
|
|
4955
5381
|
return;
|
|
4956
5382
|
}
|
|
4957
5383
|
if (probeStatusSnapshot?.projectId !== getCurrentProjectId()) return;
|
|
4958
5384
|
const { failed, probe, userInitiated } = probeStatusSnapshot;
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
status.dataset.ok = !failed && isProbeReadyForCurrentMode(probe, mode) ? 'true' : 'false';
|
|
5385
|
+
if (failed) renderProbeStatusMessage(status, tr('refreshProbeFailed'), 'warn');
|
|
5386
|
+
else renderProbeStatusLine(status, probe, { refreshed: userInitiated });
|
|
4962
5387
|
if (!failed) updateExistingProbeNotice(probe, mode);
|
|
4963
5388
|
}
|
|
4964
5389
|
|
|
5390
|
+
// Keep the read scope visible; a known Track limitation adds its own warning segment.
|
|
5391
|
+
function getProbeScope(probe) {
|
|
5392
|
+
const selection = selectionContextView.capture();
|
|
5393
|
+
const focusFiles = getActiveFocusFiles();
|
|
5394
|
+
// Match submission priority: an edit selection, then explicit focus files,
|
|
5395
|
+
// then reference context, ahead of the editor's implicit current-file hint.
|
|
5396
|
+
if (selection?.mode === 'edit') return { icon: 'file', key: 'probeScopeSelection' };
|
|
5397
|
+
if (focusFiles.some(file => /\.tex$/i.test(file))) return { icon: 'file', key: 'probeScopeFiles' };
|
|
5398
|
+
if (focusFiles.length || selection) return { icon: 'attach', key: 'probeScopeContext' };
|
|
5399
|
+
if (getProbeRunReadiness(probe).editorOk) return { icon: 'file', key: 'probeScopeCurrentFile' };
|
|
5400
|
+
return { icon: 'folder', key: 'probeScopeProject' };
|
|
5401
|
+
}
|
|
5402
|
+
|
|
5403
|
+
function isReviewingKnownUnavailable() {
|
|
5404
|
+
return Modules.Reviewing.knownUnavailable(projectSettingsCoordinator.getReviewingAvailability());
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5407
|
+
function syncReviewingTooltip() {
|
|
5408
|
+
const toggle = panel?.querySelector('[data-require-reviewing]');
|
|
5409
|
+
const title = tr(isReviewingKnownUnavailable() ? 'reviewingUnavailableTooltip' : 'requireReviewingTitle');
|
|
5410
|
+
if (toggle) toggle.title = title;
|
|
5411
|
+
const label = toggle?.closest('.codex-review-toggle');
|
|
5412
|
+
if (label) label.title = title;
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
function renderProbeStatusLine(status, probe, options = {}) {
|
|
5416
|
+
const scope = getProbeScope(probe);
|
|
5417
|
+
const parts = [];
|
|
5418
|
+
if (options.refreshed) parts.push(tr('refreshProbeDone', { status: '' }).trim());
|
|
5419
|
+
const label = document.createElement('span');
|
|
5420
|
+
label.className = 'codex-probe-scope';
|
|
5421
|
+
label.innerHTML = Modules.PanelIcons?.svg?.(scope.icon) || ''; // Static, repository-owned icon path.
|
|
5422
|
+
label.append(tr(scope.key));
|
|
5423
|
+
parts.push(label);
|
|
5424
|
+
if (isExperimentalOtEnabled()) {
|
|
5425
|
+
const separator = document.createElement('span');
|
|
5426
|
+
separator.className = 'codex-probe-sep';
|
|
5427
|
+
separator.textContent = '·';
|
|
5428
|
+
parts.push(separator, 'OT ' + formatOtStatusLabel(getCurrentOtStatus()));
|
|
5429
|
+
}
|
|
5430
|
+
if (state.requireReviewing && isReviewingKnownUnavailable()) {
|
|
5431
|
+
const warning = document.createElement('span');
|
|
5432
|
+
warning.dataset.tone = 'warn';
|
|
5433
|
+
warning.textContent = ' · ' + tr('reviewingUnavailableStatus');
|
|
5434
|
+
parts.push(warning);
|
|
5435
|
+
}
|
|
5436
|
+
syncReviewingTooltip();
|
|
5437
|
+
status.replaceChildren(...parts);
|
|
5438
|
+
delete status.dataset.tone;
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5441
|
+
function renderProbeStatusMessage(status, text, tone = '') {
|
|
5442
|
+
status.replaceChildren(text);
|
|
5443
|
+
if (tone) status.dataset.tone = tone;
|
|
5444
|
+
else delete status.dataset.tone;
|
|
5445
|
+
}
|
|
5446
|
+
|
|
4965
5447
|
function formatProbeStatusBar(probe, mode = state?.mode) {
|
|
4966
5448
|
const readiness = getProbeRunReadiness(probe);
|
|
4967
5449
|
const reviewingOk = readiness.reviewingOk;
|
|
@@ -4995,17 +5477,7 @@
|
|
|
4995
5477
|
if (!status || status.dataset.refreshing === 'true') {
|
|
4996
5478
|
return;
|
|
4997
5479
|
}
|
|
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;
|
|
5480
|
+
syncProbeStatus();
|
|
5009
5481
|
}
|
|
5010
5482
|
|
|
5011
5483
|
function getProbeRunReadiness(probe) {
|
|
@@ -5110,8 +5582,8 @@
|
|
|
5110
5582
|
ready: true,
|
|
5111
5583
|
message: manualOverride
|
|
5112
5584
|
? tx(
|
|
5113
|
-
`Ready:
|
|
5114
|
-
|
|
5585
|
+
`Ready: Codex writes without tracked changes and ${readiness.contextLabel}.`,
|
|
5586
|
+
`可以运行:Codex 写入时不使用留痕,并且${readiness.contextLabel}。`
|
|
5115
5587
|
)
|
|
5116
5588
|
: tx(
|
|
5117
5589
|
`Ready: Codex verified Overleaf Reviewing/Track Changes is on and ${readiness.contextLabel}.`,
|
|
@@ -5120,27 +5592,20 @@
|
|
|
5120
5592
|
};
|
|
5121
5593
|
}
|
|
5122
5594
|
|
|
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
|
-
|
|
5595
|
+
// ensureReviewingBeforeWrite switches Overleaf to Reviewing before the first
|
|
5596
|
+
// write and blocks the write only if that cannot be confirmed.
|
|
5133
5597
|
return {
|
|
5134
|
-
ready:
|
|
5598
|
+
ready: true,
|
|
5135
5599
|
message: tx(
|
|
5136
|
-
|
|
5137
|
-
|
|
5600
|
+
`Ready: Codex ${readiness.contextLabel} and will switch Overleaf to Reviewing/Track Changes before writing.`,
|
|
5601
|
+
`可以运行:Codex ${readiness.contextLabel},写入前会把 Overleaf 切到 Reviewing/Track Changes。`
|
|
5138
5602
|
)
|
|
5139
5603
|
};
|
|
5140
5604
|
}
|
|
5141
5605
|
|
|
5142
|
-
async function getRunProjectSnapshot() {
|
|
5606
|
+
async function getRunProjectSnapshot(expectedProjectId = getCurrentProjectId()) {
|
|
5143
5607
|
const project = await callPageBridge('getProjectSnapshot', {
|
|
5608
|
+
projectId: expectedProjectId,
|
|
5144
5609
|
force: true,
|
|
5145
5610
|
maxAgeMs: 0,
|
|
5146
5611
|
preferLightweight: true,
|
|
@@ -5152,11 +5617,12 @@
|
|
|
5152
5617
|
zipTimeoutMs: RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
|
|
5153
5618
|
focusFiles: getActiveFocusFiles()
|
|
5154
5619
|
});
|
|
5155
|
-
const sanitizedProject = sanitizeRunProjectSnapshot(project);
|
|
5620
|
+
const sanitizedProject = sanitizeRunProjectSnapshot(project, expectedProjectId);
|
|
5156
5621
|
if (sanitizedProject?.capabilities?.fullProjectSnapshot || sanitizedProject?.capabilities?.pathIdentityComplete === false) {
|
|
5157
5622
|
return sanitizedProject;
|
|
5158
5623
|
}
|
|
5159
5624
|
const pageStateProject = await callPageBridge('getProjectSnapshot', {
|
|
5625
|
+
projectId: expectedProjectId,
|
|
5160
5626
|
force: true,
|
|
5161
5627
|
maxAgeMs: 0,
|
|
5162
5628
|
preferLightweight: true,
|
|
@@ -5166,17 +5632,21 @@
|
|
|
5166
5632
|
includeBinaryFiles: false,
|
|
5167
5633
|
focusFiles: getActiveFocusFiles()
|
|
5168
5634
|
});
|
|
5169
|
-
const sanitizedPageStateProject = sanitizeRunProjectSnapshot(pageStateProject);
|
|
5635
|
+
const sanitizedPageStateProject = sanitizeRunProjectSnapshot(pageStateProject, expectedProjectId);
|
|
5170
5636
|
if (sanitizedPageStateProject?.capabilities?.fullProjectSnapshot) {
|
|
5171
5637
|
return sanitizedPageStateProject;
|
|
5172
5638
|
}
|
|
5173
5639
|
return sanitizedProject;
|
|
5174
5640
|
}
|
|
5175
5641
|
|
|
5176
|
-
function sanitizeRunProjectSnapshot(project) {
|
|
5642
|
+
function sanitizeRunProjectSnapshot(project, expectedProjectId = project?.id) {
|
|
5177
5643
|
if (!project || typeof project !== 'object') return project;
|
|
5644
|
+
if (project.id !== expectedProjectId) return { ...project, files: [], capabilities: { ...project.capabilities,
|
|
5645
|
+
fullProjectSnapshot: false, skipped: [{ path: '', reason: 'document_identity_mismatch' }] } };
|
|
5178
5646
|
const normalize = Modules.ProjectFiles.normalizeSafeProjectPath;
|
|
5179
|
-
const files = Array.isArray(project.files) ? project.files
|
|
5647
|
+
const files = Array.isArray(project.files) ? project.files.map(file => file?.documentIdentity === undefined ? file
|
|
5648
|
+
: { ...file, documentIdentity: snapshotDocumentIdentity(file.documentIdentity, file.path, project.id) }) : [];
|
|
5649
|
+
project = { ...project, files };
|
|
5180
5650
|
const counts = new Map();
|
|
5181
5651
|
for (const file of files) {
|
|
5182
5652
|
const path = normalize(file?.path);
|
|
@@ -5213,12 +5683,16 @@
|
|
|
5213
5683
|
// --- Warm Mirror Controller ---
|
|
5214
5684
|
|
|
5215
5685
|
async function getMirrorFreshness() {
|
|
5686
|
+
const projectId = getCurrentProjectId(), accountScopeId = cachedAccountScopeId;
|
|
5687
|
+
if (await writebackOrchestrator.refreshMirrorReuseFence(projectId, accountScopeId)) return null;
|
|
5688
|
+
if (getCurrentProjectId() !== projectId || cachedAccountScopeId !== accountScopeId) return null;
|
|
5216
5689
|
try {
|
|
5217
5690
|
const response = await sendBackgroundNative({
|
|
5218
5691
|
method: 'mirror.status',
|
|
5219
|
-
params: { projectId
|
|
5692
|
+
params: { projectId }
|
|
5220
5693
|
});
|
|
5221
|
-
|
|
5694
|
+
const fence = await writebackOrchestrator.refreshMirrorReuseFence(projectId, accountScopeId);
|
|
5695
|
+
if (response?.ok && !fence && getCurrentProjectId() === projectId && cachedAccountScopeId === accountScopeId) {
|
|
5222
5696
|
return response.result;
|
|
5223
5697
|
}
|
|
5224
5698
|
} catch (error) { /* fall through */ }
|
|
@@ -5249,7 +5723,7 @@
|
|
|
5249
5723
|
}
|
|
5250
5724
|
|
|
5251
5725
|
const mirrorStatus = await getMirrorFreshness();
|
|
5252
|
-
const mirrorHealth = Modules.MirrorHealth.classifyMirrorHealth(mirrorStatus);
|
|
5726
|
+
const mirrorHealth = Modules.MirrorHealth.classifyMirrorHealth(mirrorStatus || {});
|
|
5253
5727
|
if (!mirrorStatus?.exists || !mirrorHealth.reusable) {
|
|
5254
5728
|
return { useExistingMirror: false, reason: 'mirror_not_fresh', mirrorStatus };
|
|
5255
5729
|
}
|
|
@@ -5278,17 +5752,33 @@
|
|
|
5278
5752
|
return candidates
|
|
5279
5753
|
.filter(file =>
|
|
5280
5754
|
typeof file.content === 'string' &&
|
|
5281
|
-
Modules.ProjectFiles.isUsableProjectFileContent(file.content) &&
|
|
5755
|
+
(Modules.ProjectFiles.isUsableProjectFileContent(file.content) || file.content === '') &&
|
|
5282
5756
|
file.path &&
|
|
5283
5757
|
!seen.has(file.path) &&
|
|
5284
5758
|
seen.add(file.path)
|
|
5285
5759
|
)
|
|
5286
5760
|
.map(file => ({
|
|
5287
5761
|
path: file.path,
|
|
5288
|
-
content: file.content
|
|
5762
|
+
content: file.content,
|
|
5763
|
+
kind: 'text',
|
|
5764
|
+
source: file.source,
|
|
5765
|
+
...(file.documentIdentity !== undefined
|
|
5766
|
+
? { documentIdentity: snapshotDocumentIdentity(file.documentIdentity, file.path, project.id) } : {})
|
|
5289
5767
|
}));
|
|
5290
5768
|
}
|
|
5291
5769
|
|
|
5770
|
+
function snapshotDocumentIdentity(identity, path, projectId) {
|
|
5771
|
+
// Preserve invalid evidence as invalid, never silently downgrade to the
|
|
5772
|
+
// missing-evidence fallback. Native independently hashes the content.
|
|
5773
|
+
if (!identity || identity.version !== 1 || identity.source !== 'cm-ranges-v1'
|
|
5774
|
+
|| identity.path !== path || identity.projectId !== projectId
|
|
5775
|
+
|| typeof identity.documentId !== 'string' || !/^[a-f0-9]{24}$/.test(identity.documentId)
|
|
5776
|
+
|| typeof identity.targetDocumentId !== 'string' || !/^[a-f0-9]{24}$/.test(identity.targetDocumentId)
|
|
5777
|
+
|| typeof identity.contentSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(identity.contentSha256)) return null;
|
|
5778
|
+
return { version: 1, source: 'cm-ranges-v1', projectId, path,
|
|
5779
|
+
documentId: identity.documentId, targetDocumentId: identity.targetDocumentId, contentSha256: identity.contentSha256 };
|
|
5780
|
+
}
|
|
5781
|
+
|
|
5292
5782
|
function normalizeSnapshotPath(path) {
|
|
5293
5783
|
// Snapshot/overlay/focus arrays already carry canonical names from file chips.
|
|
5294
5784
|
return Modules.ProjectFiles.normalizeSafeProjectPath(path);
|
|
@@ -5521,6 +6011,7 @@
|
|
|
5521
6011
|
codexLocalSkillEnabled: prefs.codexLocalSkillEnabled || {},
|
|
5522
6012
|
experimentalOtByProject: prefs.experimentalOtByProject || {},
|
|
5523
6013
|
customInstructionsByProject: prefs.customInstructionsByProject || {},
|
|
6014
|
+
reviewingAvailabilityByProject: prefs.reviewingAvailabilityByProject || {},
|
|
5524
6015
|
governanceRulesByProject: normalizeGovernanceRulesByProject(prefs.governanceRulesByProject),
|
|
5525
6016
|
panelWidth: prefs.panelWidth || PANEL_DEFAULT_WIDTH,
|
|
5526
6017
|
sessions: sessions.map(session => ({
|
|
@@ -5531,6 +6022,7 @@
|
|
|
5531
6022
|
focusFiles: session.focusFiles || [], selectionContext: session.selectionContext?.projectId === projectId ? session.selectionContext : null,
|
|
5532
6023
|
projectReferenceFiles: session.projectReferenceFiles || [],
|
|
5533
6024
|
codexThreadId: session.codexThreadId || '',
|
|
6025
|
+
contextUsage: session.contextUsage || null,
|
|
5534
6026
|
forkedFromSessionId: session.forkedFromSessionId || '',
|
|
5535
6027
|
forkedFromRunId: session.forkedFromRunId || '',
|
|
5536
6028
|
createdAt: session.createdAt,
|
|
@@ -5554,6 +6046,8 @@
|
|
|
5554
6046
|
if (!accountScopeId || isScopedPersistenceIdentityFailure(error)) {
|
|
5555
6047
|
throw error;
|
|
5556
6048
|
}
|
|
6049
|
+
const fallbackProjectId = getCurrentProjectId();
|
|
6050
|
+
const fallbackGeneration = spaRouteGeneration;
|
|
5557
6051
|
const fallbackKey = buildScopedFallbackStorageKey(storageKey, accountScopeId);
|
|
5558
6052
|
const stored = await chrome.storage.local.get([fallbackKey]);
|
|
5559
6053
|
const fallback = stored[fallbackKey] || {};
|
|
@@ -5561,6 +6055,10 @@
|
|
|
5561
6055
|
fallback?.__codexOverleafCompactFallback === true
|
|
5562
6056
|
&& fallback.__codexOverleafFallbackAccountScopeId === accountScopeId
|
|
5563
6057
|
) {
|
|
6058
|
+
if (accountScopeId === cachedAccountScopeId && fallbackProjectId === getCurrentProjectId()
|
|
6059
|
+
&& fallbackGeneration === spaRouteGeneration) {
|
|
6060
|
+
Modules.StorageMigration.adoptProjectPreferenceBaseline(fallback, accountScopeId, fallbackProjectId);
|
|
6061
|
+
}
|
|
5564
6062
|
return {
|
|
5565
6063
|
mode: fallback.mode || '', providerId: fallback.providerId || 'builtin',
|
|
5566
6064
|
model: fallback.model || '',
|
|
@@ -5576,6 +6074,7 @@
|
|
|
5576
6074
|
codexLocalSkillEnabled: fallback.codexLocalSkillEnabled || {},
|
|
5577
6075
|
experimentalOtByProject: fallback.experimentalOtByProject || {},
|
|
5578
6076
|
customInstructionsByProject: fallback.customInstructionsByProject || {},
|
|
6077
|
+
reviewingAvailabilityByProject: fallback.reviewingAvailabilityByProject || {},
|
|
5579
6078
|
governanceRulesByProject: normalizeGovernanceRulesByProject(fallback.governanceRulesByProject),
|
|
5580
6079
|
panelWidth: fallback.panelWidth || PANEL_DEFAULT_WIDTH,
|
|
5581
6080
|
sessions: [],
|
|
@@ -5610,9 +6109,8 @@
|
|
|
5610
6109
|
// Settlement owns a run after navigation away from its project. Ordinary
|
|
5611
6110
|
// view saves must not write that run into the current route's project.
|
|
5612
6111
|
// An explicit override is reserved for callers that own a detached scope.
|
|
5613
|
-
const projectIdOverride = options && typeof options.projectIdOverride === 'string'
|
|
5614
|
-
? options.projectIdOverride
|
|
5615
|
-
: '';
|
|
6112
|
+
const projectIdOverride = options?.draftScope?.projectId || (options && typeof options.projectIdOverride === 'string'
|
|
6113
|
+
? options.projectIdOverride : '');
|
|
5616
6114
|
const urlProjectId = getCurrentProjectId();
|
|
5617
6115
|
const projectId = projectIdOverride || urlProjectId;
|
|
5618
6116
|
if (!projectId) {
|
|
@@ -5630,6 +6128,8 @@
|
|
|
5630
6128
|
));
|
|
5631
6129
|
if (options?.requireDurable || options?.reviewRunIds?.length) throw new Error('Persistence project changed.'); return;
|
|
5632
6130
|
}
|
|
6131
|
+
const savedDraft = !projectIdOverride && pendingDraftSave?.scope === getComposerDraftScope()
|
|
6132
|
+
? pendingDraftSave : null;
|
|
5633
6133
|
const compactState = prepareStateForStorage(state, { onAggressive: notifyAggressiveCompactionOnce });
|
|
5634
6134
|
compactState.autoRecompile = state.autoRecompile;
|
|
5635
6135
|
compactState.loadCodexLocalSkills = state.loadCodexLocalSkills !== false;
|
|
@@ -5638,10 +6138,24 @@
|
|
|
5638
6138
|
compactState.codexLocalSkillEnabled = getCodexLocalSkillEnabled();
|
|
5639
6139
|
compactState.experimentalOtByProject = state.experimentalOtByProject;
|
|
5640
6140
|
compactState.customInstructionsByProject = state.customInstructionsByProject;
|
|
6141
|
+
compactState.reviewingAvailabilityByProject = state.reviewingAvailabilityByProject;
|
|
5641
6142
|
compactState.governanceRulesByProject = state.governanceRulesByProject;
|
|
6143
|
+
const projectPreferenceBaseline = Migration.getProjectPreferenceBaseline(
|
|
6144
|
+
options?.draftScope?.accountScopeId || cachedAccountScopeId, projectId);
|
|
5642
6145
|
const stateSnapshot = typeof structuredClone === 'function'
|
|
5643
6146
|
? structuredClone(state)
|
|
5644
6147
|
: JSON.parse(JSON.stringify(state));
|
|
6148
|
+
const projectPreferenceWrite = Migration.captureProjectPreferenceWrite({
|
|
6149
|
+
state: stateSnapshot, baseline: projectPreferenceBaseline,
|
|
6150
|
+
accountScopeId: options?.draftScope?.accountScopeId || cachedAccountScopeId, projectId,
|
|
6151
|
+
normalizeExperimentalOtByProject, normalizeGovernanceRulesByProject, normalizeCustomInstructionsByProject
|
|
6152
|
+
});
|
|
6153
|
+
// Keep consumption provenance on the live run owner across ordinary
|
|
6154
|
+
// session normalization; it is not a composer edit or a queued input.
|
|
6155
|
+
const consumedSession = liveRunView?.draftConsumption && liveRunView.runProjectId === projectId
|
|
6156
|
+
&& liveRunView.runAccountScopeId === cachedAccountScopeId
|
|
6157
|
+
? stateSnapshot.sessions?.find(session => session.id === liveRunView.sessionId) : null;
|
|
6158
|
+
if (consumedSession?.task === '') consumedSession.draftConsumption = { ...liveRunView.draftConsumption };
|
|
5645
6159
|
const compactStateSnapshot = typeof structuredClone === 'function'
|
|
5646
6160
|
? structuredClone(compactState)
|
|
5647
6161
|
: JSON.parse(JSON.stringify(compactState));
|
|
@@ -5660,9 +6174,33 @@
|
|
|
5660
6174
|
normalizeExperimentalOtByProject,
|
|
5661
6175
|
normalizeGovernanceRulesByProject,
|
|
5662
6176
|
normalizeCustomInstructionsByProject,
|
|
6177
|
+
projectPreferenceBaseline,
|
|
6178
|
+
projectPreferenceWrite,
|
|
6179
|
+
applyProjectPreferences: (prefs, captured, fields) => {
|
|
6180
|
+
if (retentionScope.accountScopeId !== cachedAccountScopeId || retentionScope.projectId !== getCurrentProjectId()
|
|
6181
|
+
|| retentionScope.generation !== spaRouteGeneration) return false;
|
|
6182
|
+
const adopted = [];
|
|
6183
|
+
for (const field of fields) {
|
|
6184
|
+
if (JSON.stringify(state[field]?.[projectId]) !== JSON.stringify(captured[field]?.[projectId])) continue;
|
|
6185
|
+
if (JSON.stringify(state[field]?.[projectId]) === JSON.stringify(prefs[field]?.[projectId])) continue;
|
|
6186
|
+
state[field] = { ...state[field], ...prefs[field] };
|
|
6187
|
+
if (!Object.hasOwn(prefs[field] || {}, projectId)) delete state[field][projectId];
|
|
6188
|
+
adopted.push(field);
|
|
6189
|
+
}
|
|
6190
|
+
if (adopted.includes('customInstructionsByProject')) syncCustomInstructionsEditorForProject(projectId);
|
|
6191
|
+
if (adopted.includes('governanceRulesByProject')) syncProjectSettingsEditorForProject();
|
|
6192
|
+
if (adopted.includes('experimentalOtByProject')) syncExperimentalOtToggleForProject(projectId);
|
|
6193
|
+
if (adopted.includes('reviewingAvailabilityByProject')) {
|
|
6194
|
+
syncReviewingTooltip();
|
|
6195
|
+
syncProbeStatus();
|
|
6196
|
+
}
|
|
6197
|
+
return true;
|
|
6198
|
+
},
|
|
5663
6199
|
persistenceContext
|
|
5664
6200
|
});
|
|
5665
|
-
|
|
6201
|
+
if (!options?.draftScope) {
|
|
6202
|
+
await getComposerDraftPersistence().save({ accountScopeId: persistenceContext.scope.accountScopeId, projectId });
|
|
6203
|
+
}
|
|
5666
6204
|
};
|
|
5667
6205
|
|
|
5668
6206
|
const persistenceCoordinator = getScopedPersistenceCoordinator();
|
|
@@ -5671,17 +6209,16 @@
|
|
|
5671
6209
|
code: 'scoped_persistence_unavailable'
|
|
5672
6210
|
});
|
|
5673
6211
|
} else {
|
|
5674
|
-
const
|
|
5675
|
-
projectId,
|
|
5676
|
-
{
|
|
6212
|
+
const commitOptions = {
|
|
5677
6213
|
detached: Boolean(projectIdOverride),
|
|
5678
6214
|
rebase: true,
|
|
5679
6215
|
queueMutation: options?.queueMutation,
|
|
5680
6216
|
queueMutations: options?.queueMutations,
|
|
5681
6217
|
sessionTombstones: options?.deletedSessionIds
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
6218
|
+
};
|
|
6219
|
+
const committed = options?.draftScope
|
|
6220
|
+
? await persistenceCoordinator.commitDetached(projectId, options.draftScope.accountScopeId, commitOptions, persistScopedState)
|
|
6221
|
+
: await persistenceCoordinator.commit(projectId, commitOptions, persistScopedState);
|
|
5685
6222
|
if (!committed.ok && (
|
|
5686
6223
|
options?.requireDurable || options?.reviewRunIds?.length || options?.recoveryRuns?.length || options?.queueMutation
|
|
5687
6224
|
|| options?.queueMutations?.length
|
|
@@ -5692,6 +6229,16 @@
|
|
|
5692
6229
|
});
|
|
5693
6230
|
}
|
|
5694
6231
|
if (committed.ok) {
|
|
6232
|
+
if (savedDraft && pendingDraftSave === savedDraft) {
|
|
6233
|
+
const capturedTask = stateSnapshot.sessions.find(item => item.id === stateSnapshot.activeSessionId)?.task;
|
|
6234
|
+
const storedTask = StorageDb.buildSessionRecord({ task: capturedTask }).task;
|
|
6235
|
+
if (!(written || []).some(session => session.id === stateSnapshot.activeSessionId && session.task === storedTask)) {
|
|
6236
|
+
throw Object.assign(new Error(tx('The latest draft could not be saved. It is still available in this page.',
|
|
6237
|
+
'最新草稿未能保存,内容仍保留在当前页面。')), { code: 'draft_state_not_persisted' });
|
|
6238
|
+
}
|
|
6239
|
+
// A newer input revision keeps its own timer, even if this save finishes later.
|
|
6240
|
+
resetDraftSave();
|
|
6241
|
+
}
|
|
5695
6242
|
const retentionUpdates = StorageDb.projectRetentionUpdates?.({ liveState: state, capturedState: stateSnapshot, written,
|
|
5696
6243
|
capturedScope: retentionScope, currentScope: { accountScopeId: typeof cachedAccountScopeId === 'string' ? cachedAccountScopeId : '',
|
|
5697
6244
|
projectId: getCurrentProjectId(), generation: typeof spaRouteGeneration === 'number' ? spaRouteGeneration : null } }) || [];
|
|
@@ -5707,6 +6254,8 @@
|
|
|
5707
6254
|
}
|
|
5708
6255
|
} catch (error) {
|
|
5709
6256
|
if (options?.requireDurable) throw error;
|
|
6257
|
+
// A departing draft must never fall back using the destination page's state/scope.
|
|
6258
|
+
if (options?.draftScope) return;
|
|
5710
6259
|
if (error?.code === 'recovery_scope_changed') return;
|
|
5711
6260
|
if (
|
|
5712
6261
|
error?.code === 'account_scope_unavailable' &&
|
|
@@ -5824,6 +6373,65 @@
|
|
|
5824
6373
|
}, delayMs);
|
|
5825
6374
|
}
|
|
5826
6375
|
|
|
6376
|
+
function getComposerDraftScope() {
|
|
6377
|
+
return JSON.stringify([cachedAccountScopeId, getCurrentProjectId(), spaRouteGeneration, state.activeSessionId]);
|
|
6378
|
+
}
|
|
6379
|
+
|
|
6380
|
+
function resetDraftSave() {
|
|
6381
|
+
if (draftSaveTimer) clearTimeout(draftSaveTimer);
|
|
6382
|
+
draftSaveTimer = null;
|
|
6383
|
+
pendingDraftSave = null;
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6386
|
+
function captureComposerDraft() {
|
|
6387
|
+
const input = panel?.querySelector('[data-task]');
|
|
6388
|
+
if (!input || !state.activeSessionId) return;
|
|
6389
|
+
const next = updateActiveSessionDraft(state, input.value);
|
|
6390
|
+
if (next === state) return;
|
|
6391
|
+
if (next.task !== state.task) {
|
|
6392
|
+
composerDraftEditedHere = true;
|
|
6393
|
+
if (currentRunView?.sessionId === state.activeSessionId) delete currentRunView.draftConsumption;
|
|
6394
|
+
}
|
|
6395
|
+
state = next;
|
|
6396
|
+
const now = Date.now(), scope = getComposerDraftScope();
|
|
6397
|
+
pendingDraftSave = {
|
|
6398
|
+
scope,
|
|
6399
|
+
projectId: getCurrentProjectId(),
|
|
6400
|
+
accountScopeId: cachedAccountScopeId,
|
|
6401
|
+
firstAt: pendingDraftSave?.scope === scope ? pendingDraftSave.firstAt : now
|
|
6402
|
+
};
|
|
6403
|
+
if (draftSaveTimer) clearTimeout(draftSaveTimer);
|
|
6404
|
+
const delay = Math.max(0, Math.min(DRAFT_SAVE_DELAY_MS, pendingDraftSave.firstAt + DRAFT_SAVE_MAX_WAIT_MS - now));
|
|
6405
|
+
draftSaveTimer = setTimeout(() => {
|
|
6406
|
+
draftSaveTimer = null;
|
|
6407
|
+
if (pendingDraftSave?.scope !== getComposerDraftScope()) { resetDraftSave(); return; }
|
|
6408
|
+
pendingDraftSave.firstAt = Date.now();
|
|
6409
|
+
// Join the existing writer immediately; never reset its timer on every key.
|
|
6410
|
+
saveStateSoon(0);
|
|
6411
|
+
}, delay);
|
|
6412
|
+
}
|
|
6413
|
+
|
|
6414
|
+
async function flushComposerDraft({ requireDurable = false } = {}) {
|
|
6415
|
+
if (!pendingDraftSave) return;
|
|
6416
|
+
const scope = getComposerDraftScope();
|
|
6417
|
+
if (pendingDraftSave.scope !== scope) { resetDraftSave(); return; }
|
|
6418
|
+
if (draftSaveTimer) clearTimeout(draftSaveTimer);
|
|
6419
|
+
draftSaveTimer = null;
|
|
6420
|
+
pendingDraftSave.firstAt = Date.now();
|
|
6421
|
+
await flushQueuedSaveState(requireDurable ? { requireDurable: true } : null);
|
|
6422
|
+
if (requireDurable && scope !== getComposerDraftScope()) {
|
|
6423
|
+
throw new Error(tx('The active session changed while saving the draft. Try again.',
|
|
6424
|
+
'保存草稿时活动会话已改变,请重试。'));
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6427
|
+
|
|
6428
|
+
function flushComposerDraftOnLeave() {
|
|
6429
|
+
// Lifecycle events cannot await completion; periodic saves remain the primary protection.
|
|
6430
|
+
flushComposerDraft().catch(error => appendStorageNoticeOnce('save-failed', tx(
|
|
6431
|
+
`Failed to save session state: ${localizeVisibleReason(error.message)}`,
|
|
6432
|
+
`保存会话状态失败:${localizeVisibleReason(error.message)}`)));
|
|
6433
|
+
}
|
|
6434
|
+
|
|
5827
6435
|
function runQueuedSaveState() {
|
|
5828
6436
|
if (saveStateInFlightPromise) {
|
|
5829
6437
|
saveStateRunAfterFlight = true;
|
|
@@ -5924,13 +6532,11 @@
|
|
|
5924
6532
|
async function persistPanelInputs(event) {
|
|
5925
6533
|
const globalWrite = getGlobalPreferences().handleInput(event);
|
|
5926
6534
|
if (globalWrite) return globalWrite;
|
|
5927
|
-
//
|
|
5928
|
-
//
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
readPanelInputs();
|
|
5933
|
-
saveStateSoon();
|
|
6535
|
+
// Draft edits retain run/event identity. Composition updates also capture the
|
|
6536
|
+
// current text; compositionend captures its committed value if no final input fires.
|
|
6537
|
+
if (event?.target?.matches?.('[data-task]')) {
|
|
6538
|
+
captureComposerDraft();
|
|
6539
|
+
if (event.type === 'change') await flushComposerDraft();
|
|
5934
6540
|
return;
|
|
5935
6541
|
}
|
|
5936
6542
|
if (event?.target?.matches?.('[data-speed]')) {
|
|
@@ -5942,8 +6548,18 @@
|
|
|
5942
6548
|
}
|
|
5943
6549
|
renderSpeedOptions(getRenderedModelEntries());
|
|
5944
6550
|
readPanelInputs();
|
|
6551
|
+
// Only a user re-enabling Track requests another capability check. The
|
|
6552
|
+
// preceding input event and programmatic session renders must not reset it.
|
|
6553
|
+
if (event?.type === 'change' && event.isTrusted === true
|
|
6554
|
+
&& event.target?.matches?.('[data-require-reviewing]') && event.target.checked
|
|
6555
|
+
&& isReviewingKnownUnavailable()) {
|
|
6556
|
+
projectSettingsCoordinator.observeReviewingAvailability('unknown', 'user_retry');
|
|
6557
|
+
syncReviewingTooltip();
|
|
6558
|
+
syncProbeStatus();
|
|
6559
|
+
}
|
|
5945
6560
|
renderModelConfigChoices();
|
|
5946
6561
|
updateModelDisplay();
|
|
6562
|
+
renderContextUsage();
|
|
5947
6563
|
// Save feedback is per-card now (settingsPanel flashSaved); the global
|
|
5948
6564
|
// header chip and its Saving/Saved lifecycle are gone.
|
|
5949
6565
|
await saveState();
|
|
@@ -6037,9 +6653,17 @@
|
|
|
6037
6653
|
if (!composerDraftPersistence) composerDraftPersistence = Modules.ComposerAttachmentDraft.createController({
|
|
6038
6654
|
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId() }),
|
|
6039
6655
|
getAttachments: () => composerAttachmentController.serializePersistableAttachments(),
|
|
6040
|
-
|
|
6656
|
+
getOmittedAttachmentNames: () => composerAttachmentController.getOmittedAttachmentNames(),
|
|
6657
|
+
setAttachments: value => {
|
|
6658
|
+
state.composerAttachments = value;
|
|
6659
|
+
syncComposerSendAvailability();
|
|
6660
|
+
},
|
|
6041
6661
|
clear: () => composerAttachmentController.clear({ notify: false }),
|
|
6042
6662
|
restore: value => composerAttachmentController.restorePersistedAttachments(value),
|
|
6663
|
+
onRestoreOmittedAttachments: ({ count, names }) => appendPlainLog(tx(
|
|
6664
|
+
`${count} attachment${count === 1 ? '' : 's'} could not be saved in the draft. Reattach before sending: ${names.join(', ')}.`,
|
|
6665
|
+
`${count} 个附件未能保存到草稿中,请在发送前重新添加:${names.join('、')}。`
|
|
6666
|
+
)),
|
|
6043
6667
|
read: scope => Modules.StorageDb.readComposerAttachmentDraft(scope),
|
|
6044
6668
|
write: (scope, value) => Modules.StorageDb.writeComposerAttachmentDraft(scope, value),
|
|
6045
6669
|
scheduleSave: () => saveStateSoon()
|
|
@@ -6073,6 +6697,7 @@
|
|
|
6073
6697
|
if (findSessionById(submission.sessionId)?.task !== submission.task) return false;
|
|
6074
6698
|
if (state.activeSessionId !== submission.sessionId) {
|
|
6075
6699
|
updateSessionById(submission.sessionId, { task: '' });
|
|
6700
|
+
recordSubmittedDraftConsumption(submission.sessionId, submission.task);
|
|
6076
6701
|
saveStateSoon();
|
|
6077
6702
|
return false;
|
|
6078
6703
|
}
|
|
@@ -6086,6 +6711,7 @@
|
|
|
6086
6711
|
if (taskInput) {
|
|
6087
6712
|
taskInput.value = '';
|
|
6088
6713
|
}
|
|
6714
|
+
composerDraftEditedHere = false;
|
|
6089
6715
|
autosizeTaskTextarea();
|
|
6090
6716
|
if (!keepAttachments) {
|
|
6091
6717
|
composerAttachmentController.clear();
|
|
@@ -6094,10 +6720,20 @@
|
|
|
6094
6720
|
renderComposerSkillInvocation();
|
|
6095
6721
|
selectionContextView.clear();
|
|
6096
6722
|
state = updateActiveSession(state, { task: '' });
|
|
6723
|
+
if (submission) recordSubmittedDraftConsumption(submission.sessionId, submission.task);
|
|
6724
|
+
else if (currentRunView?.sessionId === state.activeSessionId) delete currentRunView.draftConsumption;
|
|
6097
6725
|
saveStateSoon();
|
|
6098
6726
|
return true;
|
|
6099
6727
|
}
|
|
6100
6728
|
|
|
6729
|
+
function recordSubmittedDraftConsumption(sessionId, task) {
|
|
6730
|
+
if (task && currentRunView?.sessionId === sessionId
|
|
6731
|
+
&& currentRunView.runProjectId === getCurrentProjectId()
|
|
6732
|
+
&& currentRunView.runAccountScopeId === cachedAccountScopeId) {
|
|
6733
|
+
currentRunView.draftConsumption = { runId: currentRunView.recordId, task };
|
|
6734
|
+
}
|
|
6735
|
+
}
|
|
6736
|
+
|
|
6101
6737
|
function restoreRejectedLocalSkillSubmission(submission, checkpoint) {
|
|
6102
6738
|
const input = panel?.querySelector('[data-task]');
|
|
6103
6739
|
if (!checkpoint || !input || submission.accountScopeId !== cachedAccountScopeId
|
|
@@ -6115,6 +6751,7 @@
|
|
|
6115
6751
|
state = updateActiveSession(state, { task: submission.task, selectionContext: submission.selectionContext });
|
|
6116
6752
|
renderComposerSkillInvocation();
|
|
6117
6753
|
selectionContextView.render();
|
|
6754
|
+
syncProbeStatus();
|
|
6118
6755
|
autosizeTaskTextarea();
|
|
6119
6756
|
syncComposerSendAvailability();
|
|
6120
6757
|
saveStateSoon();
|
|
@@ -6122,11 +6759,15 @@
|
|
|
6122
6759
|
}
|
|
6123
6760
|
|
|
6124
6761
|
function applyStateToPanel(options = {}) {
|
|
6762
|
+
if (pendingDraftSave && pendingDraftSave.scope !== getComposerDraftScope()) resetDraftSave();
|
|
6763
|
+
contextUsageView?.close();
|
|
6125
6764
|
selectionContextView.render();
|
|
6126
6765
|
writingStyleSettings.sync();
|
|
6127
6766
|
applyPanelTheme(getThemePreference());
|
|
6128
6767
|
if ((state.providerId || 'builtin') === 'builtin' && !modelSelectHasOption(state.model)) {
|
|
6129
|
-
renderModelOptions(getModelCatalog().FALLBACK_MODELS, state.model
|
|
6768
|
+
renderModelOptions(getModelCatalog().FALLBACK_MODELS, state.model, {
|
|
6769
|
+
provisionalSelected: getModelDiscovery().provisional === true
|
|
6770
|
+
});
|
|
6130
6771
|
}
|
|
6131
6772
|
panel.querySelector('[data-model]').value = state.model;
|
|
6132
6773
|
const renderedModels = getRenderedModelEntries();
|
|
@@ -6144,6 +6785,7 @@
|
|
|
6144
6785
|
autosizeTaskTextarea();
|
|
6145
6786
|
syncComposerSendAvailability();
|
|
6146
6787
|
panel.querySelector('[data-require-reviewing]').checked = state.requireReviewing;
|
|
6788
|
+
syncReviewingTooltip();
|
|
6147
6789
|
const recompileCheckbox = panel?.querySelector('[data-auto-recompile]');
|
|
6148
6790
|
if (recompileCheckbox) {
|
|
6149
6791
|
recompileCheckbox.checked = state.autoRecompile !== false;
|
|
@@ -6198,6 +6840,47 @@
|
|
|
6198
6840
|
if (record) configureWritebackButton(root, record, { running });
|
|
6199
6841
|
});
|
|
6200
6842
|
renderPendingInputs();
|
|
6843
|
+
renderContextUsage();
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6846
|
+
function renderContextUsage() {
|
|
6847
|
+
if (!contextUsageView || !state) return;
|
|
6848
|
+
contextUsageView.setState(getContextUsageDisplay());
|
|
6849
|
+
}
|
|
6850
|
+
|
|
6851
|
+
function getContextUsageDisplay() {
|
|
6852
|
+
const session = getActiveSession(state);
|
|
6853
|
+
const run = currentRunView?.sessionId === session?.id
|
|
6854
|
+
? findRunRecord(currentRunView.recordId, currentRunView.sessionId) : null;
|
|
6855
|
+
const selection = providerSettingsCoordinator.getRunSelection(state.providerId);
|
|
6856
|
+
const runningSelection = run?.status === 'running' && run.model === state.model
|
|
6857
|
+
&& (run.providerId || 'builtin') === (state.providerId || 'builtin');
|
|
6858
|
+
const scope = {
|
|
6859
|
+
threadId: session?.codexThreadId || '',
|
|
6860
|
+
providerId: state.providerId || 'builtin',
|
|
6861
|
+
providerRevision: runningSelection ? Number(run.providerRevision || 0)
|
|
6862
|
+
: selection?.providerRevision ?? ((state.providerId || 'builtin') === 'builtin' ? 0 : null),
|
|
6863
|
+
model: state.model || ''
|
|
6864
|
+
};
|
|
6865
|
+
// The composer can select the next turn's model while this turn runs.
|
|
6866
|
+
// Only show statistics produced by the selected model and provider.
|
|
6867
|
+
const matchesRun = run && run.model === scope.model
|
|
6868
|
+
&& (run.providerId || 'builtin') === scope.providerId
|
|
6869
|
+
&& Number(run.providerRevision || 0) === scope.providerRevision;
|
|
6870
|
+
// Starting another turn must not revive statistics from before an automatic
|
|
6871
|
+
// compaction. Retain the watermark across this thread's saved normal runs.
|
|
6872
|
+
const compactedAt = (session?.runs || []).reduce((latest, candidate) => candidate && !candidate.forkSnapshot
|
|
6873
|
+
&& candidate.operation !== 'compact' && scope.threadId && candidate.codexThreadId === scope.threadId
|
|
6874
|
+
? Math.max(latest, Modules.RunActivityModel.getCompactedAt(candidate)) : latest, 0);
|
|
6875
|
+
const { usage, pending, compacted } = Modules.ContextUsage.status(session, { ...scope, compactedAt });
|
|
6876
|
+
const compacting = Boolean(matchesRun && Modules.RunActivityModel.project(run).compacting);
|
|
6877
|
+
return { usage, pending, compacted, compacting, compactingKind: compacting ? (run.operation === 'compact' ? 'manual' : 'auto') : '' };
|
|
6878
|
+
}
|
|
6879
|
+
|
|
6880
|
+
function getCompactAvailability() {
|
|
6881
|
+
if (sessionManager.isTransitioning() || updateIdleClient?.isApplying() || currentRunView || runSubmissionPending) return { ok: false, reason: 'compactBusy' };
|
|
6882
|
+
if (!getActiveSession(state)?.codexThreadId) return { ok: false, reason: 'compactNoThread' };
|
|
6883
|
+
return { ok: true };
|
|
6201
6884
|
}
|
|
6202
6885
|
|
|
6203
6886
|
function prepareRetryReplacement(run, task) {
|
|
@@ -6231,6 +6914,7 @@
|
|
|
6231
6914
|
sessionId = state.activeSessionId,
|
|
6232
6915
|
sessionContext = runController.captureSessionContext(state),
|
|
6233
6916
|
codexThreadId = '',
|
|
6917
|
+
operation,
|
|
6234
6918
|
task,
|
|
6235
6919
|
mode,
|
|
6236
6920
|
model,
|
|
@@ -6247,6 +6931,7 @@
|
|
|
6247
6931
|
resetAutoFollow();
|
|
6248
6932
|
const record = {
|
|
6249
6933
|
id: createRunId(),
|
|
6934
|
+
...(operation === 'compact' ? { operation } : {}),
|
|
6250
6935
|
task,
|
|
6251
6936
|
mode,
|
|
6252
6937
|
...providerSettingsCoordinator.getProviderSnapshot(executionSnapshot?.providerId || state?.providerId),
|
|
@@ -6276,12 +6961,20 @@
|
|
|
6276
6961
|
runAccountScopeId: cachedAccountScopeId || ''
|
|
6277
6962
|
};
|
|
6278
6963
|
const active = findSessionById(sessionId);
|
|
6279
|
-
const titlePatch = active?.titleSource !== 'manual'
|
|
6964
|
+
const titlePatch = operation !== 'compact' && active?.titleSource !== 'manual'
|
|
6280
6965
|
? {
|
|
6281
6966
|
title: deriveSessionTitle([], task),
|
|
6282
6967
|
titleSource: 'auto'
|
|
6283
6968
|
}
|
|
6284
6969
|
: {};
|
|
6970
|
+
// Establish the in-memory owner before publishing or rendering the record.
|
|
6971
|
+
// If presentation throws, runTask still has the identity needed to settle it.
|
|
6972
|
+
const view = currentRunView = {
|
|
6973
|
+
operation, sessionId, sessionContext, codexThreadId, recordId: record.id,
|
|
6974
|
+
runProjectId: record.runProjectId, runAccountScopeId: record.runAccountScopeId,
|
|
6975
|
+
executionSnapshot: record.executionSnapshot, startedAt: Date.now(), queueItemId,
|
|
6976
|
+
nativeRequestId: '', activeTurn: null
|
|
6977
|
+
};
|
|
6285
6978
|
updateSessionById(sessionId, {
|
|
6286
6979
|
runs: runController.replaceRunForRetry(active?.runs, replaceRunId, record),
|
|
6287
6980
|
...titlePatch
|
|
@@ -6290,6 +6983,16 @@
|
|
|
6290
6983
|
|
|
6291
6984
|
const log = panel.querySelector('[data-log]');
|
|
6292
6985
|
const root = renderRunCard(record);
|
|
6986
|
+
Object.assign(view, {
|
|
6987
|
+
root,
|
|
6988
|
+
runProcess: root.querySelector('[data-run-process]'),
|
|
6989
|
+
processLabel: root.querySelector('[data-run-process-summary]'),
|
|
6990
|
+
events: root.querySelector('[data-run-events]'),
|
|
6991
|
+
report: root.querySelector('[data-run-report]'),
|
|
6992
|
+
status: root.querySelector('[data-run-status]'),
|
|
6993
|
+
// Seed references before warm-mirror resolution produces its snapshot.
|
|
6994
|
+
projectFiles: getCurrentProjectReferenceFiles()
|
|
6995
|
+
});
|
|
6293
6996
|
if (state.activeSessionId === sessionId) {
|
|
6294
6997
|
removeEmptyRunsMessage(log);
|
|
6295
6998
|
if (replaceRunId) log.querySelector(`[data-run-id="${cssEscape(replaceRunId)}"]`)?.remove();
|
|
@@ -6300,32 +7003,7 @@
|
|
|
6300
7003
|
renderSessionList();
|
|
6301
7004
|
applySessionLabel();
|
|
6302
7005
|
|
|
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
|
-
};
|
|
7006
|
+
return view;
|
|
6329
7007
|
}
|
|
6330
7008
|
|
|
6331
7009
|
function touchSessionForTerminalRun(sessionId, finishedAt) {
|
|
@@ -6388,6 +7066,7 @@
|
|
|
6388
7066
|
].filter(Boolean).join(' · ');
|
|
6389
7067
|
collapseRunProcess(visibleView, statusText);
|
|
6390
7068
|
}
|
|
7069
|
+
renderContextUsage();
|
|
6391
7070
|
}
|
|
6392
7071
|
|
|
6393
7072
|
|
|
@@ -6402,6 +7081,11 @@
|
|
|
6402
7081
|
}
|
|
6403
7082
|
|
|
6404
7083
|
const owner = currentRunView && findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
7084
|
+
if (storeContextUsage(event, owner, findSessionById(currentRunView?.sessionId), journalSeq)) {
|
|
7085
|
+
scheduleRunStateSave('stream');
|
|
7086
|
+
renderContextUsage();
|
|
7087
|
+
return;
|
|
7088
|
+
}
|
|
6405
7089
|
if (owner && Modules.SubagentActivityModel.ingest(owner, event, {
|
|
6406
7090
|
locale: getLocale(), sanitize: sanitizeAssistantVisibleText, nativeEventSeq: journalSeq
|
|
6407
7091
|
})) {
|
|
@@ -6430,6 +7114,20 @@
|
|
|
6430
7114
|
replaceText: activity.replaceText,
|
|
6431
7115
|
nativeEventSeq: journalSeq
|
|
6432
7116
|
});
|
|
7117
|
+
if (activity.kind !== 'stream') renderContextUsage();
|
|
7118
|
+
}
|
|
7119
|
+
|
|
7120
|
+
function storeContextUsage(event, record, session, sequence) {
|
|
7121
|
+
if (!record) return false;
|
|
7122
|
+
const usage = Modules.ContextUsage.fromEvent(event, {
|
|
7123
|
+
...record, providerRevision: Number(record.providerRevision || 0), eventSeq: sequence
|
|
7124
|
+
});
|
|
7125
|
+
if (!usage || !Modules.ContextUsage.isNewer(usage, record.contextUsage)) return false;
|
|
7126
|
+
record.contextUsage = usage;
|
|
7127
|
+
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(sequence || 0));
|
|
7128
|
+
if (session?.codexThreadId === usage.threadId
|
|
7129
|
+
&& Modules.ContextUsage.isNewer(usage, session.contextUsage)) session.contextUsage = usage;
|
|
7130
|
+
return true;
|
|
6433
7131
|
}
|
|
6434
7132
|
|
|
6435
7133
|
function bindActiveTurn(detail = {}, journalSeq = 0) {
|
|
@@ -6438,16 +7136,24 @@
|
|
|
6438
7136
|
}
|
|
6439
7137
|
const threadId = String(detail.threadId || '');
|
|
6440
7138
|
const turnId = String(detail.turnId || '');
|
|
6441
|
-
currentRunView.activeTurn = { threadId, turnId };
|
|
7139
|
+
currentRunView.activeTurn = currentRunView.operation === 'compact' ? null : { threadId, turnId };
|
|
6442
7140
|
const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
6443
7141
|
if (record) {
|
|
6444
7142
|
record.codexThreadId = threadId; record.codexTurnId = turnId;
|
|
7143
|
+
if (record.operation === 'compact') {
|
|
7144
|
+
const session = findSessionById(currentRunView.sessionId);
|
|
7145
|
+
if (session?.contextUsage?.threadId === threadId
|
|
7146
|
+
&& Date.parse(session.contextUsage.observedAt) < Date.parse(record.startedAt)) {
|
|
7147
|
+
session.contextUsage = null;
|
|
7148
|
+
}
|
|
7149
|
+
}
|
|
6445
7150
|
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(journalSeq || 0));
|
|
6446
7151
|
}
|
|
6447
7152
|
if (threadId) {
|
|
6448
7153
|
updateSessionById(currentRunView.sessionId, { codexThreadId: threadId });
|
|
6449
7154
|
}
|
|
6450
7155
|
renderPendingInputs();
|
|
7156
|
+
renderContextUsage();
|
|
6451
7157
|
saveState().catch(() => {});
|
|
6452
7158
|
}
|
|
6453
7159
|
|
|
@@ -6468,7 +7174,7 @@
|
|
|
6468
7174
|
function appendRunEvent(input = {}) {
|
|
6469
7175
|
const { title, status = 'info', detail, timestamp } = input;
|
|
6470
7176
|
const safeTitle = sanitizeAssistantVisibleText(title) || 'Event';
|
|
6471
|
-
if (!currentRunView
|
|
7177
|
+
if (!currentRunView) {
|
|
6472
7178
|
appendPlainLog(safeTitle);
|
|
6473
7179
|
return;
|
|
6474
7180
|
}
|
|
@@ -6512,21 +7218,20 @@
|
|
|
6512
7218
|
})).filter(item => item.path)
|
|
6513
7219
|
: undefined
|
|
6514
7220
|
};
|
|
7221
|
+
// Keep the activity timestamp current without retaining a run reference
|
|
7222
|
+
// across session normalization, which replaces run and event objects.
|
|
7223
|
+
const nowMs = Date.now();
|
|
7224
|
+
if (nowMs - lastRunActivityBumpMs > 60000
|
|
7225
|
+
&& findRunRecord(currentRunView.recordId, currentRunView.sessionId)) {
|
|
7226
|
+
lastRunActivityBumpMs = nowMs;
|
|
7227
|
+
state = updateActiveSession(state, {});
|
|
7228
|
+
}
|
|
6515
7229
|
const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
6516
7230
|
let renderedEvent = event;
|
|
6517
7231
|
if (record) {
|
|
6518
7232
|
if (Number.isFinite(Number(input.nativeEventSeq))) {
|
|
6519
7233
|
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(input.nativeEventSeq || 0));
|
|
6520
7234
|
}
|
|
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
7235
|
renderedEvent = event.kind === 'stream'
|
|
6531
7236
|
? upsertRunStreamRecordEvent(record, event)
|
|
6532
7237
|
: event;
|
|
@@ -6922,7 +7627,7 @@
|
|
|
6922
7627
|
}
|
|
6923
7628
|
await Modules.ScopedPersistenceCoordinator.applyReviewTransition({
|
|
6924
7629
|
getState: () => state, runId, transition: WritebackSettlement.applySettlementTransition(run, settlement),
|
|
6925
|
-
persist: flushQueuedSaveState, assertCurrent: options.assertCurrent
|
|
7630
|
+
persist: flushQueuedSaveState, assertCurrent: options.assertCurrent, SessionState: Modules.SessionState
|
|
6926
7631
|
});
|
|
6927
7632
|
}
|
|
6928
7633
|
|
|
@@ -6939,6 +7644,7 @@
|
|
|
6939
7644
|
if (!run) {
|
|
6940
7645
|
return 0;
|
|
6941
7646
|
}
|
|
7647
|
+
run = Modules.UndoOperations.withAvailableUndo(run);
|
|
6942
7648
|
const trackedEditorUndoCount = hasTrackedEditorUndo(run) ? 1 : 0;
|
|
6943
7649
|
if (trackedEditorUndoCount) {
|
|
6944
7650
|
return trackedEditorUndoCount + Modules.UndoOperations.getFileUndoOperations(run).length;
|
|
@@ -6990,16 +7696,20 @@
|
|
|
6990
7696
|
assertCurrent();
|
|
6991
7697
|
}
|
|
6992
7698
|
|
|
6993
|
-
async function settleWritebackRecovery(applied, project, verification, target) {
|
|
7699
|
+
async function settleWritebackRecovery(applied, project, verification, target, requireReviewing) {
|
|
6994
7700
|
const record = findRunRecord(target.recordId, target.sessionId);
|
|
6995
7701
|
if (!record) return;
|
|
7702
|
+
// retryRequireReviewing is in-memory only; a renormalized run copy loses it.
|
|
7703
|
+
const retryReviewing = typeof requireReviewing === 'boolean' ? requireReviewing
|
|
7704
|
+
: typeof record.retryRequireReviewing === 'boolean' ? record.retryRequireReviewing
|
|
7705
|
+
: record.executionSnapshot?.requireReviewing === true;
|
|
6996
7706
|
const owner = recoveryOwner(target);
|
|
6997
7707
|
const skippedPaths = new Set((applied.skipped || []).map(entry => entry.operation?.path));
|
|
6998
7708
|
const creates = (record.retryWriteback?.operations || []).filter(op => skippedPaths.has(op.path));
|
|
6999
7709
|
// Untouched edits (never opened / rejected) and deletes whose server check failed are safe to replay.
|
|
7000
7710
|
const edits = (applied.skipped || []).filter(({ operation: op, result }) => result?.failure?.changedDocument === false && ((op?.type === 'edit'
|
|
7001
7711
|
&& (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:
|
|
7712
|
+
const base = { ...(record.retryWriteback || { ...owner, id: crypto.randomUUID() }), requireReviewing: retryReviewing,
|
|
7003
7713
|
baseFiles: project?.files || [], requestId: applied.receiptUnconfirmed ? applied.requestId || '' : '' };
|
|
7004
7714
|
// An oversized edit pre-image must not cost the create retries (the intent has a 300 KiB ceiling).
|
|
7005
7715
|
const safeRetry = [[...creates, ...edits], creates].filter(ops => ops.length).map(ops => Modules.SessionState
|
|
@@ -7033,31 +7743,46 @@
|
|
|
7033
7743
|
const originalStatus = record.status;
|
|
7034
7744
|
const retryAutoRecompile = typeof record.executionSnapshot?.autoRecompile === 'boolean'
|
|
7035
7745
|
? record.executionSnapshot.autoRecompile : undefined;
|
|
7746
|
+
const generation = spaRouteGeneration;
|
|
7747
|
+
const isOwnerScope = () => generation === spaRouteGeneration
|
|
7748
|
+
&& owner.projectId === getCurrentProjectId() && owner.accountScopeId === cachedAccountScopeId;
|
|
7036
7749
|
// State is renormalized during the awaits below (probes, session sync), which
|
|
7037
7750
|
// replaces run objects; always write through the current copy.
|
|
7038
|
-
const live = () => findRunRecord(runId, session.id) || record;
|
|
7751
|
+
const live = () => (isOwnerScope() && findRunRecord(runId, session.id)) || record;
|
|
7039
7752
|
let finished = false;
|
|
7753
|
+
let retryOutcome = null;
|
|
7040
7754
|
runCancellationRequested = false;
|
|
7041
7755
|
runCancellationController = new AbortController();
|
|
7042
|
-
record.
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
const root = panel.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
|
|
7046
|
-
currentRunView = { root, recordId: runId, sessionId: session.id, runProjectId: owner.projectId,
|
|
7756
|
+
const requestId = record.nativeRequestId || `writeback:${owner.sessionId}:${runId}`;
|
|
7757
|
+
const ownerToken = crypto.randomUUID();
|
|
7758
|
+
const retryView = { recordId: runId, sessionId: session.id, runProjectId: owner.projectId,
|
|
7047
7759
|
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]') };
|
|
7760
|
+
startedAt: Date.now(), writebackRetry: true };
|
|
7761
|
+
currentRunView = retryView;
|
|
7051
7762
|
setRunning(true);
|
|
7052
7763
|
const assertOwner = () => {
|
|
7053
|
-
if (runCancellationRequested || currentRunView
|
|
7764
|
+
if (runCancellationRequested || currentRunView !== retryView) {
|
|
7054
7765
|
throw Object.assign(new Error('Save checking was cancelled.'), { code: 'codex_cancelled' });
|
|
7055
7766
|
}
|
|
7056
|
-
if (
|
|
7767
|
+
if (!isOwnerScope()) {
|
|
7057
7768
|
throw Object.assign(new Error('The project or account changed.'), { code: 'aborted_project_changed' });
|
|
7058
7769
|
}
|
|
7059
7770
|
};
|
|
7060
7771
|
try {
|
|
7772
|
+
const bound = await activeTurnControl.bindBrowserRun({ requestId, projectKey: owner.projectId,
|
|
7773
|
+
clientRunId: runId, sessionId: session.id, ownerToken });
|
|
7774
|
+
assertOwner();
|
|
7775
|
+
if (!bound) throw new Error(tx('Could not start sync. Reload the page and try again.', '无法开始同步,请刷新页面后重试。'));
|
|
7776
|
+
// Publish running only after another page can observe this browser owner.
|
|
7777
|
+
live().status = 'running';
|
|
7778
|
+
live().retryingWriteback = true;
|
|
7779
|
+
renderRunHistory({ preserveScroll: true });
|
|
7780
|
+
const root = panel.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
|
|
7781
|
+
Object.assign(retryView, { root, events: root?.querySelector('[data-run-events]'),
|
|
7782
|
+
report: root?.querySelector('[data-run-report]'), status: root?.querySelector('[data-run-status]'),
|
|
7783
|
+
runProcess: root?.querySelector('[data-run-process]'), processLabel: root?.querySelector('[data-run-process-summary]') });
|
|
7784
|
+
const pendingMirror = getPendingMirrorRefresh?.();
|
|
7785
|
+
if (pendingMirror) await awaitRunStep(pendingMirror.catch(() => {}));
|
|
7061
7786
|
assertOwner();
|
|
7062
7787
|
if (intent?.requestId) {
|
|
7063
7788
|
const receipt = await callPageBridge('getWritebackReceipt', { requestId: intent.requestId, runProjectId: owner.projectId });
|
|
@@ -7067,9 +7792,12 @@
|
|
|
7067
7792
|
// Edits replay against the run's original base files, so the stale guard still refuses changed documents.
|
|
7068
7793
|
const result = await applySyncChangesToOverleaf(intent.operations.map(op => op.type === 'edit'
|
|
7069
7794
|
? { type: 'write', path: op.path, patches: op.patches, content: op.replaceAll } : op.type === 'delete' ? { type: 'delete', path: op.path } : { ...op, type: 'create' }),
|
|
7070
|
-
|
|
7795
|
+
// The submitted snapshot owns the write mode; older intents may carry a lost value.
|
|
7796
|
+
{ id: owner.projectId, files: intent.baseFiles }, { mode: 'auto', requireReviewing: typeof record.executionSnapshot?.requireReviewing === 'boolean'
|
|
7797
|
+
? record.executionSnapshot.requireReviewing : intent.requireReviewing,
|
|
7071
7798
|
autoRecompile: retryAutoRecompile,
|
|
7072
|
-
retryCreates: true, assistantMessage: tx('Retried the remaining file transfers for this task.', '已重试本轮未完成的文件同步。') });
|
|
7799
|
+
retryCreates: true, assistantMessageKey: 'retry_sync', assistantMessage: tx('Retried the remaining file transfers for this task.', '已重试本轮未完成的文件同步。') });
|
|
7800
|
+
retryOutcome = result;
|
|
7073
7801
|
assertOwner();
|
|
7074
7802
|
if (result.settlement?.facts) live().settlement = WritebackSettlement.mergeRetrySettlement(live().settlement, result.settlement.facts); // Keep files outside the retry.
|
|
7075
7803
|
await finishRunView(result.hasSkippedOperations ? tx('Sync incomplete', '同步未完成') : tx('Synced', '已同步'),
|
|
@@ -7090,15 +7818,36 @@
|
|
|
7090
7818
|
}
|
|
7091
7819
|
await flushQueuedSaveState();
|
|
7092
7820
|
} catch (error) {
|
|
7821
|
+
const outcome = error.writebackOutcome || retryOutcome;
|
|
7822
|
+
const current = findRunRecord(runId, session.id);
|
|
7823
|
+
if (outcome && (runCancellationRequested || isRunCancellationError(error))
|
|
7824
|
+
&& currentRunView === retryView && isOwnerScope()
|
|
7825
|
+
&& current && !current.forkSnapshot && current.runProjectId === owner.projectId) {
|
|
7826
|
+
// Stop cannot erase writes returned by Retry sync, including a saved
|
|
7827
|
+
// reply that arrived after cancellation. Keep earlier files' facts too.
|
|
7828
|
+
if (outcome.settlement?.facts) current.settlement = WritebackSettlement.mergeRetrySettlement(
|
|
7829
|
+
current.settlement, outcome.settlement.facts);
|
|
7830
|
+
appendRunCancelledReport(outcome, false);
|
|
7831
|
+
await finishRunView(tx('Cancelled', '已取消'), 'cancelled');
|
|
7832
|
+
finished = true;
|
|
7833
|
+
}
|
|
7093
7834
|
showPluginToast(error.message || String(error));
|
|
7094
7835
|
} finally {
|
|
7095
7836
|
// Only finishRunView settles a new terminal status; every other exit
|
|
7096
7837
|
// returns the card to the status it had before the retry.
|
|
7838
|
+
if (currentRunView !== retryView) {
|
|
7839
|
+
activeTurnControl.release(requestId, ownerToken);
|
|
7840
|
+
return;
|
|
7841
|
+
}
|
|
7097
7842
|
for (const copy of new Set([record, live()])) {
|
|
7098
7843
|
delete copy.retryingWriteback;
|
|
7099
7844
|
if (!finished && copy.status === 'running') copy.status = originalStatus;
|
|
7100
7845
|
}
|
|
7101
|
-
|
|
7846
|
+
saveStateSoon();
|
|
7847
|
+
await flushQueuedSaveState().catch(() => {});
|
|
7848
|
+
activeTurnControl.release(requestId, ownerToken);
|
|
7849
|
+
if (currentRunView !== retryView) return;
|
|
7850
|
+
currentRunView = null;
|
|
7102
7851
|
runCancellationRequested = false;
|
|
7103
7852
|
runCancellationController = null;
|
|
7104
7853
|
setRunning(false);
|
|
@@ -7206,11 +7955,18 @@
|
|
|
7206
7955
|
return;
|
|
7207
7956
|
}
|
|
7208
7957
|
|
|
7209
|
-
|
|
7958
|
+
let combinedAppliedOperations = [
|
|
7210
7959
|
...(Array.isArray(record.appliedOperations) ? record.appliedOperations : []),
|
|
7211
7960
|
...appliedOperations
|
|
7212
7961
|
];
|
|
7962
|
+
const checkpoint = buildUndoCheckpoint(project, combinedAppliedOperations);
|
|
7963
|
+
combinedAppliedOperations = checkpoint.appliedOperations;
|
|
7213
7964
|
record.appliedOperations = combinedAppliedOperations;
|
|
7965
|
+
if (checkpoint.undoUnavailable.length) {
|
|
7966
|
+
appendRunEvent({ title: tr('failureReason_snapshot_undo_unavailable_user'), status: 'warning',
|
|
7967
|
+
detail: checkpoint.undoUnavailable.map(item => ({ [tr('detailFile')]: item.path,
|
|
7968
|
+
[tr('detailReason')]: item.code, [tr('detailNext')]: tr('failureReason_snapshot_undo_unavailable_next') })) });
|
|
7969
|
+
}
|
|
7214
7970
|
const createdCheckpoint = buildCreatedFileUndoCheckpoint(project, combinedAppliedOperations, record);
|
|
7215
7971
|
|
|
7216
7972
|
if ((currentRunView?.executionSnapshot?.requireReviewing ?? state.requireReviewing) === true) {
|
|
@@ -7273,7 +8029,6 @@
|
|
|
7273
8029
|
return;
|
|
7274
8030
|
}
|
|
7275
8031
|
|
|
7276
|
-
const checkpoint = buildUndoCheckpoint(project, combinedAppliedOperations);
|
|
7277
8032
|
const createdPaths = new Set(createdCheckpoint.undoOperations.map(operation => operation.path));
|
|
7278
8033
|
checkpoint.undoOperations = [
|
|
7279
8034
|
...checkpoint.undoOperations.filter(operation => !createdPaths.has(operation.path)),
|
|
@@ -7283,10 +8038,7 @@
|
|
|
7283
8038
|
...checkpoint.undoBaseFiles.filter(file => !createdPaths.has(file.path)),
|
|
7284
8039
|
...createdCheckpoint.undoBaseFiles
|
|
7285
8040
|
];
|
|
7286
|
-
if (!checkpoint.undoOperations.length)
|
|
7287
|
-
return;
|
|
7288
|
-
}
|
|
7289
|
-
|
|
8041
|
+
if (!checkpoint.undoOperations.length && !checkpoint.undoUnavailable.length) return;
|
|
7290
8042
|
record.undoOperations = checkpoint.undoOperations;
|
|
7291
8043
|
record.undoBaseFiles = checkpoint.undoBaseFiles;
|
|
7292
8044
|
record.undoTrackedChanges = [];
|
|
@@ -7299,6 +8051,7 @@
|
|
|
7299
8051
|
record.undoStatus = '';
|
|
7300
8052
|
record.partialWriteback = skippedEntries.length > 0;
|
|
7301
8053
|
refreshRunCardControls(record.id);
|
|
8054
|
+
if (!checkpoint.undoOperations.length) return;
|
|
7302
8055
|
appendRunEvent({
|
|
7303
8056
|
title: tr('undoCheckpointPlain', { count: record.undoOperations.length }),
|
|
7304
8057
|
status: 'completed',
|
|
@@ -7361,6 +8114,7 @@
|
|
|
7361
8114
|
guidanceId: sanitizeAssistantVisibleText(event.guidanceId)
|
|
7362
8115
|
};
|
|
7363
8116
|
if (event.failure) normalized.failure = sanitizeAssistantVisibleValue(event.failure);
|
|
8117
|
+
if (event.detailStructured) normalized.detailStructured = sanitizeAssistantVisibleValue(event.detailStructured);
|
|
7364
8118
|
record.events = [...(record.events || []), normalized].slice(-MAX_RUN_EVENTS);
|
|
7365
8119
|
saveStateSoon();
|
|
7366
8120
|
|
|
@@ -7810,8 +8564,14 @@
|
|
|
7810
8564
|
return;
|
|
7811
8565
|
}
|
|
7812
8566
|
|
|
8567
|
+
const groupKey = String(options.groupKey || '');
|
|
8568
|
+
const grouped = groupKey && Array.from(region.children).find(item => item.dataset.groupKey === groupKey);
|
|
8569
|
+
if (grouped) {
|
|
8570
|
+
updateToastGroup(grouped);
|
|
8571
|
+
return;
|
|
8572
|
+
}
|
|
7813
8573
|
const last = region.lastElementChild;
|
|
7814
|
-
if (last?.classList?.contains('codex-toast') && last.dataset.baseText === value) {
|
|
8574
|
+
if (!groupKey && !last?.dataset.groupKey && last?.classList?.contains('codex-toast') && last.dataset.baseText === value) {
|
|
7815
8575
|
const repeatCount = Number(last.dataset.repeatCount || '1') + 1;
|
|
7816
8576
|
last.dataset.repeatCount = String(repeatCount);
|
|
7817
8577
|
const textNode = last.querySelector('[data-toast-text]');
|
|
@@ -7829,15 +8589,34 @@
|
|
|
7829
8589
|
|
|
7830
8590
|
const body = document.createElement('div');
|
|
7831
8591
|
body.className = 'codex-toast-body';
|
|
7832
|
-
|
|
7833
|
-
|
|
8592
|
+
if (options.title) {
|
|
8593
|
+
const title = document.createElement('div');
|
|
8594
|
+
title.className = 'codex-toast-title';
|
|
8595
|
+
title.textContent = options.title;
|
|
8596
|
+
body.append(title);
|
|
8597
|
+
}
|
|
8598
|
+
const textNode = document.createElement('div');
|
|
8599
|
+
textNode.dataset.toastText = 'true';
|
|
8600
|
+
textNode.textContent = value;
|
|
8601
|
+
body.append(textNode);
|
|
8602
|
+
if (groupKey) {
|
|
8603
|
+
item.dataset.groupKey = groupKey;
|
|
8604
|
+
const details = document.createElement('details');
|
|
8605
|
+
details.className = 'codex-toast-group';
|
|
8606
|
+
const summary = document.createElement('summary');
|
|
8607
|
+
const list = document.createElement('ul');
|
|
8608
|
+
list.dataset.toastGroupItems = 'true';
|
|
8609
|
+
details.append(summary, list);
|
|
8610
|
+
body.append(details);
|
|
8611
|
+
}
|
|
7834
8612
|
item.append(body);
|
|
8613
|
+
if (groupKey) updateToastGroup(item);
|
|
7835
8614
|
|
|
7836
8615
|
const close = document.createElement('button');
|
|
7837
8616
|
close.type = 'button';
|
|
7838
8617
|
close.className = 'codex-toast-close';
|
|
7839
8618
|
close.setAttribute('aria-label', tr('dismissNotification'));
|
|
7840
|
-
close.
|
|
8619
|
+
close.innerHTML = Modules.PanelIcons.svg('close', 'codex-close-glyph');
|
|
7841
8620
|
close.addEventListener('click', () => item.remove());
|
|
7842
8621
|
item.append(close);
|
|
7843
8622
|
|
|
@@ -7851,6 +8630,20 @@
|
|
|
7851
8630
|
item.remove();
|
|
7852
8631
|
}, 5200);
|
|
7853
8632
|
}
|
|
8633
|
+
|
|
8634
|
+
function updateToastGroup(toast) {
|
|
8635
|
+
const list = toast.querySelector('[data-toast-group-items]');
|
|
8636
|
+
const value = String(options.groupItem || '').trim();
|
|
8637
|
+
if (value && !Array.from(list.children).some(item => item.textContent === value)) {
|
|
8638
|
+
const item = document.createElement('li');
|
|
8639
|
+
const code = document.createElement('code');
|
|
8640
|
+
code.textContent = value;
|
|
8641
|
+
item.append(code);
|
|
8642
|
+
list.append(item);
|
|
8643
|
+
}
|
|
8644
|
+
toast.querySelector('.codex-toast-group summary').textContent =
|
|
8645
|
+
options.groupSummary?.(list.children.length) || String(list.children.length);
|
|
8646
|
+
}
|
|
7854
8647
|
}
|
|
7855
8648
|
|
|
7856
8649
|
function updateProbeNotice(text) {
|