codex-overleaf-link 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -392
- package/README.zh-CN.md +154 -393
- package/extension/bootstrap/background.js +52 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.html +1 -0
- package/extension/bootstrap/popupFallback.js +55 -0
- package/extension/bootstrap/updateStatus.js +1 -0
- package/extension/src/background.js +283 -55
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +129 -16
- package/extension/src/backgroundUpdateCoordinator.js +242 -29
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +171 -8
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +104 -47
- package/extension/src/content/composerPanel.js +110 -12
- package/extension/src/content/contentRuntime.js +1555 -926
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +48 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +11077 -3412
- package/extension/src/content/generated/content.bundle.meta.json +14 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +10 -0
- package/extension/src/content/localSkillsPanel.js +104 -14
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +47 -11
- package/extension/src/content/modelPicker.js +8 -4
- package/extension/src/content/nativeCompatibilityController.js +74 -30
- package/extension/src/content/otWarmMirror.js +5 -6
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +17 -4
- package/extension/src/content/panelMaintenance.js +46 -14
- package/extension/src/content/panelRenderer.js +57 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +67 -6
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +73 -14
- package/extension/src/content/recentProjects.js +15 -8
- package/extension/src/content/runActivitySummary.js +160 -18
- package/extension/src/content/runCompletionMeta.js +532 -0
- package/extension/src/content/runController.js +39 -15
- package/extension/src/content/runFailureNotice.js +29 -13
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +157 -47
- package/extension/src/content/runRecoveryActions.js +147 -0
- package/extension/src/content/runResultActions.js +28 -143
- package/extension/src/content/runReviewController.js +633 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +367 -175
- package/extension/src/content/scopedPersistencePanelState.js +134 -20
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +153 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +50 -4
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +22 -5
- package/extension/src/content/settingsWorkbench.js +132 -89
- package/extension/src/content/sharedSessionViewSync.js +49 -11
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +118 -12
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +116 -94
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/ownedDirectoryCleanup.js +94 -0
- package/extension/src/page/referenceProjects.js +1 -0
- package/extension/src/page/reviewContext.js +113 -0
- package/extension/src/page/saveState.js +6 -4
- package/extension/src/page/snapshotRouter.js +84 -39
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +114 -26
- package/extension/src/page/trackedChangeCapture.js +12 -2
- package/extension/src/page/trackedChangeOwnership.js +19 -1
- package/extension/src/page/trackedChangeReplay.js +41 -21
- package/extension/src/page/trackedChangesLifecycle.js +217 -199
- package/extension/src/page/treeOperations.js +7 -10
- package/extension/src/page/updateSafetyProbe.js +41 -0
- package/extension/src/page/writebackReceiptJournal.js +2 -0
- package/extension/src/page/writebackRouter.js +103 -155
- package/extension/src/pageBridge.js +126 -69
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +373 -17
- package/extension/src/shared/auditRecords.js +4 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +129 -66
- package/extension/src/shared/composerAttachmentDraft.js +156 -0
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +349 -4
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +590 -24
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/managedUpdateProjection.js +2 -2
- package/extension/src/shared/nativeRequestIdentity.js +1 -0
- package/extension/src/shared/pageRpcContract.js +5 -4
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- 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/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +132 -28
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +118 -27
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +166 -52
- package/extension/src/shared/storageDb.js +90 -26
- package/extension/src/shared/storageMigration.js +78 -112
- package/extension/src/shared/storageRunActions.js +267 -30
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +253 -32
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +17 -3
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +58 -40
- package/extension/styles/panel.css +1123 -493
- package/native-host/src/anthropicBridgeRequest.js +4 -2
- package/native-host/src/anthropicBridgeResponse.js +6 -2
- package/native-host/src/anthropicMessagesBridge.js +24 -16
- package/native-host/src/bootstrapMigration.js +296 -0
- package/native-host/src/bootstrapMigrationContract.js +101 -0
- package/native-host/src/bootstrapUpdate.js +269 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +20 -1
- package/native-host/src/chatCompletionsBridge.js +48 -15
- package/native-host/src/codexConfigPaths.js +106 -0
- package/native-host/src/codexHome.js +66 -73
- package/native-host/src/codexHomeScope.js +124 -0
- package/native-host/src/codexInheritedAuth.js +90 -0
- package/native-host/src/codexLocalSkills.js +301 -0
- package/native-host/src/codexPromptAssembly.js +114 -127
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +234 -198
- package/native-host/src/codexSessionTiming.js +18 -8
- package/native-host/src/codexSkillIsolation.js +170 -0
- package/native-host/src/codexThreadFork.js +19 -3
- package/native-host/src/index.js +19 -7
- package/native-host/src/managedInstall.js +110 -35
- package/native-host/src/managedInstallRecovery.js +140 -0
- package/native-host/src/managedRuntimeEvidence.js +57 -0
- package/native-host/src/mirrorWorkspace.js +92 -22
- package/native-host/src/nativeDoctor.js +15 -0
- package/native-host/src/nativeHelperLifecycle.js +21 -0
- package/native-host/src/nativeHostRegistration.js +33 -0
- package/native-host/src/nativeRunLifecycle.js +71 -0
- package/native-host/src/processStopBarrier.js +42 -7
- package/native-host/src/projectPathPlatform.js +30 -0
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerHttp.js +80 -0
- package/native-host/src/providerProfile.js +4 -2
- package/native-host/src/providerReasoning.js +2 -7
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/providerStore.js +72 -14
- package/native-host/src/responsesPassthroughBridge.js +266 -52
- package/native-host/src/runtimeInstaller.js +21 -1
- package/native-host/src/selectionScope.js +36 -8
- package/native-host/src/subagentAdmission.js +26 -0
- package/native-host/src/subagentBroker.js +79 -66
- package/native-host/src/subagentTelemetry.js +19 -10
- package/native-host/src/subagentWorkspacePath.js +5 -2
- package/native-host/src/taskRunnerRuntime.js +92 -57
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +409 -46
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +5 -8
- package/scripts/install-managed.mjs +1 -0
|
@@ -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;
|
|
@@ -56,7 +57,12 @@
|
|
|
56
57
|
'codex.history.clearPlugin',
|
|
57
58
|
'skills.list',
|
|
58
59
|
'skills.install',
|
|
59
|
-
'skills.remove'
|
|
60
|
+
'skills.remove',
|
|
61
|
+
'writing-style.get',
|
|
62
|
+
'writing-style.set',
|
|
63
|
+
'codex.thread.fork',
|
|
64
|
+
'asset.readChunk',
|
|
65
|
+
'asset.release'
|
|
60
66
|
]);
|
|
61
67
|
const PageRpcContract = Modules.PageRpcContract;
|
|
62
68
|
const PANEL_DEFAULT_WIDTH = 380;
|
|
@@ -64,7 +70,9 @@
|
|
|
64
70
|
const PANEL_MAX_WIDTH = 760;
|
|
65
71
|
const PAGE_MIN_WIDTH = 520;
|
|
66
72
|
const CodexOverleafCompatibility = Modules.Compatibility;
|
|
67
|
-
const
|
|
73
|
+
const MANAGED_INSTALLATION = chrome.runtime.getManifest?.()?.background?.service_worker === 'bootstrap/background.js'
|
|
74
|
+
|| root.__CODEX_OVERLEAF_RUNTIME_PREFIX__ === 'runtime/';
|
|
75
|
+
const INSTALL_COMMAND = MANAGED_INSTALLATION ? '' : CodexOverleafCompatibility?.buildInstallCommand?.(
|
|
68
76
|
undefined,
|
|
69
77
|
undefined,
|
|
70
78
|
getCurrentExtensionId()
|
|
@@ -91,7 +99,8 @@
|
|
|
91
99
|
recordSessionResult,
|
|
92
100
|
selectVisibleSessionsForList,
|
|
93
101
|
setActiveSession,
|
|
94
|
-
updateActiveSession
|
|
102
|
+
updateActiveSession,
|
|
103
|
+
updateActiveSessionDraft
|
|
95
104
|
} = Modules.SessionState;
|
|
96
105
|
const LineReferences = Modules.LineReferences;
|
|
97
106
|
const { getProjectStorageKey } = Modules.StorageKeys;
|
|
@@ -207,6 +216,8 @@
|
|
|
207
216
|
closeSlashMenu: () => closeSlashMenu(),
|
|
208
217
|
getCurrentProjectId: () => getCurrentProjectId(),
|
|
209
218
|
getLocalSkillsPanel: () => localSkillsPanel,
|
|
219
|
+
getCodexLocalSkills: () => getCodexLocalSkillsCatalog(),
|
|
220
|
+
onSkillsChange: () => updateSlashMenuForTaskInput(),
|
|
210
221
|
getLocale: () => getLocale(),
|
|
211
222
|
getPanel: () => panel,
|
|
212
223
|
getPanelRendererInstance: () => panelRendererInstance,
|
|
@@ -229,6 +240,9 @@
|
|
|
229
240
|
closeCustomInstructionsSettings,
|
|
230
241
|
closeSkillsView,
|
|
231
242
|
getCodexOverleafSkillEnabled,
|
|
243
|
+
getCodexLocalSkillEnabled,
|
|
244
|
+
isCodexLocalSkillEnabled,
|
|
245
|
+
setCodexLocalSkillEnabled,
|
|
232
246
|
getCustomInstructionsForCurrentProject,
|
|
233
247
|
getGovernanceRulesForCurrentProject,
|
|
234
248
|
getSkillLoadingSettings,
|
|
@@ -335,6 +349,7 @@
|
|
|
335
349
|
otWarmMirrorController,
|
|
336
350
|
getActiveFocusFiles,
|
|
337
351
|
getCurrentProjectId,
|
|
352
|
+
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
|
|
338
353
|
isExperimentalOtEnabled,
|
|
339
354
|
readOtBridgeStatus,
|
|
340
355
|
formatOtStatusLabel,
|
|
@@ -383,9 +398,17 @@
|
|
|
383
398
|
openCustomInstructionsSettings: () => openCustomInstructionsSettings(),
|
|
384
399
|
onHistoryRowJump: record => jumpToHistoryRun(record),
|
|
385
400
|
resetProjectNameCache: () => recentProjects.resetProjectNameCache(),
|
|
401
|
+
clearHistoryComposer: () => {
|
|
402
|
+
pendingRetryReplacement = null;
|
|
403
|
+
clearTaskComposer();
|
|
404
|
+
persistComposerAttachmentsToState();
|
|
405
|
+
},
|
|
386
406
|
getPanel: () => panel,
|
|
387
407
|
getState: () => state,
|
|
388
|
-
setState: next => {
|
|
408
|
+
setState: next => {
|
|
409
|
+
if (next.activeSessionId !== state?.activeSessionId) composerDraftEditedHere = false;
|
|
410
|
+
state = next;
|
|
411
|
+
},
|
|
389
412
|
getCurrentRunView: () => currentRunView,
|
|
390
413
|
getSettingsPanelInstance: () => settingsPanelInstance,
|
|
391
414
|
prepareRetryReplacement: (run, task) => prepareRetryReplacement(run, task)
|
|
@@ -406,30 +429,45 @@
|
|
|
406
429
|
const sessionForkController = Modules.SessionForkController.create({
|
|
407
430
|
SessionState: Modules.SessionState,
|
|
408
431
|
getState: () => state,
|
|
409
|
-
setState: next => {
|
|
410
|
-
|
|
411
|
-
|
|
432
|
+
setState: next => {
|
|
433
|
+
if (next.activeSessionId !== state?.activeSessionId) composerDraftEditedHere = false;
|
|
434
|
+
state = next;
|
|
435
|
+
},
|
|
436
|
+
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
|
|
437
|
+
sendBackgroundNative, saveState,
|
|
438
|
+
getEnabledCodexLocalSkillIds: () => getEnabledCodexLocalSkillIds(),
|
|
439
|
+
captureLocalSkillIdsForFork: async preferences => {
|
|
440
|
+
if (preferences.loadCodexLocalSkills === false) return [];
|
|
441
|
+
const choices = { ...preferences.codexLocalSkillEnabled };
|
|
442
|
+
const skills = await primeCodexLocalSkillsCatalog({ required: true });
|
|
443
|
+
return skills.filter(skill => Object.hasOwn(choices, skill.id) ? choices[skill.id] !== false : skill.enabled !== false).map(skill => skill.id);
|
|
444
|
+
},
|
|
412
445
|
applyStateToPanel,
|
|
413
446
|
showPluginToast,
|
|
414
447
|
tr
|
|
415
448
|
});
|
|
416
449
|
const runResultActions = Modules.RunResultActions.create({
|
|
417
450
|
tr,
|
|
418
|
-
getLocale,
|
|
419
451
|
forkRunFromNode: sessionForkController.forkRunFromNode
|
|
420
452
|
});
|
|
421
453
|
const runTimelineView = Modules.RunTimelineView.create({
|
|
422
454
|
RunActivitySummary: Modules.RunActivitySummary,
|
|
423
455
|
RunPresence: Modules.RunPresence,
|
|
456
|
+
PanelIcons: Modules.PanelIcons,
|
|
424
457
|
// recentProjects is composed below; the empty state only reads it after startup.
|
|
425
458
|
getProjectName: () => { try { return recentProjects.readCurrentProjectNameFromDom(); } catch { return ''; } },
|
|
426
|
-
getCurrentProjectId: () => getCurrentProjectId(),
|
|
427
459
|
RunActivityModel: Modules.RunActivityModel,
|
|
428
460
|
SubagentActivityView: Modules.SubagentActivityView,
|
|
429
461
|
RunGuidanceView: Modules.RunGuidanceView,
|
|
462
|
+
getPendingGuidance: run => runGuidanceController.getPending(run),
|
|
430
463
|
RunResultActions: runResultActions,
|
|
431
464
|
RunScrollLayout: Modules.RunScrollLayout,
|
|
432
465
|
RunFailureNotice: Modules.RunFailureNotice,
|
|
466
|
+
I18n: i18n,
|
|
467
|
+
FailureReasons,
|
|
468
|
+
projectContextLimitEvent: event => Modules.AgentTranscript.projectContextLimitEvent(event, getLocale()),
|
|
469
|
+
localizeVisibleReason: value => localizeVisibleReason(value),
|
|
470
|
+
localizeCompileReason: value => Modules.CompileAdapter.localizeCompileReason(value, getLocale()),
|
|
433
471
|
tr,
|
|
434
472
|
tx,
|
|
435
473
|
getLocale,
|
|
@@ -447,7 +485,8 @@
|
|
|
447
485
|
cssEscape,
|
|
448
486
|
renderMarkdownInlineText,
|
|
449
487
|
renderMarkdownBlockText,
|
|
450
|
-
decorateCompletionReport: Modules.MarkdownDomRenderer.decorateCompletionReport,
|
|
488
|
+
decorateCompletionReport: (container, options) => Modules.MarkdownDomRenderer.decorateCompletionReport(container,
|
|
489
|
+
{ ...options, formatOperationType }),
|
|
451
490
|
sanitizeAssistantVisibleText,
|
|
452
491
|
sanitizeAssistantVisibleValue,
|
|
453
492
|
buildMarkdownInlineNodes,
|
|
@@ -457,6 +496,8 @@
|
|
|
457
496
|
refillComposerForRetry,
|
|
458
497
|
canEditRun: run => runController.isRetryReplacementEligible(run, WritebackSettlement.projectRunSettlement(run)),
|
|
459
498
|
retryWriteback: retryRunWriteback,
|
|
499
|
+
getCurrentProjectId,
|
|
500
|
+
getAccountScopeId: () => cachedAccountScopeId,
|
|
460
501
|
showNativeSetupGuidance: () => showNativeUpdateGuidanceModal({}),
|
|
461
502
|
openProjectFileForFailure,
|
|
462
503
|
openStorageSettings,
|
|
@@ -489,6 +530,7 @@
|
|
|
489
530
|
SessionPanel: Modules.SessionPanel,
|
|
490
531
|
SessionState: Modules.SessionState,
|
|
491
532
|
SessionMenuView: Modules.SessionMenuView,
|
|
533
|
+
PanelIcons: Modules.PanelIcons,
|
|
492
534
|
tr,
|
|
493
535
|
showPluginConfirm,
|
|
494
536
|
showPluginToast,
|
|
@@ -496,10 +538,29 @@
|
|
|
496
538
|
saveState,
|
|
497
539
|
saveStateSoon,
|
|
498
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,
|
|
499
557
|
applyStateToPanel, syncSessionProvider: () => providerSettingsCoordinator.syncSessionProvider(),
|
|
500
558
|
getPanel: () => panel,
|
|
501
559
|
getState: () => state,
|
|
502
|
-
setState: next => {
|
|
560
|
+
setState: next => {
|
|
561
|
+
if (next.activeSessionId !== state?.activeSessionId) composerDraftEditedHere = false;
|
|
562
|
+
state = next;
|
|
563
|
+
},
|
|
503
564
|
getSessionPanelInstance: () => sessionPanelInstance,
|
|
504
565
|
formatSessionTime
|
|
505
566
|
});
|
|
@@ -563,6 +624,7 @@
|
|
|
563
624
|
selectionContext: value,
|
|
564
625
|
...(options.edit ? { mode: 'auto' } : {})
|
|
565
626
|
});
|
|
627
|
+
syncProbeStatus();
|
|
566
628
|
if (options.edit && panel) {
|
|
567
629
|
const modeInput = panel.querySelector('[data-mode]');
|
|
568
630
|
if (modeInput) modeInput.value = 'auto';
|
|
@@ -609,6 +671,14 @@
|
|
|
609
671
|
locale: getLocale()
|
|
610
672
|
};
|
|
611
673
|
},
|
|
674
|
+
preflightProjectReferences: ({ project, assertCurrent }) => {
|
|
675
|
+
assertCurrent();
|
|
676
|
+
return Modules.SensitivePreflight.create({
|
|
677
|
+
SensitiveScan, normalizeGovernanceRules, isTextSnapshotFile, getCurrentProjectId,
|
|
678
|
+
appendRunEvent: () => {}, appendCompletionReport: () => {},
|
|
679
|
+
showPluginConfirm, tr, tx
|
|
680
|
+
}).run({ task: '', project, rules: getGovernanceRulesForCurrentProject(), assertCurrent });
|
|
681
|
+
},
|
|
612
682
|
startBuild: async params => {
|
|
613
683
|
const gate = await nativeCompatibilityController.ensureForMethod('codex.run');
|
|
614
684
|
if (!gate.ok) throw new Error(gate.response?.error?.message || 'Native host update required.');
|
|
@@ -722,6 +792,7 @@
|
|
|
722
792
|
|| { providerId: state?.providerId || 'builtin', providerRevision: 0 },
|
|
723
793
|
readSelectedSpeedInput,
|
|
724
794
|
getRenderedModelEntries,
|
|
795
|
+
onSelectionRendered: renderContextUsage,
|
|
725
796
|
persistPanelInputs,
|
|
726
797
|
closeDiagnosticsMenu,
|
|
727
798
|
closeCustomInstructionsSettings,
|
|
@@ -765,8 +836,10 @@
|
|
|
765
836
|
window,
|
|
766
837
|
getSettingsPanelInstance: () => settingsPanelInstance,
|
|
767
838
|
getSelectedModel: () => state?.model || '', getSelectedProviderId: () => state?.providerId || 'builtin',
|
|
839
|
+
getSelectedProviderRevision: () => state?.providerRevision,
|
|
768
840
|
setSelectedProviderId: projectProviderSelection.commit,
|
|
769
841
|
confirmProviderSwitch: projectProviderSelection.confirmSwitch,
|
|
842
|
+
assertProviderSwitchAllowed: projectProviderSelection.assertSwitchAllowed,
|
|
770
843
|
refreshModelOptions: loadModelOptions, persistInputs: persistPanelInputs
|
|
771
844
|
});
|
|
772
845
|
// v1.8.1 D3: the dashboard was a one-shot render — another tab finishing a
|
|
@@ -802,8 +875,11 @@
|
|
|
802
875
|
document.addEventListener('visibilitychange', () => {
|
|
803
876
|
if (document.visibilityState === 'visible') {
|
|
804
877
|
refreshDashboardSoon();
|
|
878
|
+
} else {
|
|
879
|
+
flushComposerDraftOnLeave();
|
|
805
880
|
}
|
|
806
881
|
});
|
|
882
|
+
window.addEventListener('pagehide', flushComposerDraftOnLeave);
|
|
807
883
|
|
|
808
884
|
const recentProjects = Modules.RecentProjects.create({
|
|
809
885
|
ProjectSessionCleanup: Modules.ProjectSessionCleanup,
|
|
@@ -845,9 +921,11 @@
|
|
|
845
921
|
const MAX_ATTACHMENT_PREVIEW_DATA_URL_CHARS = 768 * 1024;
|
|
846
922
|
const composerAttachmentController = Modules.ComposerAttachments.createComposerAttachmentController({
|
|
847
923
|
getPanel: () => panel,
|
|
924
|
+
icons: Modules.PanelIcons,
|
|
848
925
|
tr,
|
|
849
926
|
tx,
|
|
850
927
|
appendPlainLog,
|
|
928
|
+
onChange: () => getComposerDraftPersistence().changed(),
|
|
851
929
|
limits: {
|
|
852
930
|
maxAttachmentBytes: MAX_COMPOSER_ATTACHMENT_BYTES,
|
|
853
931
|
maxAttachmentTotalBytes: MAX_COMPOSER_ATTACHMENT_TOTAL_BYTES,
|
|
@@ -874,11 +952,14 @@
|
|
|
874
952
|
});
|
|
875
953
|
const contextTrayController = ContextTray.createContextTrayController({
|
|
876
954
|
root: window,
|
|
955
|
+
icons: Modules.PanelIcons,
|
|
877
956
|
document,
|
|
878
957
|
getPanel: () => panel,
|
|
879
958
|
getState: () => state,
|
|
880
959
|
setState: nextState => {
|
|
881
960
|
state = nextState;
|
|
961
|
+
// Paint focus changes before ContextTray waits for durable persistence.
|
|
962
|
+
syncProbeStatus();
|
|
882
963
|
},
|
|
883
964
|
saveState,
|
|
884
965
|
saveStateSoon,
|
|
@@ -905,9 +986,15 @@
|
|
|
905
986
|
saveState,
|
|
906
987
|
sendBackgroundNative,
|
|
907
988
|
getCurrentProjectId,
|
|
989
|
+
getScope: () => getCodexLocalSkillsCatalogScope(),
|
|
908
990
|
getSkillLoadingSettings,
|
|
909
991
|
isCodexOverleafSkillEnabled,
|
|
910
992
|
setCodexOverleafSkillEnabled,
|
|
993
|
+
isCodexLocalSkillEnabled,
|
|
994
|
+
setCodexLocalSkillEnabled,
|
|
995
|
+
getCodexLocalSkills: () => getCodexLocalSkillsCatalog(),
|
|
996
|
+
getCodexLocalSkillWarnings: () => getCodexLocalSkillWarnings(),
|
|
997
|
+
refreshCodexLocalSkills: options => primeCodexLocalSkillsCatalog(options),
|
|
911
998
|
setProjectSettingsStatus,
|
|
912
999
|
tr,
|
|
913
1000
|
tx,
|
|
@@ -936,15 +1023,22 @@
|
|
|
936
1023
|
let settingsPanelInstance = null;
|
|
937
1024
|
let diagnosticsPanelInstance = null;
|
|
938
1025
|
let composerPanelInstance = null;
|
|
1026
|
+
let contextUsageView = null;
|
|
939
1027
|
let state = null;
|
|
940
1028
|
let storageKey = LEGACY_STORAGE_KEY;
|
|
941
1029
|
let globalPreferencesController = null;
|
|
942
1030
|
let currentRunView = null;
|
|
1031
|
+
let runSubmissionPending = false;
|
|
943
1032
|
let pendingRetryReplacement = null;
|
|
944
1033
|
let runQueueScheduler = null;
|
|
1034
|
+
let deferredSessionStart = null;
|
|
945
1035
|
let runInputCoordinator = null;
|
|
946
1036
|
const activeTurnControl = ActiveTurnControl.create({ chrome });
|
|
947
1037
|
let saveStateTimer = null;
|
|
1038
|
+
const DRAFT_SAVE_DELAY_MS = 800;
|
|
1039
|
+
const DRAFT_SAVE_MAX_WAIT_MS = 4000;
|
|
1040
|
+
let draftSaveTimer = null;
|
|
1041
|
+
let pendingDraftSave = null;
|
|
948
1042
|
// Two-phase saveState scheduling. `saveStateInFlight` flips to true while
|
|
949
1043
|
// an async saveState() is actually writing; `saveStateRunAfterFlight` is
|
|
950
1044
|
// set when a fresh saveStateSoon() fires during the in-flight phase. When
|
|
@@ -960,6 +1054,9 @@
|
|
|
960
1054
|
let pendingStreamRenderEvents = new Map();
|
|
961
1055
|
let storageNoticeKeys = new Set();
|
|
962
1056
|
let composerSkillInvocation = null;
|
|
1057
|
+
let composerDraftRevision = 0;
|
|
1058
|
+
// Page-local provenance only; the draft itself stays shared per conversation.
|
|
1059
|
+
let composerDraftEditedHere = false;
|
|
963
1060
|
let slashCodexOverleafSkills = [];
|
|
964
1061
|
let slashCodexOverleafSkillsLoaded = false;
|
|
965
1062
|
let slashCodexOverleafSkillsLoading = false;
|
|
@@ -982,6 +1079,7 @@
|
|
|
982
1079
|
showPluginToast,
|
|
983
1080
|
getPanel: () => panel,
|
|
984
1081
|
ensurePanelOpen,
|
|
1082
|
+
getIconUrl: () => chrome.runtime.getURL('assets/icons/codex-overleaf-dialog-icon.png'),
|
|
985
1083
|
getPanelRendererInstance: () => panelRendererInstance,
|
|
986
1084
|
setBadge: (...args) => PanelRenderer.setBadge(...args),
|
|
987
1085
|
setDiagnosticsHealth,
|
|
@@ -1000,7 +1098,7 @@
|
|
|
1000
1098
|
getBusyState: () => ({
|
|
1001
1099
|
run: Boolean(currentRunView),
|
|
1002
1100
|
cancelling: runCancellationRequested,
|
|
1003
|
-
storage: saveStateInFlight || Boolean(saveStateTimer),
|
|
1101
|
+
storage: saveStateInFlight || Boolean(saveStateTimer) || Boolean(pendingDraftSave),
|
|
1004
1102
|
reviewAction: trackedChangeInFlight.size > 0,
|
|
1005
1103
|
dialog: Boolean(pluginConfirmController?.isOpen())
|
|
1006
1104
|
}),
|
|
@@ -1017,6 +1115,7 @@
|
|
|
1017
1115
|
const diagnosticsExporter = Modules.DiagnosticsExport.create({
|
|
1018
1116
|
root, window, document, Blob, URL, TextEncoder, AuditRecords, StorageDb: Modules.StorageDb,
|
|
1019
1117
|
CodexOverleafCompatibility, getState: () => state, getCurrentRunView: () => currentRunView,
|
|
1118
|
+
getAccountScopeId: () => cachedAccountScopeId || '',
|
|
1020
1119
|
getCurrentProjectId, findRunRecord, getMirrorFreshness, getExtensionCompatibilityMetadata,
|
|
1021
1120
|
getModelDiscovery, getGovernanceRulesForCurrentProject, sendBackgroundNative,
|
|
1022
1121
|
fallbackNativeCompatibility, isNativeCompatibilityCompatible, getNativeCompatibilityClassification,
|
|
@@ -1029,7 +1128,7 @@
|
|
|
1029
1128
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
1030
1129
|
if (message?.type === 'codex-overleaf/runtime-health-probe') {
|
|
1031
1130
|
sendResponse?.({
|
|
1032
|
-
ok: true,
|
|
1131
|
+
ok: true, runOwnerProtocol: 2, runOwners: [activeTurnControl.getOwner()].filter(Boolean),
|
|
1033
1132
|
version: CodexOverleafCompatibility?.BUILD_TARGET_VERSION || chrome.runtime.getManifest().version
|
|
1034
1133
|
});
|
|
1035
1134
|
return false;
|
|
@@ -1094,6 +1193,8 @@
|
|
|
1094
1193
|
await refreshAccountScopeId();
|
|
1095
1194
|
storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
|
|
1096
1195
|
state = normalizeLoadedPanelState(await loadStoredState());
|
|
1196
|
+
composerDraftEditedHere = false;
|
|
1197
|
+
await getRunReviewController().recoverInterruptedUndoStatuses();
|
|
1097
1198
|
initializeRunQueueScheduler();
|
|
1098
1199
|
recoverInterruptedRunJournals()
|
|
1099
1200
|
.then(() => applyStateToPanel())
|
|
@@ -1165,7 +1266,8 @@
|
|
|
1165
1266
|
version: CodexOverleafCompatibility?.BUILD_TARGET_VERSION ||
|
|
1166
1267
|
chrome.runtime.getManifest?.().version ||
|
|
1167
1268
|
'',
|
|
1168
|
-
extensionId: getCurrentExtensionId()
|
|
1269
|
+
extensionId: getCurrentExtensionId(),
|
|
1270
|
+
installIdentity: { managed: MANAGED_INSTALLATION }
|
|
1169
1271
|
};
|
|
1170
1272
|
}
|
|
1171
1273
|
|
|
@@ -1197,7 +1299,6 @@
|
|
|
1197
1299
|
callbacks: {
|
|
1198
1300
|
onLauncherToggle: () => ensurePanelOpen(),
|
|
1199
1301
|
onClosePanel: () => closePanel(),
|
|
1200
|
-
onRefresh: () => refreshProbe({ userInitiated: true }),
|
|
1201
1302
|
onNewSession: () => startNewSession(),
|
|
1202
1303
|
onChangeHistory: () => openChangeHistory(),
|
|
1203
1304
|
onSettingsClick: () => toggleCustomInstructionsSettings(),
|
|
@@ -1219,7 +1320,9 @@
|
|
|
1219
1320
|
diagnosticsPanelInstance = DiagnosticsPanel.create({
|
|
1220
1321
|
container: panelRendererInstance.diagnosticsSlot,
|
|
1221
1322
|
i18n: { tr },
|
|
1323
|
+
icons: Modules.PanelIcons,
|
|
1222
1324
|
callbacks: {
|
|
1325
|
+
onRefresh: () => refreshProbe({ userInitiated: true }),
|
|
1223
1326
|
onBeforeOpen: () => {
|
|
1224
1327
|
closeModelConfigPopover();
|
|
1225
1328
|
closeContextTray();
|
|
@@ -1257,6 +1360,7 @@
|
|
|
1257
1360
|
projectId: getCurrentProjectId(),
|
|
1258
1361
|
i18n: { tr },
|
|
1259
1362
|
button: panel.querySelector('[data-custom-instructions-settings]'),
|
|
1363
|
+
getProjectName: () => { try { return recentProjects.readCurrentProjectNameFromDom(); } catch { return ''; } },
|
|
1260
1364
|
callbacks: {
|
|
1261
1365
|
onBack: () => closeCustomInstructionsSettings(),
|
|
1262
1366
|
onInputChange: event => persistPanelInputs(event),
|
|
@@ -1297,9 +1401,12 @@
|
|
|
1297
1401
|
composerPanelInstance = ComposerPanel.create({
|
|
1298
1402
|
container: panelRendererInstance.composerSlot,
|
|
1299
1403
|
i18n: { tr },
|
|
1404
|
+
icons: Modules.PanelIcons,
|
|
1300
1405
|
attachmentController: composerAttachmentController,
|
|
1301
1406
|
callbacks: {
|
|
1302
1407
|
onSubmit: () => safeRunTask(),
|
|
1408
|
+
getTaskScope: getComposerDraftScope,
|
|
1409
|
+
getTaskValue: () => state.task,
|
|
1303
1410
|
isRunning: () => Boolean(currentRunView),
|
|
1304
1411
|
onCancel: () => cancelActiveRun(),
|
|
1305
1412
|
onPaste: handleComposerPaste,
|
|
@@ -1309,12 +1416,13 @@
|
|
|
1309
1416
|
onTaskKeydown: handleTaskInputKeydown,
|
|
1310
1417
|
onTaskInput: handleTaskInput,
|
|
1311
1418
|
onSlashMenuClick: handleSlashMenuClick,
|
|
1419
|
+
onMenuLayoutChange: updateSlashMenuLayout,
|
|
1312
1420
|
onClearSkillInvocation: clearComposerSkillInvocation,
|
|
1313
1421
|
onAddContext: toggleContextTray,
|
|
1314
1422
|
onAttachClick: () => panel.querySelector('[data-attach-input]')?.click(),
|
|
1315
1423
|
onAttachInput: handleAttachInputChange,
|
|
1316
1424
|
onContextRefresh: () => loadContextFiles({ force: true }),
|
|
1317
|
-
onModelConfigToggle: () => toggleModelConfigPopover(),
|
|
1425
|
+
onModelConfigToggle: () => { contextUsageView?.close(); toggleModelConfigPopover(); },
|
|
1318
1426
|
onModelConfigChoiceClick: event => {
|
|
1319
1427
|
handleModelConfigChoiceClick(event).catch(error => {
|
|
1320
1428
|
appendPlainLog(tx(`Failed to update model settings: ${error.message}`, `更新模型设置失败:${error.message}`));
|
|
@@ -1327,6 +1435,18 @@
|
|
|
1327
1435
|
}
|
|
1328
1436
|
});
|
|
1329
1437
|
|
|
1438
|
+
contextUsageView = Modules.ContextUsageView.create({
|
|
1439
|
+
getPanel: () => panel, getLocale, tr,
|
|
1440
|
+
onOpen: () => {
|
|
1441
|
+
closeModelConfigPopover();
|
|
1442
|
+
closeContextTray();
|
|
1443
|
+
closeSlashMenu();
|
|
1444
|
+
},
|
|
1445
|
+
getCompactAvailability,
|
|
1446
|
+
level: Modules.ContextUsage.level,
|
|
1447
|
+
onCompact: () => safeRunTask({ operation: 'compact' })
|
|
1448
|
+
});
|
|
1449
|
+
renderContextUsage();
|
|
1330
1450
|
installContextDismiss();
|
|
1331
1451
|
installModelConfigDismiss();
|
|
1332
1452
|
bindLogAutoFollow();
|
|
@@ -1335,34 +1455,87 @@
|
|
|
1335
1455
|
renderContextSelection();
|
|
1336
1456
|
scheduleMirrorPrefetch({ reason: 'cold-start', delayMs: 2500 });
|
|
1337
1457
|
primeCodexOverleafSkillsCatalog().catch(() => { /* non-blocking */ });
|
|
1458
|
+
primeCodexLocalSkillsCatalog().catch(() => { /* retry through settings, slash, or submission */ });
|
|
1338
1459
|
}
|
|
1339
1460
|
|
|
1340
1461
|
PanelRenderer.setVisible(panel, true);
|
|
1341
1462
|
}
|
|
1342
1463
|
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
// the Skills page. Fire-and-forget on panel open; only fetches when the
|
|
1348
|
-
// catalog is empty.
|
|
1349
|
-
async function primeCodexOverleafSkillsCatalog() {
|
|
1464
|
+
// Panel prefetch and first submission share the same official catalog read.
|
|
1465
|
+
// A required read fails through the existing task-start error presentation.
|
|
1466
|
+
let codexOverleafSkillsCatalogPromise = null;
|
|
1467
|
+
async function primeCodexOverleafSkillsCatalog({ required = false } = {}) {
|
|
1350
1468
|
if (Array.isArray(state?.codexOverleafSkills) && state.codexOverleafSkills.length) {
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
if (getSkillLoadingSettings().loadCodexOverleafSkills === false)
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1469
|
+
return state.codexOverleafSkills;
|
|
1470
|
+
}
|
|
1471
|
+
if (!required && getSkillLoadingSettings().loadCodexOverleafSkills === false) return [];
|
|
1472
|
+
if (!codexOverleafSkillsCatalogPromise) {
|
|
1473
|
+
codexOverleafSkillsCatalogPromise = sendBackgroundNative({
|
|
1474
|
+
method: 'skills.list', params: { scope: 'codex-overleaf' }
|
|
1475
|
+
}).then(response => {
|
|
1476
|
+
const skills = Array.isArray(response?.result?.skills) ? response.result.skills : [];
|
|
1477
|
+
if (!response?.ok || !skills.length) throw new Error(tx(
|
|
1478
|
+
'Could not load Codex Overleaf skills. Try again.', '无法加载 Codex Overleaf 技能,请重试。'));
|
|
1479
|
+
state = { ...state, codexOverleafSkills: skills };
|
|
1480
|
+
saveStateSoon();
|
|
1481
|
+
return skills;
|
|
1482
|
+
}).finally(() => { codexOverleafSkillsCatalogPromise = null; });
|
|
1483
|
+
}
|
|
1484
|
+
return codexOverleafSkillsCatalogPromise;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Local metadata stays in memory. A stale page/account read cannot become the
|
|
1488
|
+
// next page's selected catalog, and an empty successful directory is valid.
|
|
1489
|
+
let codexLocalSkillsCatalog = null;
|
|
1490
|
+
let codexLocalSkillsWarnings = [];
|
|
1491
|
+
let codexLocalSkillsCatalogScope = '';
|
|
1492
|
+
let codexLocalSkillsCatalogRequest = null;
|
|
1493
|
+
function getCodexLocalSkillsCatalogScope() {
|
|
1494
|
+
return [cachedAccountScopeId, getCurrentProjectId(), spaRouteGeneration].join(':');
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function getCodexLocalSkillsCatalog() {
|
|
1498
|
+
return codexLocalSkillsCatalogScope === getCodexLocalSkillsCatalogScope() ? codexLocalSkillsCatalog : null;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function getCodexLocalSkillWarnings() {
|
|
1502
|
+
return codexLocalSkillsCatalogScope === getCodexLocalSkillsCatalogScope() ? codexLocalSkillsWarnings : [];
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
async function primeCodexLocalSkillsCatalog({ required = false, force = false } = {}) {
|
|
1506
|
+
const scope = getCodexLocalSkillsCatalogScope();
|
|
1507
|
+
if (!required && !force && getSkillLoadingSettings().loadCodexLocalSkills === false) return [];
|
|
1508
|
+
if (!force && codexLocalSkillsCatalogScope === scope && Array.isArray(codexLocalSkillsCatalog)) return codexLocalSkillsCatalog;
|
|
1509
|
+
if (codexLocalSkillsCatalogRequest?.scope === scope) return codexLocalSkillsCatalogRequest.promise;
|
|
1510
|
+
const request = { scope, promise: null };
|
|
1511
|
+
request.promise = sendBackgroundNative({ method: 'skills.list', params: { scope: 'codex-local' } }).then(response => {
|
|
1512
|
+
if (!response?.ok || !Array.isArray(response.result?.skills)) throw new Error(tx(
|
|
1513
|
+
'Could not load local Codex skills. Try again.', '无法加载 Codex 本地技能,请重试。'));
|
|
1514
|
+
if (scope !== getCodexLocalSkillsCatalogScope()) throw new Error(tx(
|
|
1515
|
+
'The active project changed while loading skills. Try again.', '加载技能时活动项目已改变,请重试。'));
|
|
1516
|
+
const seen = new Set();
|
|
1517
|
+
const skills = response.result.skills.filter(skill => {
|
|
1518
|
+
const id = String(skill?.id || '');
|
|
1519
|
+
if (!isSafeSkillId(id) || seen.has(id)) return false;
|
|
1520
|
+
seen.add(id); return true;
|
|
1521
|
+
});
|
|
1522
|
+
codexLocalSkillsCatalog = skills;
|
|
1523
|
+
codexLocalSkillsWarnings = (Array.isArray(response.result.discoveryWarnings) ? response.result.discoveryWarnings : [])
|
|
1524
|
+
.filter(warning => warning?.code === 'codex_local_skills_skipped' && Number.isSafeInteger(warning.count) && warning.count > 0)
|
|
1525
|
+
.map(warning => ({ code: warning.code, count: warning.count }));
|
|
1526
|
+
codexLocalSkillsCatalogScope = scope;
|
|
1527
|
+
updateSkillsEntrySummary();
|
|
1528
|
+
updateSlashMenuForTaskInput();
|
|
1529
|
+
return skills;
|
|
1530
|
+
}).catch(() => {
|
|
1531
|
+
if (scope !== getCodexLocalSkillsCatalogScope()) throw new Error(tx(
|
|
1532
|
+
'The active project changed while loading skills. Try again.', '加载技能时活动项目已改变,请重试。'));
|
|
1533
|
+
throw new Error(tx('Could not load local Codex skills. Try again.', '无法加载 Codex 本地技能,请重试。'));
|
|
1534
|
+
}).finally(() => {
|
|
1535
|
+
if (codexLocalSkillsCatalogRequest === request) codexLocalSkillsCatalogRequest = null;
|
|
1359
1536
|
});
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
state = { ...state, codexOverleafSkills: skills };
|
|
1365
|
-
saveStateSoon();
|
|
1537
|
+
codexLocalSkillsCatalogRequest = request;
|
|
1538
|
+
return request.promise;
|
|
1366
1539
|
}
|
|
1367
1540
|
|
|
1368
1541
|
function closePanel() {
|
|
@@ -1445,9 +1618,13 @@
|
|
|
1445
1618
|
panel.querySelectorAll('[data-i18n]').forEach(element => {
|
|
1446
1619
|
element.textContent = tr(element.dataset.i18n);
|
|
1447
1620
|
});
|
|
1621
|
+
panel.querySelectorAll('[data-i18n-aria-label]').forEach(element => {
|
|
1622
|
+
element.setAttribute('aria-label', tr(element.dataset.i18nAriaLabel));
|
|
1623
|
+
});
|
|
1624
|
+
panelRendererInstance?.setLocale?.({ launcher: tx('Show or hide Codex panel', '显示或隐藏 Codex 边栏'), close: tx('Close Codex panel', '关闭 Codex 边栏') });
|
|
1448
1625
|
|
|
1449
1626
|
setElementTitleAndAria('[data-panel-resize-handle]', tr('resizePanel'), tr('resizePanel'));
|
|
1450
|
-
|
|
1627
|
+
setRefreshProbeLoading(panel?.querySelector('[data-refresh]')?.dataset.loading === 'true');
|
|
1451
1628
|
// State-dependent tooltip: re-derive from the dot's current health so a
|
|
1452
1629
|
// locale/session refresh doesn't clobber it back to the generic label.
|
|
1453
1630
|
setDiagnosticsHealth(panel.querySelector('[data-diagnostics-health-dot]')?.dataset.health || 'unknown');
|
|
@@ -1498,7 +1675,7 @@
|
|
|
1498
1675
|
}
|
|
1499
1676
|
const reviewToggle = panel.querySelector('.codex-review-toggle');
|
|
1500
1677
|
if (reviewToggle) {
|
|
1501
|
-
|
|
1678
|
+
syncReviewingTooltip();
|
|
1502
1679
|
}
|
|
1503
1680
|
const recompileToggle = panel.querySelector('.codex-recompile-toggle');
|
|
1504
1681
|
if (recompileToggle) {
|
|
@@ -1514,18 +1691,32 @@
|
|
|
1514
1691
|
contextStatus.textContent = tr('contextStatus');
|
|
1515
1692
|
}
|
|
1516
1693
|
// Re-render rather than patch text: the first div is the animated mark.
|
|
1517
|
-
if (
|
|
1694
|
+
if (arguments[0]?.reprojectDynamic !== false) runTimelineView.refreshLocale?.();
|
|
1518
1695
|
|
|
1519
1696
|
syncModeControls();
|
|
1520
1697
|
updateOtStatusDisplay();
|
|
1521
1698
|
updateExperimentalOtMenuStatus();
|
|
1522
1699
|
renderModelConfigChoices();
|
|
1523
1700
|
updateModelDisplay();
|
|
1701
|
+
// The header switcher's title and accessible name carry localized text too.
|
|
1702
|
+
applySessionLabel();
|
|
1524
1703
|
renderSessionList();
|
|
1704
|
+
renderContextUsage();
|
|
1705
|
+
if (panel.querySelector('[data-slash-menu]')?.hidden === false) updateSlashMenuForTaskInput();
|
|
1525
1706
|
renderContextSelection();
|
|
1707
|
+
renderContextSummary();
|
|
1708
|
+
renderPendingInputs();
|
|
1709
|
+
renderComposerAttachments();
|
|
1710
|
+
renderComposerSkillInvocation();
|
|
1526
1711
|
writingStyleSettings.sync();
|
|
1527
1712
|
updateSkillsEntrySummary();
|
|
1713
|
+
localSkillsPanel.refreshLocale?.();
|
|
1528
1714
|
providerSettingsCoordinator.renderSummary();
|
|
1715
|
+
providerSettingsCoordinator.refreshLocale?.();
|
|
1716
|
+
settingsPanelInstance?.refreshLocale?.();
|
|
1717
|
+
Modules.UpdateNotice?.refreshLocale?.();
|
|
1718
|
+
DiagnosticsPanel.refreshLocale?.(diagnosticsPanelInstance, diagnosticsController.refreshLocale?.());
|
|
1719
|
+
if (!panel.querySelector('[data-context-tray]')?.hidden) renderContextFiles();
|
|
1529
1720
|
if (settingsPanelInstance?.container?.querySelector('[data-storage-card]')?.open) refreshStorageUsageSummary();
|
|
1530
1721
|
}
|
|
1531
1722
|
|
|
@@ -1685,47 +1876,81 @@
|
|
|
1685
1876
|
function resetContextProject() { return contextTrayController.resetContextProject(); }
|
|
1686
1877
|
|
|
1687
1878
|
async function runTask(options = {}) {
|
|
1688
|
-
//
|
|
1689
|
-
// turn (v1.7.5) BEFORE startRunView creates the new run view: the mirror's
|
|
1690
|
-
// wrap-up events must land in the run that produced them, not the new
|
|
1691
|
-
// one, and mirror.sync landing mid-run would swap the local baseline
|
|
1692
|
-
// under Codex.
|
|
1693
|
-
const pendingMirror = getPendingMirrorRefresh?.();
|
|
1694
|
-
if (pendingMirror) {
|
|
1695
|
-
await pendingMirror.catch(() => {});
|
|
1696
|
-
}
|
|
1879
|
+
// Capture the click before any preparation can yield to another draft/session.
|
|
1697
1880
|
readPanelInputs();
|
|
1698
1881
|
const queuedInput = options.queuedInput || null;
|
|
1699
|
-
const
|
|
1882
|
+
const compact = !queuedInput && options.operation === 'compact';
|
|
1883
|
+
const task = compact ? '/compact' : String(queuedInput?.text || state.task || '').trim();
|
|
1700
1884
|
if (!task) {
|
|
1701
1885
|
appendLog(tx('Enter a task first.', '请先输入任务。'));
|
|
1702
1886
|
return;
|
|
1703
1887
|
}
|
|
1888
|
+
const submittedSessionId = state.activeSessionId;
|
|
1889
|
+
const submittedSession = runController.captureSessionContext(state);
|
|
1890
|
+
const submittedThreadId = getActiveSession(state)?.codexThreadId || '';
|
|
1891
|
+
if (compact && !submittedThreadId) {
|
|
1892
|
+
showPluginToast(tr('compactNoThread'), { status: 'info' });
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
const submittedDraft = state.task;
|
|
1896
|
+
const submittedRetryReplacement = pendingRetryReplacement;
|
|
1704
1897
|
const submittedWritingStyleScope = { accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId() };
|
|
1705
1898
|
const submittedPanelState = {
|
|
1706
|
-
mode: state.mode,
|
|
1899
|
+
mode: compact ? 'ask' : state.mode,
|
|
1707
1900
|
providerId: state.providerId,
|
|
1708
1901
|
model: state.model,
|
|
1709
1902
|
reasoningEffort: state.reasoningEffort,
|
|
1710
1903
|
speedTier: state.speedTier,
|
|
1711
1904
|
autoRecompile: state.autoRecompile !== false,
|
|
1712
1905
|
requireReviewing: state.requireReviewing === true,
|
|
1713
|
-
focusFiles: getActiveFocusFiles(),
|
|
1714
|
-
selectionContext: selectionContextView.capture()
|
|
1906
|
+
focusFiles: compact ? [] : getActiveFocusFiles(),
|
|
1907
|
+
selectionContext: compact ? null : selectionContextView.capture()
|
|
1715
1908
|
};
|
|
1716
|
-
const
|
|
1717
|
-
|
|
1718
|
-
const
|
|
1719
|
-
const
|
|
1909
|
+
const queuedContext = queuedInput
|
|
1910
|
+
? RunInputQueue.normalizeSubmissionContext(queuedInput.payload?.submissionContext) : null;
|
|
1911
|
+
const submittedCustomInstructions = queuedContext ? queuedContext.customInstructions : getCustomInstructionsForCurrentProject();
|
|
1912
|
+
const submittedSkillLoadingSettings = queuedContext ? queuedContext.skillLoadingSettings : getSkillLoadingSettings();
|
|
1913
|
+
const submittedAttachments = queuedInput || compact ? [] : getComposerAttachmentsForRun();
|
|
1914
|
+
const submittedSkillInvocation = compact ? null : queuedContext ? queuedContext.skillInvocation : getComposerSkillInvocationForRun();
|
|
1915
|
+
let submittedEnabledSkillIds = queuedContext ? queuedContext.enabledCodexOverleafSkillIds : getEnabledCodexOverleafSkillIds();
|
|
1916
|
+
const submittedSkillEnabled = { ...state.codexOverleafSkillEnabled };
|
|
1917
|
+
const submittedLocalSkillEnabled = { ...state.codexLocalSkillEnabled };
|
|
1918
|
+
const submittedSelectionRevision = selectionContextView.getRevision?.();
|
|
1919
|
+
let submittedEnabledLocalSkillIds = queuedContext ? queuedContext.enabledCodexLocalSkillIds : compact ? getEnabledCodexLocalSkillIds() : [];
|
|
1920
|
+
// Capture submission first; a save must not adopt text typed while it waits.
|
|
1921
|
+
if (!queuedInput) await flushComposerDraft({ requireDurable: true });
|
|
1922
|
+
if (!compact && !queuedInput && submittedSkillLoadingSettings.loadCodexLocalSkills !== false) {
|
|
1923
|
+
const localSkills = await primeCodexLocalSkillsCatalog({ required: true, force: submittedSkillInvocation?.scope === 'codex-local' });
|
|
1924
|
+
submittedEnabledLocalSkillIds = localSkills.filter(skill => Object.hasOwn(submittedLocalSkillEnabled, skill.id)
|
|
1925
|
+
? submittedLocalSkillEnabled[skill.id] !== false : skill.enabled !== false).map(skill => skill.id);
|
|
1926
|
+
}
|
|
1927
|
+
if (!queuedInput && submittedSkillInvocation?.scope === 'codex-local'
|
|
1928
|
+
&& (submittedSkillLoadingSettings.loadCodexLocalSkills === false || !submittedEnabledLocalSkillIds.includes(submittedSkillInvocation.id))) {
|
|
1929
|
+
throw new Error(tx('The selected local Codex skill is unavailable or disabled. Select an enabled skill and try again.',
|
|
1930
|
+
'所选 Codex 本地技能不可用或已关闭,请选择已启用的技能后重试。'));
|
|
1931
|
+
}
|
|
1932
|
+
if (!compact && !queuedInput && submittedSkillLoadingSettings.loadCodexOverleafSkills !== false) {
|
|
1933
|
+
const skills = await primeCodexOverleafSkillsCatalog({ required: true });
|
|
1934
|
+
submittedEnabledSkillIds = skills.map(skill => String(skill?.id || '').trim())
|
|
1935
|
+
.filter(id => id && submittedSkillEnabled[id] !== false);
|
|
1936
|
+
}
|
|
1937
|
+
// Keep the previous mirror's completion events on its own run.
|
|
1938
|
+
const pendingMirror = getPendingMirrorRefresh?.();
|
|
1939
|
+
if (pendingMirror) await pendingMirror.catch(() => {});
|
|
1720
1940
|
await providerSettingsCoordinator.ensureLoaded();
|
|
1721
|
-
if (!queuedInput) await writingStyleSettings.ensureLoaded(true);
|
|
1941
|
+
if (!queuedInput && !compact) await writingStyleSettings.ensureLoaded(true);
|
|
1722
1942
|
if (submittedWritingStyleScope.accountScopeId !== cachedAccountScopeId
|
|
1723
1943
|
|| submittedWritingStyleScope.projectId !== getCurrentProjectId()) {
|
|
1724
1944
|
throw new Error(tx('The active project changed before submission. Submit the task again.',
|
|
1725
1945
|
'提交前活动项目已改变,请重新提交任务。'));
|
|
1726
1946
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1947
|
+
if (!findSessionById(submittedSessionId) || currentRunView
|
|
1948
|
+
|| (compact && findSessionById(submittedSessionId)?.codexThreadId !== submittedThreadId)) {
|
|
1949
|
+
throw new Error(tx('The submitted session changed before submission. Submit the task again.',
|
|
1950
|
+
'提交所属会话已改变,请重新提交任务。'));
|
|
1951
|
+
}
|
|
1952
|
+
submittedPanelState.writingStyle = queuedInput || compact ? null : writingStyleSettings.capture();
|
|
1953
|
+
let executionSnapshot = queuedInput
|
|
1729
1954
|
? RunExecutionSnapshot.resolveForExecution(
|
|
1730
1955
|
RunExecutionSnapshot.fromQueuePayload(queuedInput.payload),
|
|
1731
1956
|
{ catalog: providerSettingsCoordinator.getCatalog() }
|
|
@@ -1733,33 +1958,89 @@
|
|
|
1733
1958
|
: captureCurrentExecutionSnapshot(submittedPanelState);
|
|
1734
1959
|
// Freeze submitted run identity before the panel can change during the run.
|
|
1735
1960
|
const submittedMode = executionSnapshot.mode;
|
|
1736
|
-
|
|
1737
|
-
const replaceRunId = queuedInput ? '' : runController.resolveRetryReplacement({
|
|
1738
|
-
candidate:
|
|
1739
|
-
activeSessionId:
|
|
1740
|
-
runs:
|
|
1961
|
+
let submittedRequireReviewing = executionSnapshot.requireReviewing === true;
|
|
1962
|
+
const replaceRunId = queuedInput || compact ? '' : runController.resolveRetryReplacement({
|
|
1963
|
+
candidate: submittedRetryReplacement,
|
|
1964
|
+
activeSessionId: submittedSessionId,
|
|
1965
|
+
runs: findSessionById(submittedSessionId)?.runs,
|
|
1741
1966
|
projectRunSettlement: run => WritebackSettlement.projectRunSettlement(run)
|
|
1742
1967
|
});
|
|
1743
1968
|
|
|
1744
1969
|
runCancellationRequested = false;
|
|
1745
1970
|
runCancellationController = new AbortController();
|
|
1746
1971
|
setRunning(true);
|
|
1747
|
-
|
|
1748
|
-
task,
|
|
1749
|
-
mode: submittedMode,
|
|
1750
|
-
model: executionSnapshot.model,
|
|
1751
|
-
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
1752
|
-
speedTier: executionSnapshot.speedTier,
|
|
1753
|
-
executionSnapshot,
|
|
1754
|
-
attachments: submittedAttachments,
|
|
1755
|
-
skillInvocation: submittedSkillInvocation,
|
|
1756
|
-
queueItemId: queuedInput?.id || '',
|
|
1757
|
-
replaceRunId
|
|
1758
|
-
});
|
|
1759
|
-
if (!queuedInput) pendingRetryReplacement = null;
|
|
1760
|
-
const runSessionId = currentRunView.sessionId;
|
|
1972
|
+
const runSessionId = submittedSessionId;
|
|
1761
1973
|
let runAuditDraft = null;
|
|
1974
|
+
let localSkillRestorePoint = null;
|
|
1762
1975
|
try {
|
|
1976
|
+
currentRunView = startRunView({
|
|
1977
|
+
sessionId: submittedSessionId,
|
|
1978
|
+
sessionContext: submittedSession,
|
|
1979
|
+
codexThreadId: submittedThreadId,
|
|
1980
|
+
operation: compact ? 'compact' : undefined,
|
|
1981
|
+
task,
|
|
1982
|
+
mode: submittedMode,
|
|
1983
|
+
model: executionSnapshot.model,
|
|
1984
|
+
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
1985
|
+
speedTier: executionSnapshot.speedTier,
|
|
1986
|
+
executionSnapshot,
|
|
1987
|
+
attachments: submittedAttachments,
|
|
1988
|
+
skillInvocation: submittedSkillInvocation,
|
|
1989
|
+
queueItemId: queuedInput?.id || '',
|
|
1990
|
+
replaceRunId
|
|
1991
|
+
});
|
|
1992
|
+
// Preparation can await project/audit work before a Native request exists.
|
|
1993
|
+
// Give other pages a live owner to check throughout that interval.
|
|
1994
|
+
const bound = await awaitRunStep(activeTurnControl.bindBrowserRun({
|
|
1995
|
+
requestId: ActiveTurnControl.getStartupRequestId(runSessionId, currentRunView.recordId),
|
|
1996
|
+
projectKey: currentRunView.runProjectId, clientRunId: currentRunView.recordId,
|
|
1997
|
+
sessionId: runSessionId, ownerToken: crypto.randomUUID()
|
|
1998
|
+
}));
|
|
1999
|
+
throwIfRunCancellationRequested();
|
|
2000
|
+
if (!bound) throw new Error(tx('Could not start the task. Reload the page and try again.',
|
|
2001
|
+
'无法启动任务,请刷新页面后重试。'));
|
|
2002
|
+
renderContextUsage();
|
|
2003
|
+
if (!queuedInput && !compact && pendingRetryReplacement === submittedRetryReplacement) pendingRetryReplacement = null;
|
|
2004
|
+
if (compact) {
|
|
2005
|
+
announceCrossTabRunStart();
|
|
2006
|
+
// Only the slash-command text is consumed; a draft kept while compacting from the popover stays.
|
|
2007
|
+
if (options.consumeCompactDraft === true && state.activeSessionId === submittedSessionId
|
|
2008
|
+
&& /^\/[^\s/]*$/.test(String(submittedDraft || '').trim())
|
|
2009
|
+
&& panel?.querySelector('[data-task]')?.value === submittedDraft) {
|
|
2010
|
+
panel.querySelector('[data-task]').value = '';
|
|
2011
|
+
updateSessionById(submittedSessionId, { task: '' });
|
|
2012
|
+
recordSubmittedDraftConsumption(submittedSessionId, submittedDraft);
|
|
2013
|
+
composerDraftEditedHere = false;
|
|
2014
|
+
composerDraftRevision += 1;
|
|
2015
|
+
autosizeTaskTextarea();
|
|
2016
|
+
closeSlashMenu();
|
|
2017
|
+
syncComposerSendAvailability(true);
|
|
2018
|
+
}
|
|
2019
|
+
appendRunEvent({ title: tr('contextUsageCompacting'), status: 'running',
|
|
2020
|
+
activity: { v: 1, kind: 'lifecycle', state: 'running', target: 'context_compaction' } });
|
|
2021
|
+
renderContextUsage();
|
|
2022
|
+
const response = await awaitRunStep(sendTrackedCodexRun({
|
|
2023
|
+
operation: 'compact', task: '/compact', mode: 'ask',
|
|
2024
|
+
projectId: currentRunView.runProjectId, accountScopeId: currentRunView.runAccountScopeId,
|
|
2025
|
+
clientRunId: currentRunView.recordId, clientSessionId: runSessionId,
|
|
2026
|
+
threadId: submittedThreadId, model: executionSnapshot.model,
|
|
2027
|
+
reasoningEffort: executionSnapshot.reasoningEffort, speedTier: executionSnapshot.speedTier,
|
|
2028
|
+
providerSelection: RunExecutionSnapshot.toProviderSelection(executionSnapshot),
|
|
2029
|
+
locale: getLocale(), ...submittedSkillLoadingSettings,
|
|
2030
|
+
enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
|
|
2031
|
+
enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds
|
|
2032
|
+
}));
|
|
2033
|
+
if (!response?.ok) throw Object.assign(new Error(response?.error?.message || 'Compaction failed'), response?.error);
|
|
2034
|
+
if (response.result?.operation !== 'compact' || response.result?.status !== 'completed'
|
|
2035
|
+
|| response.result?.threadId !== submittedThreadId) {
|
|
2036
|
+
throw Object.assign(new Error('Compaction completion was not confirmed'), { code: 'codex_compact_unconfirmed' });
|
|
2037
|
+
}
|
|
2038
|
+
throwIfRunCancellationRequested();
|
|
2039
|
+
appendCompletionReport({ conclusion: tr('compactCompleted'), conclusionKey: 'context_compacted',
|
|
2040
|
+
status: 'completed', mode: 'ask', operations: [], applyResults: [] });
|
|
2041
|
+
await finishRunView(tr('compactCompleted'), 'completed');
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
1763
2044
|
if (queuedInput?.id) {
|
|
1764
2045
|
await runQueueScheduler?.markExecuting(runSessionId, queuedInput.id, currentRunView.recordId);
|
|
1765
2046
|
}
|
|
@@ -1770,14 +2051,32 @@
|
|
|
1770
2051
|
focusFiles: executionSnapshot.focusFiles,
|
|
1771
2052
|
executionSnapshot
|
|
1772
2053
|
}));
|
|
2054
|
+
if (submittedWritingStyleScope.accountScopeId !== cachedAccountScopeId
|
|
2055
|
+
|| submittedWritingStyleScope.projectId !== getCurrentProjectId()) {
|
|
2056
|
+
throw Object.assign(createRunCancellationError(), {
|
|
2057
|
+
message: 'The active project or account changed before dispatch.'
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
1773
2060
|
announceCrossTabRunStart();
|
|
1774
2061
|
// Binary attachments belong to this submitted turn. The run record has
|
|
1775
2062
|
// already captured their snapshots, so clear them from the composer
|
|
1776
2063
|
// together with the submitted text. Persistent @context remains intact.
|
|
1777
|
-
clearTaskComposer({ keepAttachments: false
|
|
2064
|
+
const submittedComposerCleared = !queuedInput && clearTaskComposer({ keepAttachments: false, submission: {
|
|
2065
|
+
...submittedWritingStyleScope,
|
|
2066
|
+
sessionId: submittedSessionId, task: submittedDraft, attachments: submittedAttachments,
|
|
2067
|
+
skillInvocation: submittedSkillInvocation, selectionContext: submittedPanelState.selectionContext,
|
|
2068
|
+
selectionRevision: submittedSelectionRevision
|
|
2069
|
+
} });
|
|
2070
|
+
if (submittedComposerCleared && submittedSkillInvocation?.scope === 'codex-local'
|
|
2071
|
+
&& state.activeSessionId === submittedSessionId && !panel?.querySelector('[data-task]')?.value
|
|
2072
|
+
&& !getComposerAttachmentsForRun().length && !composerAttachmentController.getAttachmentByteUsage?.().pending
|
|
2073
|
+
&& !getComposerSkillInvocationForRun() && !selectionContextView.capture()) {
|
|
2074
|
+
localSkillRestorePoint = { draftRevision: composerDraftRevision,
|
|
2075
|
+
attachmentRevision: composerAttachmentController.getRevision?.(), selectionRevision: selectionContextView.getRevision?.() };
|
|
2076
|
+
}
|
|
1778
2077
|
syncComposerSendAvailability(true);
|
|
1779
2078
|
appendRunEvent({
|
|
1780
|
-
title: submittedSkillInvocation?.id === 'skill-installer'
|
|
2079
|
+
title: submittedSkillInvocation?.id === 'skill-installer' && !submittedSkillInvocation.scope
|
|
1781
2080
|
? tx('I will use the Codex skill installer for this request.', '我会用 Codex skill installer 处理这个请求。')
|
|
1782
2081
|
: tx('I will first understand your request, then inspect the relevant Overleaf files.', '我会先理解你的请求,再检查相关 Overleaf 文件。'),
|
|
1783
2082
|
status: 'running',
|
|
@@ -1792,7 +2091,7 @@
|
|
|
1792
2091
|
'@context': formatContextItems(executionSnapshot.focusFiles)
|
|
1793
2092
|
}
|
|
1794
2093
|
});
|
|
1795
|
-
if (submittedSkillInvocation?.id === 'skill-installer') {
|
|
2094
|
+
if (submittedSkillInvocation?.id === 'skill-installer' && !submittedSkillInvocation.scope) {
|
|
1796
2095
|
await awaitRunStep(runSkillInstallerTask({
|
|
1797
2096
|
task,
|
|
1798
2097
|
runSessionId,
|
|
@@ -1800,6 +2099,8 @@
|
|
|
1800
2099
|
submittedMode,
|
|
1801
2100
|
submittedCustomInstructions,
|
|
1802
2101
|
submittedSkillLoadingSettings,
|
|
2102
|
+
submittedEnabledLocalSkillIds,
|
|
2103
|
+
submittedEnabledSkillIds,
|
|
1803
2104
|
submittedAttachments,
|
|
1804
2105
|
submittedSkillInvocation,
|
|
1805
2106
|
executionSnapshot
|
|
@@ -1818,7 +2119,30 @@
|
|
|
1818
2119
|
await awaitRunStep(pauseOtWarmMirror('run-start'));
|
|
1819
2120
|
const writeSafety = await awaitRunStep(preflightWriteSafety({
|
|
1820
2121
|
mode: submittedMode,
|
|
1821
|
-
requireReviewing: submittedRequireReviewing
|
|
2122
|
+
requireReviewing: submittedRequireReviewing,
|
|
2123
|
+
onUntracked: async choice => {
|
|
2124
|
+
submittedRequireReviewing = false;
|
|
2125
|
+
// Keep the effective policy identical in dispatch, recovery and Undo consumers.
|
|
2126
|
+
const view = currentRunView;
|
|
2127
|
+
const record = findRunRecord(view.recordId, view.sessionId);
|
|
2128
|
+
executionSnapshot = RunExecutionSnapshot.applyUntrackedDecision(view, record, choice);
|
|
2129
|
+
if (choice === 'disable') {
|
|
2130
|
+
updateSessionById(view.sessionId, { requireReviewing: false });
|
|
2131
|
+
if (state.activeSessionId === view.sessionId) {
|
|
2132
|
+
state.requireReviewing = false;
|
|
2133
|
+
panel.querySelector('[data-require-reviewing]').checked = false;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
appendRunEvent({ title: tr('reviewingUnavailableRunUntracked'), status: 'completed',
|
|
2137
|
+
technicalDetail: { code: 'reviewing_unavailable', submittedRequireReviewing: true,
|
|
2138
|
+
effectiveRequireReviewing: false, choice } });
|
|
2139
|
+
saveStateSoon(0);
|
|
2140
|
+
syncProbeStatus();
|
|
2141
|
+
runAuditDraft = await finalizeAuditDraftPartial(runAuditDraft, {
|
|
2142
|
+
requireReviewing: false, trackingDecision: choice
|
|
2143
|
+
});
|
|
2144
|
+
await flushQueuedSaveState({ requireDurable: true });
|
|
2145
|
+
}
|
|
1822
2146
|
}));
|
|
1823
2147
|
if (!writeSafety.ok) {
|
|
1824
2148
|
await finalizeAuditRecord(runAuditDraft, {
|
|
@@ -1860,6 +2184,7 @@
|
|
|
1860
2184
|
project,
|
|
1861
2185
|
snapshotWarnings,
|
|
1862
2186
|
focusFiles,
|
|
2187
|
+
focusFilesAreLiteral: true,
|
|
1863
2188
|
isUsableProjectFileContent: Modules.ProjectFiles.isUsableProjectFileContent
|
|
1864
2189
|
});
|
|
1865
2190
|
let restrictToFocusFiles = runController.shouldRestrictWritebackToFocus({ focusFiles, focusedPartialSnapshot });
|
|
@@ -1879,7 +2204,7 @@
|
|
|
1879
2204
|
fileCount: (project?.files || []).length
|
|
1880
2205
|
}
|
|
1881
2206
|
});
|
|
1882
|
-
appendCompletionReport({
|
|
2207
|
+
appendCompletionReport({ conclusionKey: 'full_project_unread',
|
|
1883
2208
|
conclusion: tx('This run did not continue: the full Overleaf project was not read.', '这轮没有继续:没有读到完整的 Overleaf 项目内容。'),
|
|
1884
2209
|
status: 'blocked',
|
|
1885
2210
|
operations: [],
|
|
@@ -1928,8 +2253,7 @@
|
|
|
1928
2253
|
}
|
|
1929
2254
|
}
|
|
1930
2255
|
|
|
1931
|
-
const
|
|
1932
|
-
const codexThreadId = activeSession?.codexThreadId || '';
|
|
2256
|
+
const codexThreadId = submittedThreadId;
|
|
1933
2257
|
|
|
1934
2258
|
// Resolve @compile-log if mentioned in task
|
|
1935
2259
|
let compileLogContext = null;
|
|
@@ -1957,14 +2281,14 @@
|
|
|
1957
2281
|
}
|
|
1958
2282
|
});
|
|
1959
2283
|
appendRunEvent({
|
|
1960
|
-
title: tx(`Compile log unavailable: ${compileLogContext.reason}`, `编译日志不可用:${compileLogContext.reason}`),
|
|
2284
|
+
title: tx(`Compile log unavailable: ${Modules.CompileAdapter.localizeCompileReason(compileLogContext.reason, getLocale())}`, `编译日志不可用:${Modules.CompileAdapter.localizeCompileReason(compileLogContext.reason, getLocale())}`),
|
|
1961
2285
|
status: 'failed',
|
|
1962
2286
|
failure: contextFailure
|
|
1963
2287
|
});
|
|
1964
2288
|
}
|
|
1965
2289
|
}
|
|
1966
2290
|
|
|
1967
|
-
|
|
2291
|
+
let sensitiveFindings = await awaitRunStep(runSensitivePreflight({
|
|
1968
2292
|
task,
|
|
1969
2293
|
project,
|
|
1970
2294
|
rules: getGovernanceRulesForCurrentProject(),
|
|
@@ -2001,6 +2325,8 @@
|
|
|
2001
2325
|
compileLogContext,
|
|
2002
2326
|
customInstructions: submittedCustomInstructions,
|
|
2003
2327
|
skillLoadingSettings: submittedSkillLoadingSettings,
|
|
2328
|
+
enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
|
|
2329
|
+
enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds,
|
|
2004
2330
|
attachments: submittedAttachments,
|
|
2005
2331
|
skillInvocation: submittedSkillInvocation,
|
|
2006
2332
|
submittedMode
|
|
@@ -2022,6 +2348,25 @@
|
|
|
2022
2348
|
fileOverlays = null;
|
|
2023
2349
|
otWarmStart = false;
|
|
2024
2350
|
restrictToFocusFiles = staleRetry.restrictToFocusFiles;
|
|
2351
|
+
const retryFindings = await awaitRunStep(runSensitivePreflight({
|
|
2352
|
+
task, project, rules: getGovernanceRulesForCurrentProject(), useExistingMirror: false
|
|
2353
|
+
}));
|
|
2354
|
+
if (retryFindings.blocked) {
|
|
2355
|
+
await finalizeAuditRecord(runAuditDraft, {
|
|
2356
|
+
resultStatus: 'blocked_sensitive', sensitiveFindings: retryFindings.findings,
|
|
2357
|
+
blockedFiles: retryFindings.findings.map(finding => ({
|
|
2358
|
+
path: finding.path || finding.source || 'task', reason: finding.detectorId || 'sensitive'
|
|
2359
|
+
}))
|
|
2360
|
+
});
|
|
2361
|
+
await finishRunView(tx('Blocked: sensitive content review required', '已阻止:需要处理敏感内容'), 'failed');
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
if (retryFindings.findings.length) {
|
|
2365
|
+
sensitiveFindings = {
|
|
2366
|
+
blocked: false, findings: [...sensitiveFindings.findings, ...retryFindings.findings]
|
|
2367
|
+
};
|
|
2368
|
+
runAuditDraft = await updateAuditSensitiveFindings(runAuditDraft, sensitiveFindings.findings);
|
|
2369
|
+
}
|
|
2025
2370
|
response = await awaitRunStep(sendTrackedCodexRun(buildCodexRunParams({
|
|
2026
2371
|
task,
|
|
2027
2372
|
project,
|
|
@@ -2034,6 +2379,8 @@
|
|
|
2034
2379
|
compileLogContext,
|
|
2035
2380
|
customInstructions: submittedCustomInstructions,
|
|
2036
2381
|
skillLoadingSettings: submittedSkillLoadingSettings,
|
|
2382
|
+
enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
|
|
2383
|
+
enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds,
|
|
2037
2384
|
attachments: submittedAttachments,
|
|
2038
2385
|
skillInvocation: submittedSkillInvocation,
|
|
2039
2386
|
submittedMode
|
|
@@ -2044,12 +2391,17 @@
|
|
|
2044
2391
|
const userChoice = await showThreadResumeFailedPrompt();
|
|
2045
2392
|
if (userChoice === 'new') {
|
|
2046
2393
|
updateSessionById(runSessionId, { codexThreadId: '' });
|
|
2394
|
+
renderContextUsage();
|
|
2047
2395
|
const StorageDb = Modules.StorageDb;
|
|
2048
|
-
|
|
2049
|
-
|
|
2396
|
+
// A live session goes through the ordinary writer. An envelope
|
|
2397
|
+
// conflict there must not abort the run that is choosing a new thread.
|
|
2398
|
+
if (findSessionById(runSessionId)) {
|
|
2399
|
+
saveStateSoon();
|
|
2400
|
+
} else if (StorageDb) {
|
|
2401
|
+
const record = await StorageDb.getRecord('sessions', runSessionId, { adopt: false });
|
|
2050
2402
|
if (record) {
|
|
2051
2403
|
record.codexThreadId = '';
|
|
2052
|
-
await StorageDb.putRecord('sessions', record);
|
|
2404
|
+
await StorageDb.putRecord('sessions', record, { adopt: false });
|
|
2053
2405
|
}
|
|
2054
2406
|
}
|
|
2055
2407
|
appendRunEvent({ title: tx('Creating a new Codex conversation thread.', '正在新建 Codex 会话线程。'), status: 'running' });
|
|
@@ -2065,6 +2417,8 @@
|
|
|
2065
2417
|
compileLogContext,
|
|
2066
2418
|
customInstructions: submittedCustomInstructions,
|
|
2067
2419
|
skillLoadingSettings: submittedSkillLoadingSettings,
|
|
2420
|
+
enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
|
|
2421
|
+
enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds,
|
|
2068
2422
|
attachments: submittedAttachments,
|
|
2069
2423
|
skillInvocation: submittedSkillInvocation,
|
|
2070
2424
|
submittedMode
|
|
@@ -2085,19 +2439,33 @@
|
|
|
2085
2439
|
void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
|
|
2086
2440
|
return;
|
|
2087
2441
|
}
|
|
2088
|
-
|
|
2442
|
+
if (response.error?.code === 'codex_local_skill_unavailable' && localSkillRestorePoint) {
|
|
2443
|
+
restoreRejectedLocalSkillSubmission({ ...submittedWritingStyleScope, sessionId: submittedSessionId,
|
|
2444
|
+
task: submittedDraft, attachments: submittedAttachments, skillInvocation: submittedSkillInvocation,
|
|
2445
|
+
selectionContext: submittedPanelState.selectionContext }, localSkillRestorePoint);
|
|
2446
|
+
}
|
|
2447
|
+
const translated = translateRawError(response.error.message, { mode: submittedMode, locale: getLocale(), code: response.error?.code });
|
|
2089
2448
|
// Structured FailureReason (§9.7): split bridge-availability from
|
|
2090
2449
|
// Codex-side errors. `native_connection_failed` / `native_unavailable`
|
|
2091
2450
|
// / `native_update_required` indicate the bridge itself is missing,
|
|
2092
|
-
// so emit `native_bridge_unavailable` (blocked, retryable).
|
|
2093
|
-
//
|
|
2094
|
-
// `codex_no_usable_result` (error, retryable).
|
|
2451
|
+
// so emit `native_bridge_unavailable` (blocked, retryable). Recognized
|
|
2452
|
+
// Codex failures retain their specific cause; only unmatched errors
|
|
2453
|
+
// fall back to `codex_no_usable_result` (error, retryable).
|
|
2095
2454
|
// Map native/codex failures onto their SPECIFIC catalog codes — the
|
|
2096
2455
|
// timeout/not-found/output-limit entries (and their bilingual next
|
|
2097
2456
|
// steps) existed but were never emitted, so everything collapsed into
|
|
2098
2457
|
// "no usable result" and pointed users at the wrong recovery.
|
|
2099
2458
|
const runErrorMessage = String(response.error?.message || '');
|
|
2100
|
-
const codexRunFailure =
|
|
2459
|
+
const codexRunFailure = translated.failureCode === 'codex_context_limit'
|
|
2460
|
+
? buildContentFailure('codex_context_limit', { path: 'codex.run' }, {
|
|
2461
|
+
technicalMessage: runErrorMessage,
|
|
2462
|
+
evidence: { errorCode: response.error?.code || '' }
|
|
2463
|
+
})
|
|
2464
|
+
: response.error?.code === 'codex_local_skill_unavailable'
|
|
2465
|
+
? buildContentFailure('codex_local_skill_unavailable', { path: 'codex.run' }, { technicalMessage: runErrorMessage })
|
|
2466
|
+
: translated.failureCode === 'codex_local_skills_failed'
|
|
2467
|
+
? buildContentFailure('codex_local_skills_failed', { path: 'codex.run' }, { technicalMessage: runErrorMessage })
|
|
2468
|
+
: response.error?.code === 'native_execution_interrupted'
|
|
2101
2469
|
? buildContentFailure('native_request_failed', { path: 'codex.run' }, {
|
|
2102
2470
|
// The host IS installed — the request was interrupted mid-flight.
|
|
2103
2471
|
// Telling this user to reinstall would be a misdiagnosis.
|
|
@@ -2154,7 +2522,7 @@
|
|
|
2154
2522
|
detail: response.error
|
|
2155
2523
|
});
|
|
2156
2524
|
appendCompletionReport({
|
|
2157
|
-
conclusion: translated.conclusion,
|
|
2525
|
+
conclusion: translated.conclusion, conclusionKey: translated.conclusionKey,
|
|
2158
2526
|
status: response.error?.code === 'project_locked' ? 'blocked' : 'failed',
|
|
2159
2527
|
operations: [],
|
|
2160
2528
|
applyResults: [],
|
|
@@ -2204,7 +2572,7 @@
|
|
|
2204
2572
|
status: 'failed',
|
|
2205
2573
|
failure: emptyFailure
|
|
2206
2574
|
});
|
|
2207
|
-
appendCompletionReport({
|
|
2575
|
+
appendCompletionReport({ conclusionKey: 'empty_result',
|
|
2208
2576
|
conclusion: emptyResultConclusion,
|
|
2209
2577
|
status: 'failed',
|
|
2210
2578
|
operations: [],
|
|
@@ -2308,12 +2676,14 @@
|
|
|
2308
2676
|
if (returnedThreadId && returnedThreadId !== codexThreadId) {
|
|
2309
2677
|
updateSessionById(runSessionId, { codexThreadId: returnedThreadId });
|
|
2310
2678
|
const StorageDb = Modules.StorageDb;
|
|
2311
|
-
|
|
2312
|
-
|
|
2679
|
+
// The live session is saved below with its latest draft. Writing an
|
|
2680
|
+
// older DB snapshot here can give stale text a newer timestamp.
|
|
2681
|
+
if (StorageDb && !findSessionById(runSessionId)) {
|
|
2682
|
+
const record = await StorageDb.getRecord('sessions', runSessionId, { adopt: false });
|
|
2313
2683
|
if (record) {
|
|
2314
2684
|
record.codexThreadId = returnedThreadId;
|
|
2315
2685
|
record.updatedAt = new Date().toISOString();
|
|
2316
|
-
await StorageDb.putRecord('sessions', record);
|
|
2686
|
+
await StorageDb.putRecord('sessions', record, { adopt: false });
|
|
2317
2687
|
}
|
|
2318
2688
|
}
|
|
2319
2689
|
}
|
|
@@ -2334,17 +2704,43 @@
|
|
|
2334
2704
|
}
|
|
2335
2705
|
} catch (error) {
|
|
2336
2706
|
if (runCancellationRequested || isRunCancellationError(error)) {
|
|
2337
|
-
|
|
2707
|
+
if (currentRunView?.reviewingUnavailablePreflight) {
|
|
2708
|
+
appendCompletionReport({ conclusionKey: 'launch_not_started',
|
|
2709
|
+
conclusion: tx('This task did not start: Codex did not run and no files were written.', '这轮任务没有开始:Codex 没有运行,也没有写入文件。'),
|
|
2710
|
+
status: 'blocked', operations: [], applyResults: [],
|
|
2711
|
+
failure: buildContentFailure('reviewing_unavailable', {}, {
|
|
2712
|
+
changedDocument: false, evidence: { declinedChoice: true }
|
|
2713
|
+
}),
|
|
2714
|
+
nextStep: tr('failureReason_reviewing_unavailable_cancelled_next') });
|
|
2715
|
+
} else appendRunCancelledReport();
|
|
2338
2716
|
await finishRunView(tx('Cancelled', '已取消'), 'cancelled');
|
|
2339
2717
|
void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
|
|
2340
2718
|
return;
|
|
2341
2719
|
}
|
|
2720
|
+
if (compact) {
|
|
2721
|
+
const key = error.code === 'codex_compact_unsupported' ? 'compactUnsupported'
|
|
2722
|
+
: ['project_locked', 'codex_compact_thread_busy'].includes(error.code) ? 'compactBusy' : 'compactFailed';
|
|
2723
|
+
const contextLimit = FailureReasons.isContextLimitError(error.message, error.code)
|
|
2724
|
+
? translateRawError(error.message, { locale: getLocale(), code: error.code }) : null;
|
|
2725
|
+
const failure = contextLimit ? buildContentFailure('codex_context_limit', { path: 'codex.run' }, {
|
|
2726
|
+
technicalMessage: error.message, evidence: { errorCode: error.code || '' }
|
|
2727
|
+
}) : undefined;
|
|
2728
|
+
const conclusion = contextLimit?.conclusion || tr(key);
|
|
2729
|
+
appendRunEvent({ title: conclusion, status: 'failed', failure,
|
|
2730
|
+
technicalDetail: { code: error.code || '', message: error.message } });
|
|
2731
|
+
appendCompletionReport({ conclusion, conclusionKey: contextLimit?.conclusionKey || (key === 'compactUnsupported'
|
|
2732
|
+
? 'compact_unsupported' : key === 'compactBusy' ? 'compact_busy' : 'compact_failed'),
|
|
2733
|
+
nextStep: contextLimit?.nextStep, failure,
|
|
2734
|
+
status: 'failed', mode: 'ask', operations: [], applyResults: [] });
|
|
2735
|
+
await finishRunView(conclusion, 'failed');
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2342
2738
|
// codexReturned: assistantMessage arrived on the stream before the
|
|
2343
2739
|
// exception escaped, so Codex's answer is preserved in the conversation
|
|
2344
2740
|
// — the fallback conclusion must reflect that instead of saying
|
|
2345
2741
|
// 'local Codex returned no usable result'.
|
|
2346
2742
|
const codexReturned = Boolean(getAssistantAnswerForCurrentRun());
|
|
2347
|
-
const translated = translateRawError(error.message, { mode: submittedMode, locale: getLocale(), codexReturned });
|
|
2743
|
+
const translated = translateRawError(error.message, { mode: submittedMode, locale: getLocale(), code: error.code, codexReturned });
|
|
2348
2744
|
// When translateRawError maps the raw error to a FailureReason code,
|
|
2349
2745
|
// attach the structured failure to the run-event and completion-report
|
|
2350
2746
|
// so the run record carries machine-readable failure data alongside
|
|
@@ -2372,7 +2768,7 @@
|
|
|
2372
2768
|
}
|
|
2373
2769
|
});
|
|
2374
2770
|
appendCompletionReport({
|
|
2375
|
-
conclusion: translated.conclusion,
|
|
2771
|
+
conclusion: translated.conclusion, conclusionKey: translated.conclusionKey,
|
|
2376
2772
|
status: 'failed',
|
|
2377
2773
|
operations: [],
|
|
2378
2774
|
applyResults: [],
|
|
@@ -2386,6 +2782,10 @@
|
|
|
2386
2782
|
blockedFiles: [{ path: 'task', reason: error.message }]
|
|
2387
2783
|
});
|
|
2388
2784
|
await finishRunView(tx('Task failed', '任务失败'), 'failed');
|
|
2785
|
+
// A failed initial card render must still leave a visible terminal record.
|
|
2786
|
+
if (currentRunView?.sessionId === state.activeSessionId && !getCurrentRunViewForRender()) {
|
|
2787
|
+
renderRunHistory({ preserveScroll: true });
|
|
2788
|
+
}
|
|
2389
2789
|
} finally {
|
|
2390
2790
|
const settledView = currentRunView;
|
|
2391
2791
|
const settledRecord = settledView ? findRunRecord(settledView.recordId, settledView.sessionId) : null;
|
|
@@ -2422,6 +2822,8 @@
|
|
|
2422
2822
|
submittedMode,
|
|
2423
2823
|
submittedCustomInstructions,
|
|
2424
2824
|
submittedSkillLoadingSettings,
|
|
2825
|
+
submittedEnabledLocalSkillIds,
|
|
2826
|
+
submittedEnabledSkillIds,
|
|
2425
2827
|
submittedAttachments,
|
|
2426
2828
|
submittedSkillInvocation,
|
|
2427
2829
|
executionSnapshot
|
|
@@ -2430,8 +2832,7 @@
|
|
|
2430
2832
|
title: tx('Starting the Codex skill installer.', '正在启动 Codex skill installer。'),
|
|
2431
2833
|
status: 'running'
|
|
2432
2834
|
});
|
|
2433
|
-
const
|
|
2434
|
-
const codexThreadId = activeSession?.codexThreadId || '';
|
|
2835
|
+
const codexThreadId = currentRunView?.codexThreadId || '';
|
|
2435
2836
|
const params = {
|
|
2436
2837
|
projectId: getCurrentProjectId(),
|
|
2437
2838
|
mode: submittedMode,
|
|
@@ -2439,21 +2840,24 @@
|
|
|
2439
2840
|
model: executionSnapshot.model,
|
|
2440
2841
|
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
2441
2842
|
speedTier: executionSnapshot.speedTier,
|
|
2442
|
-
session:
|
|
2843
|
+
session: currentRunView?.sessionContext,
|
|
2844
|
+
locale: getLocale(),
|
|
2845
|
+
clientRunId: currentRunView?.recordId || '',
|
|
2846
|
+
clientSessionId: runSessionId,
|
|
2847
|
+
accountScopeId: currentRunView?.runAccountScopeId || '',
|
|
2443
2848
|
threadId: codexThreadId || undefined,
|
|
2444
2849
|
customInstructions: submittedCustomInstructions || undefined,
|
|
2445
2850
|
loadCodexLocalSkills: submittedSkillLoadingSettings?.loadCodexLocalSkills !== false,
|
|
2446
2851
|
loadCodexOverleafSkills: submittedSkillLoadingSettings?.loadCodexOverleafSkills !== false,
|
|
2852
|
+
enabledCodexLocalSkillIds: submittedEnabledLocalSkillIds,
|
|
2853
|
+
enabledCodexOverleafSkillIds: submittedEnabledSkillIds,
|
|
2447
2854
|
attachments: submittedAttachments.length ? submittedAttachments : undefined,
|
|
2448
2855
|
skillInvocation: submittedSkillInvocation,
|
|
2449
2856
|
focusFiles: [],
|
|
2450
2857
|
skipMirrorSync: true
|
|
2451
2858
|
};
|
|
2452
2859
|
|
|
2453
|
-
const response = await
|
|
2454
|
-
method: 'codex.run',
|
|
2455
|
-
params
|
|
2456
|
-
});
|
|
2860
|
+
const response = await sendTrackedCodexRun(params);
|
|
2457
2861
|
|
|
2458
2862
|
if (!response.ok) {
|
|
2459
2863
|
if (runCancellationRequested || isRunCancellationError(response.error)) {
|
|
@@ -2479,7 +2883,7 @@
|
|
|
2479
2883
|
failure: translatedFailure
|
|
2480
2884
|
});
|
|
2481
2885
|
appendCompletionReport({
|
|
2482
|
-
conclusion: translated.conclusion,
|
|
2886
|
+
conclusion: translated.conclusion, conclusionKey: translated.conclusionKey,
|
|
2483
2887
|
status: 'failed',
|
|
2484
2888
|
operations: [],
|
|
2485
2889
|
applyResults: [],
|
|
@@ -2497,7 +2901,7 @@
|
|
|
2497
2901
|
}
|
|
2498
2902
|
|
|
2499
2903
|
const assistantMessage = response.result?.assistantMessage || getAssistantAnswerForCurrentRun();
|
|
2500
|
-
appendCompletionReport({
|
|
2904
|
+
appendCompletionReport({ conclusionKey: !assistantMessage ? 'skill_installer_completed' : undefined,
|
|
2501
2905
|
conclusion: assistantMessage || tx('Skill installer completed.', 'Skill installer 已完成。'),
|
|
2502
2906
|
status: 'completed',
|
|
2503
2907
|
operations: [],
|
|
@@ -2531,11 +2935,14 @@
|
|
|
2531
2935
|
if (returnedThreadId && returnedThreadId !== codexThreadId) {
|
|
2532
2936
|
updateSessionById(runSessionId, { codexThreadId: returnedThreadId });
|
|
2533
2937
|
const StorageDb = Modules.StorageDb;
|
|
2534
|
-
|
|
2535
|
-
|
|
2938
|
+
// The installer already finished; a later envelope conflict only delays this save.
|
|
2939
|
+
if (findSessionById(runSessionId)) {
|
|
2940
|
+
saveStateSoon();
|
|
2941
|
+
} else if (StorageDb) {
|
|
2942
|
+
const record = await StorageDb.getRecord('sessions', runSessionId, { adopt: false });
|
|
2536
2943
|
if (record) {
|
|
2537
2944
|
record.codexThreadId = returnedThreadId;
|
|
2538
|
-
await StorageDb.putRecord('sessions', record);
|
|
2945
|
+
await StorageDb.putRecord('sessions', record, { adopt: false });
|
|
2539
2946
|
}
|
|
2540
2947
|
}
|
|
2541
2948
|
}
|
|
@@ -2549,8 +2956,6 @@
|
|
|
2549
2956
|
if (mode === 'ask') {
|
|
2550
2957
|
return { ok: true, skipped: true };
|
|
2551
2958
|
}
|
|
2552
|
-
const method = requireReviewing ? 'ensureReviewing' : 'ensureEditing';
|
|
2553
|
-
|
|
2554
2959
|
appendRunEvent({
|
|
2555
2960
|
title: requireReviewing
|
|
2556
2961
|
? tx('Checking Overleaf Reviewing/Track Changes before starting.', '正在确认 Overleaf 留痕状态。')
|
|
@@ -2558,30 +2963,67 @@
|
|
|
2558
2963
|
status: 'running'
|
|
2559
2964
|
});
|
|
2560
2965
|
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2966
|
+
const owner = currentRunView;
|
|
2967
|
+
const signal = runCancellationController?.signal;
|
|
2968
|
+
const isCurrent = () => owner === currentRunView && !signal?.aborted && !runCancellationRequested
|
|
2969
|
+
&& owner?.runProjectId === getCurrentProjectId() && owner?.runAccountScopeId === cachedAccountScopeId;
|
|
2970
|
+
const assertCurrent = () => { if (!isCurrent()) throw createRunCancellationError(); };
|
|
2971
|
+
assertCurrent();
|
|
2972
|
+
if (requireReviewing) {
|
|
2973
|
+
// Another tab may have observed this project's capability since hydration.
|
|
2974
|
+
const prefs = await Modules.StorageMigration.loadPrefs(owner.runAccountScopeId, owner.runProjectId);
|
|
2975
|
+
assertCurrent();
|
|
2976
|
+
const incoming = prefs?.reviewingAvailabilityByProject?.[owner.runProjectId];
|
|
2977
|
+
const current = projectSettingsCoordinator.getReviewingAvailability();
|
|
2978
|
+
if (incoming && (!current || incoming.observedAt > current.observedAt
|
|
2979
|
+
|| (incoming.observedAt === current.observedAt && incoming.status === 'available'))) {
|
|
2980
|
+
state = { ...state, reviewingAvailabilityByProject: {
|
|
2981
|
+
...state.reviewingAvailabilityByProject, [owner.runProjectId]: incoming
|
|
2982
|
+
} };
|
|
2983
|
+
syncProbeStatus();
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
let result = await Modules.Reviewing.resolveWritePolicy({ requireReviewing,
|
|
2987
|
+
knownUnavailable: isReviewingKnownUnavailable() }, {
|
|
2988
|
+
assertCurrent,
|
|
2989
|
+
ensureMode: async required => {
|
|
2990
|
+
try {
|
|
2991
|
+
return await callPageBridge(required ? 'ensureReviewing' : 'ensureEditing', {
|
|
2992
|
+
waitMs: 1800, runProjectId: owner.runProjectId
|
|
2993
|
+
});
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
assertCurrent();
|
|
2996
|
+
return { ok: false, code: error?.code, reason: error?.message, reviewing: null };
|
|
2997
|
+
}
|
|
2998
|
+
},
|
|
2999
|
+
observe: async (status, evidence) => {
|
|
3000
|
+
projectSettingsCoordinator.observeReviewingAvailability(status, evidence);
|
|
3001
|
+
syncProbeStatus();
|
|
3002
|
+
// Publish a detected paywall before waiting for the user's decision.
|
|
3003
|
+
if (status === 'unavailable') await flushQueuedSaveState({ requireDurable: true });
|
|
3004
|
+
assertCurrent();
|
|
3005
|
+
},
|
|
3006
|
+
choose: () => {
|
|
3007
|
+
owner.reviewingUnavailablePreflight = true;
|
|
3008
|
+
return showPluginConfirm({
|
|
3009
|
+
title: tr('reviewingUnavailableTitle'),
|
|
3010
|
+
message: tr('reviewingUnavailableChoiceMessage'),
|
|
3011
|
+
choices: [
|
|
3012
|
+
{ value: 'continue', label: tr('reviewingUnavailableContinue') },
|
|
3013
|
+
{ value: 'disable', label: tr('reviewingUnavailableDisable'), secondary: true },
|
|
3014
|
+
{ value: 'cancel', label: tr('confirmDefaultCancel') }
|
|
3015
|
+
],
|
|
3016
|
+
signal, isCurrent, timeoutMs: REVIEWING_CONFIRM_TIMEOUT_MS
|
|
3017
|
+
});
|
|
3018
|
+
},
|
|
3019
|
+
applyUntracked: choice => { assertCurrent(); return options.onUntracked(choice); }
|
|
3020
|
+
});
|
|
3021
|
+
if (result.ok) owner.reviewingUnavailablePreflight = false;
|
|
3022
|
+
const effectiveRequireReviewing = result.requireReviewing ?? requireReviewing;
|
|
2581
3023
|
|
|
2582
3024
|
if (result?.ok) {
|
|
2583
3025
|
appendRunEvent({
|
|
2584
|
-
title:
|
|
3026
|
+
title: effectiveRequireReviewing
|
|
2585
3027
|
? (result.activated
|
|
2586
3028
|
? tx('Overleaf Reviewing/Track Changes is now on. Starting the task.', '已开启 Overleaf 留痕,开始处理任务。')
|
|
2587
3029
|
: tx('Overleaf Reviewing/Track Changes is already on. Starting the task.', 'Overleaf 留痕已经开启,开始处理任务。'))
|
|
@@ -2593,10 +3035,23 @@
|
|
|
2593
3035
|
return result;
|
|
2594
3036
|
}
|
|
2595
3037
|
|
|
2596
|
-
const
|
|
3038
|
+
const unavailable = result?.code === 'reviewing_unavailable';
|
|
3039
|
+
if (unavailable && !result.failure) result.failure = buildContentFailure('reviewing_unavailable', {}, {
|
|
3040
|
+
changedDocument: false, evidence: result.evidence
|
|
3041
|
+
});
|
|
3042
|
+
const reason = unavailable ? tr('failureReason_reviewing_unavailable_user') : localizeVisibleReason(result?.reason || (effectiveRequireReviewing
|
|
2597
3043
|
? tx('Overleaf did not return track changes status', 'Overleaf 没有返回留痕状态')
|
|
2598
3044
|
: tx('Overleaf did not return Editing mode status', 'Overleaf 没有返回 Editing 模式状态')));
|
|
2599
|
-
|
|
3045
|
+
// Affirmative choices retain trackingDecision even if the Editing check fails.
|
|
3046
|
+
const declinedReviewingChoice = owner.reviewingUnavailablePreflight === true && !result.trackingDecision;
|
|
3047
|
+
const reportFailure = unavailable && declinedReviewingChoice
|
|
3048
|
+
? buildContentFailure('reviewing_unavailable', {}, {
|
|
3049
|
+
...result.failure, changedDocument: false,
|
|
3050
|
+
evidence: { ...result.failure?.evidence, declinedChoice: true }
|
|
3051
|
+
}) : result?.failure;
|
|
3052
|
+
const nextStep = unavailable ? tr(declinedReviewingChoice
|
|
3053
|
+
? 'failureReason_reviewing_unavailable_cancelled_next'
|
|
3054
|
+
: 'failureReason_reviewing_unavailable_next') : effectiveRequireReviewing
|
|
2600
3055
|
? tx(
|
|
2601
3056
|
'You may not have permission, or this Overleaf page did not expose the Reviewing switch. Switch to Reviewing manually and retry, or turn off Track before writing.',
|
|
2602
3057
|
'你可能没有权限,或 Overleaf 当前页面没有暴露切换入口。可以手动切到 Reviewing 后重试,或关闭“留痕”再运行。'
|
|
@@ -2606,7 +3061,7 @@
|
|
|
2606
3061
|
'你可能没有权限,或 Overleaf 当前页面没有暴露切换入口。请在 Overleaf 手动切到 Editing 后重试。'
|
|
2607
3062
|
);
|
|
2608
3063
|
appendRunEvent({
|
|
2609
|
-
title:
|
|
3064
|
+
title: effectiveRequireReviewing
|
|
2610
3065
|
? tx('Task not started: could not enable Overleaf Reviewing/Track Changes.', '任务未开始:无法开启 Overleaf 留痕。')
|
|
2611
3066
|
: tx('Task not started: could not switch to Overleaf Editing.', '任务未开始:无法切换到 Overleaf Editing。'),
|
|
2612
3067
|
status: 'failed',
|
|
@@ -2615,24 +3070,25 @@
|
|
|
2615
3070
|
[tr('detailNext')]: nextStep
|
|
2616
3071
|
},
|
|
2617
3072
|
technicalDetail: {
|
|
2618
|
-
reason,
|
|
3073
|
+
reason, code: result?.code, evidence: result?.evidence, failure: result?.failure,
|
|
2619
3074
|
reviewing: result?.reviewing || null
|
|
2620
3075
|
}
|
|
2621
3076
|
});
|
|
2622
|
-
appendCompletionReport({
|
|
3077
|
+
appendCompletionReport({ conclusionKey: 'launch_not_started',
|
|
2623
3078
|
conclusion: tx('This task did not start: Codex did not run and no files were written.', '这轮任务没有开始:Codex 没有运行,也没有写入文件。'),
|
|
2624
3079
|
status: 'blocked',
|
|
2625
3080
|
operations: [],
|
|
2626
3081
|
applyResults: [],
|
|
2627
|
-
nextStep
|
|
3082
|
+
nextStep,
|
|
3083
|
+
reason: result?.code, failure: reportFailure
|
|
2628
3084
|
});
|
|
2629
|
-
const finishTitle =
|
|
3085
|
+
const finishTitle = effectiveRequireReviewing
|
|
2630
3086
|
? tx('Not started: could not enable Track Changes', '未开始:无法开启留痕')
|
|
2631
3087
|
: tx('Not started: could not switch to Editing', '未开始:无法切换到 Editing');
|
|
2632
3088
|
await finishRunView(finishTitle, 'failed');
|
|
2633
3089
|
return {
|
|
2634
|
-
ok: false,
|
|
2635
|
-
reason,
|
|
3090
|
+
...result, ok: false,
|
|
3091
|
+
reason: result?.code || reason,
|
|
2636
3092
|
reviewing: result?.reviewing || null
|
|
2637
3093
|
};
|
|
2638
3094
|
}
|
|
@@ -2645,27 +3101,47 @@
|
|
|
2645
3101
|
return runController.truncateSessionHistoryText(value, maxLength);
|
|
2646
3102
|
}
|
|
2647
3103
|
|
|
2648
|
-
function safeRunTask() {
|
|
3104
|
+
function safeRunTask(options = {}) {
|
|
3105
|
+
if (sessionManager.isTransitioning()) return;
|
|
2649
3106
|
if (updateIdleClient?.isApplying()) {
|
|
2650
3107
|
showPluginToast(tx('An update is being applied. Codex will be available again after reload.', '正在应用更新,重新加载后即可继续使用 Codex。'), { status: 'info' });
|
|
2651
3108
|
return;
|
|
2652
3109
|
}
|
|
3110
|
+
const submittedCompactCommand = options.operation !== 'compact'
|
|
3111
|
+
&& panel?.querySelector('[data-task]')?.value.trim() === '/compact';
|
|
3112
|
+
const compact = options.operation === 'compact' || submittedCompactCommand;
|
|
3113
|
+
if (compact && (currentRunView || runSubmissionPending)) {
|
|
3114
|
+
showPluginToast(tr('compactBusy'), { status: 'info' });
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
2653
3117
|
if (currentRunView) {
|
|
2654
3118
|
return queueComposerInput();
|
|
2655
3119
|
}
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
3120
|
+
if (runSubmissionPending) return;
|
|
3121
|
+
runSubmissionPending = true;
|
|
3122
|
+
runTask(compact ? { operation: 'compact', consumeCompactDraft: options.consumeCompactDraft === true || submittedCompactCommand } : {}).catch(error => {
|
|
3123
|
+
if (!currentRunView) {
|
|
3124
|
+
setRunning(false);
|
|
3125
|
+
nativeChannel.clearActiveRequest();
|
|
3126
|
+
stopRunElapsedTick();
|
|
3127
|
+
runCancellationController = null;
|
|
3128
|
+
}
|
|
2662
3129
|
console.error('[codex-overleaf] failed to start task', error);
|
|
2663
|
-
appendPlainLog(tx(`Could not start Codex task: ${error.message}`, `无法启动 Codex 任务:${error.message}`));
|
|
2664
|
-
});
|
|
3130
|
+
appendPlainLog(tx(`Could not start Codex task: ${localizeVisibleReason(error.message)}`, `无法启动 Codex 任务:${localizeVisibleReason(error.message)}`));
|
|
3131
|
+
}).finally(() => { runSubmissionPending = false; });
|
|
2665
3132
|
}
|
|
2666
3133
|
|
|
2667
3134
|
const runGuidanceController = Modules.RunGuidanceController.create({
|
|
2668
|
-
findRunRecord,
|
|
3135
|
+
findRunRecord, getCurrentRunView: () => getCurrentRunViewForRender(),
|
|
3136
|
+
getActiveSessionId: () => state?.activeSessionId,
|
|
3137
|
+
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
|
|
3138
|
+
getSessionCreatedAt: sessionId => findSessionById(sessionId)?.createdAt,
|
|
3139
|
+
getRunRoot: target => target.sessionId === state?.activeSessionId
|
|
3140
|
+
? panel?.querySelector(`[data-run-id="${cssEscape(target.recordId)}"]`) : null,
|
|
3141
|
+
onRecordChanged: target => updateSessionById(target.sessionId, {}),
|
|
3142
|
+
onPresentationError: error => console.warn('[codex-overleaf] guidance presentation failed', error),
|
|
3143
|
+
RunGuidanceView: Modules.RunGuidanceView, tx, maxRunEvents: MAX_RUN_EVENTS,
|
|
3144
|
+
revealGuidance: row => runTimelineView.revealGuidance(row),
|
|
2669
3145
|
renderRunEvent, cssEscape, bumpUnreadIfDetached
|
|
2670
3146
|
});
|
|
2671
3147
|
|
|
@@ -2675,17 +3151,27 @@
|
|
|
2675
3151
|
getActiveSession: () => getActiveSession(state),
|
|
2676
3152
|
setQueue: (sessionId, pendingInputs) => updateSessionById(sessionId, { pendingInputs }),
|
|
2677
3153
|
getCurrentRun: () => currentRunView,
|
|
2678
|
-
canStart:
|
|
3154
|
+
canStart: canStartQueuedSession,
|
|
2679
3155
|
readInputs: readPanelInputs,
|
|
2680
3156
|
getTask: () => panel?.querySelector('[data-task]')?.value || '',
|
|
2681
3157
|
hasAttachments: () => getComposerAttachmentsForRun().length > 0,
|
|
2682
|
-
capturePayload: () =>
|
|
3158
|
+
capturePayload: () => ({
|
|
3159
|
+
...RunExecutionSnapshot.toQueuePayload(captureCurrentExecutionSnapshot()),
|
|
3160
|
+
submissionContext: RunInputQueue.normalizeSubmissionContext({
|
|
3161
|
+
customInstructions: getCustomInstructionsForCurrentProject(),
|
|
3162
|
+
skillLoadingSettings: getSkillLoadingSettings(),
|
|
3163
|
+
skillInvocation: getComposerSkillInvocationForRun(),
|
|
3164
|
+
enabledCodexOverleafSkillIds: getEnabledCodexOverleafSkillIds(),
|
|
3165
|
+
enabledCodexLocalSkillIds: getEnabledCodexLocalSkillIds()
|
|
3166
|
+
})
|
|
3167
|
+
}),
|
|
2683
3168
|
prepareClaim: () => providerSettingsCoordinator.ensureLoaded(),
|
|
2684
3169
|
resolveExecutionSnapshot: snapshot => RunExecutionSnapshot.resolveForExecution(snapshot, {
|
|
2685
3170
|
catalog: providerSettingsCoordinator.getCatalog()
|
|
2686
3171
|
}),
|
|
2687
3172
|
clearTask: () => {
|
|
2688
3173
|
panel.querySelector('[data-task]').value = '';
|
|
3174
|
+
composerDraftEditedHere = false;
|
|
2689
3175
|
selectionContextView.clear();
|
|
2690
3176
|
updateSessionById(state.activeSessionId, { task: '' });
|
|
2691
3177
|
autosizeTaskTextarea();
|
|
@@ -2696,12 +3182,22 @@
|
|
|
2696
3182
|
RunExecutionSnapshot.fromQueuePayload(item.payload),
|
|
2697
3183
|
{ catalog: providerSettingsCoordinator.getCatalog() }
|
|
2698
3184
|
);
|
|
3185
|
+
// Promotion uses its captured snapshot; this line still describes the current draft.
|
|
3186
|
+
syncProbeStatus();
|
|
2699
3187
|
},
|
|
2700
3188
|
run: item => runTask({ queuedInput: item }),
|
|
2701
|
-
|
|
3189
|
+
getGuideScope: () => ({ projectId: getCurrentProjectId(), accountScopeId: cachedAccountScopeId, generation: spaRouteGeneration }),
|
|
3190
|
+
isGuideCancelled: () => runCancellationRequested || runCancellationController?.signal?.aborted === true,
|
|
3191
|
+
preflightGuide: ({ task, assertCurrent }) => runSensitivePreflight({
|
|
3192
|
+
task, project: { files: [] }, rules: getGovernanceRulesForCurrentProject(),
|
|
3193
|
+
guidance: true, useExistingMirror: false, assertCurrent
|
|
3194
|
+
}),
|
|
3195
|
+
sendSteer: (params, context) => nativeCompatibilityController.sendBackgroundNative({ method: 'codex.steer', params }, context),
|
|
2702
3196
|
getContainer: () => panel?.querySelector('[data-pending-inputs]'),
|
|
2703
3197
|
createView: options => PendingInputView.create(options),
|
|
2704
3198
|
appendEvent: (title, status) => appendRunEvent({ title, status }),
|
|
3199
|
+
previewGuidance: runGuidanceController.preview,
|
|
3200
|
+
clearGuidancePreview: runGuidanceController.clearPreview,
|
|
2705
3201
|
appendGuidance: runGuidanceController.upsert,
|
|
2706
3202
|
removeGuidance: runGuidanceController.remove,
|
|
2707
3203
|
toast: (message, status) => showPluginToast(message, { status }),
|
|
@@ -2740,7 +3236,29 @@
|
|
|
2740
3236
|
});
|
|
2741
3237
|
}
|
|
2742
3238
|
|
|
3239
|
+
function canStartQueuedSession(sessionId) {
|
|
3240
|
+
if (state?.activeSessionId !== sessionId || !isProjectEditorRoute(window.location)) return false;
|
|
3241
|
+
if (sessionManager.isTransitioning()) {
|
|
3242
|
+
deferredSessionStart = { sessionId, scope: getComposerDraftScope() };
|
|
3243
|
+
return false;
|
|
3244
|
+
}
|
|
3245
|
+
return true;
|
|
3246
|
+
}
|
|
3247
|
+
|
|
2743
3248
|
function queueComposerInput() {
|
|
3249
|
+
if (sessionManager.isTransitioning()) return;
|
|
3250
|
+
if (getSkillLoadingSettings().loadCodexLocalSkills !== false && !Array.isArray(getCodexLocalSkillsCatalog())) {
|
|
3251
|
+
primeCodexLocalSkillsCatalog({ required: true }).catch(() => {});
|
|
3252
|
+
showPluginToast(tx('Local Codex skills are still loading. Try submitting again when loading finishes.',
|
|
3253
|
+
'Codex 本地技能仍在加载,请在加载完成后重新提交。'), { status: 'info' });
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
const invocation = getComposerSkillInvocationForRun();
|
|
3257
|
+
if (invocation?.scope === 'codex-local' && !getEnabledCodexLocalSkillIds()?.includes(invocation.id)) {
|
|
3258
|
+
showPluginToast(tx('The selected local Codex skill is unavailable or disabled. Select an enabled skill and try again.',
|
|
3259
|
+
'所选 Codex 本地技能不可用或已关闭,请选择已启用的技能后重试。'), { status: 'failed' });
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
2744
3262
|
return runInputCoordinator?.queueComposerInput();
|
|
2745
3263
|
}
|
|
2746
3264
|
|
|
@@ -2790,7 +3308,7 @@
|
|
|
2790
3308
|
const nativeResult = results[0];
|
|
2791
3309
|
const nativeResponse = nativeResult?.status === 'fulfilled' ? nativeResult.value : null;
|
|
2792
3310
|
if (nativeResponse && !nativeResponse.ok) {
|
|
2793
|
-
const message = nativeResponse?.error?.message || tx('native host did not respond', 'native host 没有响应');
|
|
3311
|
+
const message = localizeVisibleReason(nativeResponse?.error?.message || tx('native host did not respond', 'native host 没有响应'));
|
|
2794
3312
|
appendRunEvent({
|
|
2795
3313
|
title: tx(`Cancel request was not delivered: ${message}`, `中断请求没有送达:${message}`),
|
|
2796
3314
|
status: 'failed'
|
|
@@ -2832,25 +3350,8 @@
|
|
|
2832
3350
|
force: true
|
|
2833
3351
|
}
|
|
2834
3352
|
});
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
const released = result.lockReleased === true || result.cancelled === true;
|
|
2838
|
-
appendPlainLog(released
|
|
2839
|
-
? tx(
|
|
2840
|
-
`Force-released the stuck Codex run for project ${projectKey}. You can retry now.`,
|
|
2841
|
-
`已强制释放项目 ${projectKey} 的 Codex 占用,可以重试了。`
|
|
2842
|
-
)
|
|
2843
|
-
: tx(
|
|
2844
|
-
`No stuck run was found for project ${projectKey}.`,
|
|
2845
|
-
`没有发现项目 ${projectKey} 上的卡住任务。`
|
|
2846
|
-
));
|
|
2847
|
-
return response;
|
|
2848
|
-
}
|
|
2849
|
-
const message = response?.error?.message || tx('native host did not respond', 'native host 没有响应');
|
|
2850
|
-
appendPlainLog(tx(
|
|
2851
|
-
`Force-release request was not delivered: ${message}`,
|
|
2852
|
-
`强制释放请求没有送达:${message}`
|
|
2853
|
-
));
|
|
3353
|
+
appendPlainLog(Modules.RunRecoveryActions.formatForceCancelOutcome(response, tx));
|
|
3354
|
+
if (response?.ok !== true && response?.error?.message) appendPlainLog(localizeVisibleReason(response.error.message));
|
|
2854
3355
|
return response;
|
|
2855
3356
|
}
|
|
2856
3357
|
|
|
@@ -2954,6 +3455,12 @@
|
|
|
2954
3455
|
.filter(id => id && isCodexOverleafSkillEnabled(id));
|
|
2955
3456
|
}
|
|
2956
3457
|
|
|
3458
|
+
function getEnabledCodexLocalSkillIds() {
|
|
3459
|
+
if (getSkillLoadingSettings().loadCodexLocalSkills === false) return [];
|
|
3460
|
+
const skills = getCodexLocalSkillsCatalog();
|
|
3461
|
+
return Array.isArray(skills) ? skills.filter(skill => isCodexLocalSkillEnabled(skill.id, skill)).map(skill => skill.id) : undefined;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
2957
3464
|
function buildCodexRunParams({
|
|
2958
3465
|
task,
|
|
2959
3466
|
project,
|
|
@@ -2966,15 +3473,20 @@
|
|
|
2966
3473
|
compileLogContext,
|
|
2967
3474
|
customInstructions,
|
|
2968
3475
|
skillLoadingSettings,
|
|
3476
|
+
enabledCodexOverleafSkillIds,
|
|
3477
|
+
enabledCodexLocalSkillIds,
|
|
2969
3478
|
attachments,
|
|
2970
3479
|
skillInvocation,
|
|
2971
3480
|
submittedMode
|
|
2972
3481
|
} = {}) {
|
|
2973
3482
|
const executionSnapshot = currentRunView?.executionSnapshot || captureCurrentExecutionSnapshot();
|
|
2974
|
-
const submittedState = RunExecutionSnapshot.applyToState(
|
|
3483
|
+
const submittedState = RunExecutionSnapshot.applyToState({
|
|
3484
|
+
...state, session: currentRunView?.sessionContext || state.session
|
|
3485
|
+
}, executionSnapshot);
|
|
2975
3486
|
const params = runController.buildCodexRunParams({
|
|
2976
3487
|
currentProjectId: getCurrentProjectId(),
|
|
2977
3488
|
state: submittedState,
|
|
3489
|
+
locale: getLocale(),
|
|
2978
3490
|
task,
|
|
2979
3491
|
project,
|
|
2980
3492
|
useExistingMirror,
|
|
@@ -2991,7 +3503,9 @@
|
|
|
2991
3503
|
? getSkillLoadingSettings()
|
|
2992
3504
|
: { loadCodexLocalSkills: true, loadCodexOverleafSkills: true })
|
|
2993
3505
|
: skillLoadingSettings,
|
|
2994
|
-
enabledCodexOverleafSkillIds:
|
|
3506
|
+
enabledCodexOverleafSkillIds: enabledCodexOverleafSkillIds === undefined
|
|
3507
|
+
? getEnabledCodexOverleafSkillIds() : enabledCodexOverleafSkillIds,
|
|
3508
|
+
enabledCodexLocalSkillIds,
|
|
2995
3509
|
attachments,
|
|
2996
3510
|
skillInvocation,
|
|
2997
3511
|
compileLogContext,
|
|
@@ -3009,11 +3523,26 @@
|
|
|
3009
3523
|
}
|
|
3010
3524
|
|
|
3011
3525
|
async function sendTrackedCodexRun(params) {
|
|
3012
|
-
const
|
|
3526
|
+
const owner = currentRunView;
|
|
3527
|
+
const signal = runCancellationController?.signal;
|
|
3528
|
+
const assertCurrent = () => {
|
|
3529
|
+
if (signal?.aborted || (owner && owner !== currentRunView)) throw createRunCancellationError();
|
|
3530
|
+
throwIfRunCancellationRequested();
|
|
3531
|
+
if (owner && (owner.runProjectId !== getCurrentProjectId()
|
|
3532
|
+
|| owner.runAccountScopeId !== cachedAccountScopeId)) {
|
|
3533
|
+
throw Object.assign(createRunCancellationError(), {
|
|
3534
|
+
message: 'The active project or account changed before dispatch.'
|
|
3535
|
+
});
|
|
3536
|
+
}
|
|
3537
|
+
};
|
|
3538
|
+
const compatibilityGate = await nativeCompatibilityController.ensureForMethod('codex.run', { assertCurrent });
|
|
3539
|
+
assertCurrent();
|
|
3013
3540
|
if (!compatibilityGate.ok) {
|
|
3014
3541
|
return compatibilityGate.response;
|
|
3015
3542
|
}
|
|
3016
|
-
|
|
3543
|
+
if (params.operation === 'compact' && compatibilityGate.compatibility?.native?.capabilities?.threadCompact !== true) {
|
|
3544
|
+
return { ok: false, error: { code: 'codex_compact_unsupported', message: 'This Native host does not support context compaction.' } };
|
|
3545
|
+
}
|
|
3017
3546
|
const tracked = nativeChannel.sendNativeTracked(nativeCompatibilityController.attachEvidence({
|
|
3018
3547
|
method: 'codex.run',
|
|
3019
3548
|
params
|
|
@@ -3041,57 +3570,44 @@
|
|
|
3041
3570
|
}
|
|
3042
3571
|
|
|
3043
3572
|
async function recoverInterruptedRunJournals() {
|
|
3044
|
-
const projectKey = getCurrentProjectId();
|
|
3573
|
+
const projectKey = getCurrentProjectId(), accountScopeId = cachedAccountScopeId;
|
|
3574
|
+
const generation = spaRouteGeneration;
|
|
3575
|
+
const isCurrent = () => generation === spaRouteGeneration
|
|
3576
|
+
&& projectKey === getCurrentProjectId() && accountScopeId === cachedAccountScopeId;
|
|
3045
3577
|
const recovery = await activeTurnControl.recoverJournals({
|
|
3046
3578
|
projectKey,
|
|
3579
|
+
accountScopeId,
|
|
3580
|
+
isCurrent,
|
|
3581
|
+
onRecheck: async () => {
|
|
3582
|
+
if (!isCurrent()) return;
|
|
3583
|
+
await recoverInterruptedRunJournals();
|
|
3584
|
+
if (isCurrent()) applyStateToPanel();
|
|
3585
|
+
},
|
|
3047
3586
|
requireOwnerLost: Modules.StorageDb.sharedSessionsEnabled?.() === true,
|
|
3048
3587
|
normalizeInterruptedRun: record => Object.assign(record, Modules.SessionState.normalizeRuns([record], {
|
|
3049
3588
|
restoreRunningRuns: true,
|
|
3050
3589
|
locale: getLocale()
|
|
3051
3590
|
})[0]),
|
|
3052
3591
|
findSession: findSessionById,
|
|
3592
|
+
listSessions: () => state?.sessions || [],
|
|
3593
|
+
isLiveRun: (record, session) => currentRunView?.recordId === record.id
|
|
3594
|
+
&& currentRunView.sessionId === session.id && currentRunView.runProjectId === projectKey,
|
|
3053
3595
|
getActiveSession: () => getActiveSession(state),
|
|
3054
|
-
createInterruptedRun: journal => (
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
status: 'interrupted',
|
|
3060
|
-
statusText: tr('restoredRunStoppedStatus'),
|
|
3061
|
-
runProjectId: projectKey,
|
|
3062
|
-
startedAt: journal.createdAt || new Date().toISOString(),
|
|
3063
|
-
finishedAt: journal.updatedAt || new Date().toISOString(),
|
|
3064
|
-
events: [],
|
|
3065
|
-
attachments: [],
|
|
3066
|
-
undoOperations: [],
|
|
3067
|
-
undoTrackedChanges: [],
|
|
3068
|
-
undoExpectedFiles: []
|
|
3596
|
+
createInterruptedRun: journal => ActiveTurnControl.createInterruptedRun(journal, projectKey, tr('restoredRunStoppedStatus')),
|
|
3597
|
+
onRawEvent: (raw, { record, session, sequence }) => storeContextUsage(raw, record, session, sequence),
|
|
3598
|
+
mapEvent: (raw, journal) => ActiveTurnControl.mapRecoveredEvent(raw, journal, {
|
|
3599
|
+
mapActivity: value => mapAgentEventToActivity(value, { locale: getLocale() }),
|
|
3600
|
+
sanitizeText: sanitizeAssistantVisibleText, sanitizeValue: sanitizeAssistantVisibleValue
|
|
3069
3601
|
}),
|
|
3070
|
-
mapEvent: (raw, journal) => {
|
|
3071
|
-
const activity = mapAgentEventToActivity(raw, { locale: getLocale() });
|
|
3072
|
-
if (!activity?.visible || activity.kind === 'technical') {
|
|
3073
|
-
return null;
|
|
3074
|
-
}
|
|
3075
|
-
return {
|
|
3076
|
-
kind: activity.kind || 'activity',
|
|
3077
|
-
title: sanitizeAssistantVisibleText(activity.title) || 'Event',
|
|
3078
|
-
status: activity.status || 'running',
|
|
3079
|
-
detail: sanitizeAssistantVisibleValue(activity.detail),
|
|
3080
|
-
timestamp: raw.timestamp || journal.updatedAt || new Date().toISOString(),
|
|
3081
|
-
streamKey: sanitizeAssistantVisibleText(activity.streamKey),
|
|
3082
|
-
streamRole: sanitizeAssistantVisibleText(activity.streamRole),
|
|
3083
|
-
appendText: sanitizeAssistantVisibleText(activity.appendText),
|
|
3084
|
-
replaceText: activity.replaceText
|
|
3085
|
-
};
|
|
3086
|
-
},
|
|
3087
3602
|
upsertStream: upsertRunStreamRecordEvent,
|
|
3088
3603
|
pauseSessionQueue: (session, reason) => {
|
|
3089
3604
|
session.pendingInputs = RunInputQueue.pauseAll(session.pendingInputs || [], reason);
|
|
3090
3605
|
},
|
|
3091
3606
|
maxEvents: MAX_RUN_EVENTS
|
|
3092
3607
|
});
|
|
3608
|
+
if (!isCurrent()) return;
|
|
3093
3609
|
if (recovery.changed) {
|
|
3094
|
-
await saveState();
|
|
3610
|
+
if (await saveState({ recoveryRuns: recovery.recoveredRuns }) !== true || !isCurrent()) return;
|
|
3095
3611
|
for (const requestId of recovery.acknowledgeIds) {
|
|
3096
3612
|
await activeTurnControl.acknowledge(requestId).catch(() => false);
|
|
3097
3613
|
}
|
|
@@ -3103,14 +3619,21 @@
|
|
|
3103
3619
|
if (!StorageDb || !AuditRecords) {
|
|
3104
3620
|
return null;
|
|
3105
3621
|
}
|
|
3622
|
+
// Audit follows the captured run owner even if the visible page changes.
|
|
3623
|
+
// Missing owner identity must never be filled with a later account.
|
|
3624
|
+
const accountScopeId = currentRunView ? currentRunView.runAccountScopeId : cachedAccountScopeId;
|
|
3625
|
+
const projectId = currentRunView ? currentRunView.runProjectId : getCurrentProjectId();
|
|
3626
|
+
if (!accountScopeId || !projectId) return null;
|
|
3106
3627
|
const executionSnapshot = input.executionSnapshot
|
|
3107
3628
|
|| currentRunView?.executionSnapshot
|
|
3108
3629
|
|| captureCurrentExecutionSnapshot();
|
|
3109
3630
|
const draft = AuditRecords.buildAuditDraftRecord({
|
|
3110
|
-
|
|
3631
|
+
accountScopeId,
|
|
3632
|
+
projectId,
|
|
3111
3633
|
sessionId: input.sessionId || '',
|
|
3112
3634
|
turnId: currentRunView?.recordId || '',
|
|
3113
3635
|
mode: input.mode || executionSnapshot.mode,
|
|
3636
|
+
requireReviewing: executionSnapshot.requireReviewing,
|
|
3114
3637
|
...providerSettingsCoordinator.getProviderSnapshot(executionSnapshot.providerId),
|
|
3115
3638
|
providerRevision: executionSnapshot.providerRevision,
|
|
3116
3639
|
model: executionSnapshot.model,
|
|
@@ -3142,8 +3665,10 @@
|
|
|
3142
3665
|
return draft;
|
|
3143
3666
|
}
|
|
3144
3667
|
const record = StorageDb.buildAuditLogRecord
|
|
3145
|
-
? StorageDb.buildAuditLogRecord({ ...draft, ...updates, id: draft.id,
|
|
3146
|
-
|
|
3668
|
+
? StorageDb.buildAuditLogRecord({ ...draft, ...updates, id: draft.id,
|
|
3669
|
+
accountScopeId: draft.accountScopeId, projectId: draft.projectId,
|
|
3670
|
+
completedAt: updates.completedAt || draft.completedAt || '' })
|
|
3671
|
+
: { ...draft, ...updates, id: draft.id, accountScopeId: draft.accountScopeId, projectId: draft.projectId };
|
|
3147
3672
|
return StorageDb.putRecord('auditLogs', record).then(() => record).catch(() => draft);
|
|
3148
3673
|
}
|
|
3149
3674
|
|
|
@@ -3173,121 +3698,10 @@
|
|
|
3173
3698
|
}
|
|
3174
3699
|
|
|
3175
3700
|
async function runSensitivePreflight(options) {
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
return { blocked: false, findings: [] };
|
|
3181
|
-
}
|
|
3182
|
-
if (!SensitiveScan?.scanSensitiveInputs && !useExistingMirror) {
|
|
3183
|
-
return { blocked: false, findings: [] };
|
|
3184
|
-
}
|
|
3185
|
-
let findings = SensitiveScan?.scanSensitiveInputs
|
|
3186
|
-
? SensitiveScan.scanSensitiveInputs({
|
|
3187
|
-
task,
|
|
3188
|
-
files: (project?.files || []).filter(isTextSnapshotFile)
|
|
3189
|
-
})
|
|
3190
|
-
: [];
|
|
3191
|
-
if (useExistingMirror) {
|
|
3192
|
-
findings = dedupeSensitiveFindings([
|
|
3193
|
-
...findings,
|
|
3194
|
-
...await scanNativeMirrorSensitiveFindings()
|
|
3195
|
-
]);
|
|
3196
|
-
}
|
|
3197
|
-
if (!findings.length) {
|
|
3198
|
-
return { blocked: false, findings: [] };
|
|
3199
|
-
}
|
|
3200
|
-
appendRunEvent({
|
|
3201
|
-
title: tx(
|
|
3202
|
-
`Sensitive content check found ${findings.length} item(s).`,
|
|
3203
|
-
`敏感内容检查发现 ${findings.length} 项。`
|
|
3204
|
-
),
|
|
3205
|
-
status: 'failed',
|
|
3206
|
-
detail: findings.slice(0, 8).map(finding => ({
|
|
3207
|
-
[tr('detailFile')]: finding.path || finding.source || 'task',
|
|
3208
|
-
[tr('detailReason')]: finding.detectorId,
|
|
3209
|
-
[tx('Preview', '预览')]: finding.preview || '[REDACTED]'
|
|
3210
|
-
}))
|
|
3211
|
-
});
|
|
3212
|
-
if (normalizedRules.sensitiveConfirmAllowed === true) {
|
|
3213
|
-
const approved = await showPluginConfirm({
|
|
3214
|
-
title: tr('sensitiveConfirmTitle'),
|
|
3215
|
-
message: formatSensitiveConfirmMessage(findings),
|
|
3216
|
-
confirmLabel: tr('sensitiveConfirmRun'),
|
|
3217
|
-
cancelLabel: tr('confirmDefaultCancel'),
|
|
3218
|
-
destructive: true
|
|
3219
|
-
});
|
|
3220
|
-
return { blocked: !approved, findings };
|
|
3221
|
-
}
|
|
3222
|
-
appendCompletionReport({
|
|
3223
|
-
conclusion: tx(
|
|
3224
|
-
'This run was blocked before Codex received project context because sensitive content was detected.',
|
|
3225
|
-
'检测到敏感内容,本轮已在向 Codex 发送项目上下文前阻止。'
|
|
3226
|
-
),
|
|
3227
|
-
status: 'blocked',
|
|
3228
|
-
operations: [],
|
|
3229
|
-
applyResults: [],
|
|
3230
|
-
nextStep: tx(
|
|
3231
|
-
'Remove the sensitive content, narrow @context, or enable explicit sensitive confirmation in Project Settings.',
|
|
3232
|
-
'请移除敏感内容、缩小 @context,或在项目设置里允许敏感内容显式确认。'
|
|
3233
|
-
)
|
|
3234
|
-
});
|
|
3235
|
-
return { blocked: true, findings };
|
|
3236
|
-
}
|
|
3237
|
-
|
|
3238
|
-
function formatSensitiveConfirmMessage(findings = []) {
|
|
3239
|
-
const visible = findings.slice(0, 8).map(finding => {
|
|
3240
|
-
const path = finding.path || finding.source || 'task';
|
|
3241
|
-
return `${path}: ${finding.detectorId || 'sensitive'} (${finding.preview || '[REDACTED]'})`;
|
|
3242
|
-
});
|
|
3243
|
-
return [
|
|
3244
|
-
tr('sensitiveConfirmMessage'),
|
|
3245
|
-
'',
|
|
3246
|
-
...visible,
|
|
3247
|
-
findings.length > visible.length ? tx(`${findings.length - visible.length} more finding(s).`, `另有 ${findings.length - visible.length} 项。`) : ''
|
|
3248
|
-
].filter(Boolean).join('\n');
|
|
3249
|
-
}
|
|
3250
|
-
|
|
3251
|
-
async function scanNativeMirrorSensitiveFindings() {
|
|
3252
|
-
try {
|
|
3253
|
-
const response = await sendBackgroundNative({
|
|
3254
|
-
method: 'mirror.scanSensitive',
|
|
3255
|
-
params: { projectId: getCurrentProjectId() }
|
|
3256
|
-
});
|
|
3257
|
-
if (response?.ok) {
|
|
3258
|
-
return Array.isArray(response.result?.findings) ? response.result.findings : [];
|
|
3259
|
-
}
|
|
3260
|
-
return [{
|
|
3261
|
-
detectorId: 'mirror-sensitive-scan-unavailable',
|
|
3262
|
-
source: 'local-mirror',
|
|
3263
|
-
preview: response?.error?.message || 'Could not scan the local mirror before Codex runs.'
|
|
3264
|
-
}];
|
|
3265
|
-
} catch (error) {
|
|
3266
|
-
return [{
|
|
3267
|
-
detectorId: 'mirror-sensitive-scan-unavailable',
|
|
3268
|
-
source: 'local-mirror',
|
|
3269
|
-
preview: error?.message || 'Could not scan the local mirror before Codex runs.'
|
|
3270
|
-
}];
|
|
3271
|
-
}
|
|
3272
|
-
}
|
|
3273
|
-
|
|
3274
|
-
function dedupeSensitiveFindings(findings = []) {
|
|
3275
|
-
const seen = new Set();
|
|
3276
|
-
const deduped = [];
|
|
3277
|
-
for (const finding of findings || []) {
|
|
3278
|
-
const key = [
|
|
3279
|
-
finding?.detectorId || '',
|
|
3280
|
-
finding?.source || '',
|
|
3281
|
-
finding?.path || '',
|
|
3282
|
-
finding?.preview || ''
|
|
3283
|
-
].join('\u0000');
|
|
3284
|
-
if (seen.has(key)) {
|
|
3285
|
-
continue;
|
|
3286
|
-
}
|
|
3287
|
-
seen.add(key);
|
|
3288
|
-
deduped.push(finding);
|
|
3289
|
-
}
|
|
3290
|
-
return deduped;
|
|
3701
|
+
return Modules.SensitivePreflight.create({
|
|
3702
|
+
SensitiveScan, normalizeGovernanceRules, isTextSnapshotFile, sendBackgroundNative,
|
|
3703
|
+
getCurrentProjectId, appendRunEvent, appendCompletionReport, showPluginConfirm, tr, tx
|
|
3704
|
+
}).run(options);
|
|
3291
3705
|
}
|
|
3292
3706
|
|
|
3293
3707
|
function appendRunCancelledReport() {
|
|
@@ -3298,7 +3712,7 @@
|
|
|
3298
3712
|
status: 'info',
|
|
3299
3713
|
activity: { v: 1, kind: 'lifecycle', state: 'cancelled', target: 'cancelled' }
|
|
3300
3714
|
});
|
|
3301
|
-
appendCompletionReport({
|
|
3715
|
+
appendCompletionReport({ conclusionKey: 'cancelled',
|
|
3302
3716
|
conclusion: tx('This run was cancelled. It did not continue syncing or writing to Overleaf.', '这轮已中断,没有继续同步或写入 Overleaf。'),
|
|
3303
3717
|
status: 'cancelled',
|
|
3304
3718
|
operations: [],
|
|
@@ -3327,7 +3741,8 @@
|
|
|
3327
3741
|
const running = typeof runningOverride === 'boolean'
|
|
3328
3742
|
? runningOverride
|
|
3329
3743
|
: Boolean(currentRunView);
|
|
3330
|
-
const
|
|
3744
|
+
const transitioning = sessionManager.isTransitioning();
|
|
3745
|
+
const action = running && (!hasInput || transitioning) ? 'cancel' : 'send';
|
|
3331
3746
|
const label = action === 'cancel'
|
|
3332
3747
|
? tr('cancelRun')
|
|
3333
3748
|
: running
|
|
@@ -3336,13 +3751,17 @@
|
|
|
3336
3751
|
runButton.dataset.action = action;
|
|
3337
3752
|
runButton.disabled = action === 'cancel'
|
|
3338
3753
|
? runCancellationRequested
|
|
3339
|
-
: !hasInput;
|
|
3754
|
+
: !hasInput || transitioning;
|
|
3340
3755
|
runButton.title = label;
|
|
3341
3756
|
runButton.setAttribute('aria-label', label);
|
|
3342
|
-
if (currentRunView?.writebackRetry && hasInput) runButton.disabled = true;
|
|
3757
|
+
if (currentRunView?.writebackRetry && hasInput && action !== 'cancel') runButton.disabled = true;
|
|
3343
3758
|
}
|
|
3344
3759
|
|
|
3345
3760
|
function handleTaskInputKeydown(event) {
|
|
3761
|
+
if (sessionManager.isTransitioning()) {
|
|
3762
|
+
if (event.key === 'Enter' && !event.isComposing && event.keyCode !== 229) event.preventDefault();
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3346
3765
|
if (handleSlashMenuKeydown(event)) {
|
|
3347
3766
|
return;
|
|
3348
3767
|
}
|
|
@@ -3354,6 +3773,7 @@
|
|
|
3354
3773
|
}
|
|
3355
3774
|
|
|
3356
3775
|
function handleTaskInput() {
|
|
3776
|
+
composerDraftRevision += 1;
|
|
3357
3777
|
autosizeTaskTextarea();
|
|
3358
3778
|
syncComposerSendAvailability();
|
|
3359
3779
|
renderPendingInputs();
|
|
@@ -3369,17 +3789,18 @@
|
|
|
3369
3789
|
|
|
3370
3790
|
function getSlashCommands() {
|
|
3371
3791
|
const commands = [
|
|
3792
|
+
{ id: 'compact', kind: 'compact', group: 'commands', title: tr('slashCompactTitle'), slug: '/compact',
|
|
3793
|
+
subtitle: tr('slashCompactSubtitle') },
|
|
3372
3794
|
{
|
|
3373
3795
|
id: 'install-skill',
|
|
3374
3796
|
kind: 'installer',
|
|
3797
|
+
group: 'commands',
|
|
3375
3798
|
title: tr('slashInstallSkillTitle'),
|
|
3376
3799
|
subtitle: tr('slashInstallSkillSubtitle')
|
|
3377
3800
|
}
|
|
3378
3801
|
];
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
}
|
|
3382
|
-
for (const skill of slashCodexOverleafSkills) {
|
|
3802
|
+
const settings = getSkillLoadingSettings();
|
|
3803
|
+
for (const skill of settings.loadCodexOverleafSkills === false ? [] : slashCodexOverleafSkills) {
|
|
3383
3804
|
const id = String(skill?.id || '').trim();
|
|
3384
3805
|
if (!isSafeSkillId(id)) {
|
|
3385
3806
|
continue;
|
|
@@ -3392,16 +3813,34 @@
|
|
|
3392
3813
|
commands.push({
|
|
3393
3814
|
id: `skill:${id}`,
|
|
3394
3815
|
kind: 'codex-overleaf-skill',
|
|
3816
|
+
group: 'overleaf',
|
|
3395
3817
|
scope: 'codex-overleaf',
|
|
3396
3818
|
skillId: id,
|
|
3397
3819
|
title,
|
|
3398
|
-
subtitle: tr('
|
|
3820
|
+
subtitle: tr('slashUseOverleafSkillSubtitle')
|
|
3399
3821
|
});
|
|
3400
3822
|
}
|
|
3823
|
+
const localSkills = settings.loadCodexLocalSkills === false ? [] : (getCodexLocalSkillsCatalog() || []);
|
|
3824
|
+
for (const skill of localSkills) {
|
|
3825
|
+
if (!isSafeSkillId(skill.id) || !isCodexLocalSkillEnabled(skill.id, skill)) continue;
|
|
3826
|
+
const sameName = localSkills.filter(other => String(other.title || other.name || other.id) === String(skill.title || skill.name || skill.id)).length > 1;
|
|
3827
|
+
// Source and duplicate hints lead so a truncated preview cannot hide them.
|
|
3828
|
+
commands.push({ id: `local-skill:${skill.id}`, kind: 'codex-local-skill', group: 'local', scope: 'codex-local', skillId: skill.id,
|
|
3829
|
+
title: String(skill.title || skill.name || skill.id).trim().slice(0, 80),
|
|
3830
|
+
searchName: String(skill.name || ''),
|
|
3831
|
+
subtitle: [skill.sourceKind === 'plugin' && skill.sourceLabel ? skill.sourceLabel : '',
|
|
3832
|
+
sameName ? skill.sourceHint || skill.id.slice(-8) : '',
|
|
3833
|
+
String(skill.preview || '').trim() || tr('slashUseLocalSkillSubtitle')].filter(Boolean).join(' · ') });
|
|
3834
|
+
}
|
|
3401
3835
|
return commands;
|
|
3402
3836
|
}
|
|
3403
3837
|
|
|
3404
3838
|
async function refreshCodexOverleafSkillsForSlashMenu() {
|
|
3839
|
+
if (getSlashTrigger() && getSkillLoadingSettings().loadCodexLocalSkills !== false) {
|
|
3840
|
+
primeCodexLocalSkillsCatalog().catch(() => {
|
|
3841
|
+
showPluginToast(tx('Could not load local Codex skills. Try again.', '无法加载 Codex 本地技能,请重试。'), { status: 'failed' });
|
|
3842
|
+
});
|
|
3843
|
+
}
|
|
3405
3844
|
if (slashCodexOverleafSkillsLoaded || slashCodexOverleafSkillsLoading) {
|
|
3406
3845
|
return;
|
|
3407
3846
|
}
|
|
@@ -3435,9 +3874,10 @@
|
|
|
3435
3874
|
const commands = getSlashCommands().filter(command => {
|
|
3436
3875
|
return !query
|
|
3437
3876
|
|| command.title.toLowerCase().includes(query)
|
|
3438
|
-
|| command.id.toLowerCase().includes(query)
|
|
3877
|
+
|| command.id.toLowerCase().includes(query)
|
|
3878
|
+
|| String(command.searchName || '').toLowerCase().includes(query);
|
|
3439
3879
|
});
|
|
3440
|
-
renderSlashMenu(commands);
|
|
3880
|
+
renderSlashMenu(commands, { query });
|
|
3441
3881
|
return;
|
|
3442
3882
|
}
|
|
3443
3883
|
const atTrigger = getAtFileTrigger();
|
|
@@ -3535,7 +3975,7 @@
|
|
|
3535
3975
|
closeSlashMenu();
|
|
3536
3976
|
return;
|
|
3537
3977
|
}
|
|
3538
|
-
renderSlashMenu(entries);
|
|
3978
|
+
renderSlashMenu(entries, { query });
|
|
3539
3979
|
}
|
|
3540
3980
|
|
|
3541
3981
|
// Lazy one-shot load: the tray's file list is usually warm (panel open
|
|
@@ -3618,7 +4058,22 @@
|
|
|
3618
4058
|
};
|
|
3619
4059
|
}
|
|
3620
4060
|
|
|
3621
|
-
|
|
4061
|
+
const SLASH_MENU_GROUPS = { commands: 'slashGroupCommands', overleaf: 'slashGroupOverleafSkills', local: 'slashGroupLocalSkills' };
|
|
4062
|
+
const SLASH_MENU_ICONS = { compact: 'compact', installer: 'install', 'codex-overleaf-skill': 'book', 'codex-local-skill': 'terminal' };
|
|
4063
|
+
|
|
4064
|
+
function appendHighlightedText(node, value, query) {
|
|
4065
|
+
const text = String(value || '');
|
|
4066
|
+
const at = query ? text.toLowerCase().indexOf(query) : -1;
|
|
4067
|
+
if (at < 0) {
|
|
4068
|
+
node.append(text);
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
const mark = document.createElement('mark');
|
|
4072
|
+
mark.textContent = text.slice(at, at + query.length);
|
|
4073
|
+
node.append(text.slice(0, at), mark, text.slice(at + query.length));
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
function renderSlashMenu(commands = [], { query = '' } = {}) {
|
|
3622
4077
|
const menu = panel?.querySelector('[data-slash-menu]');
|
|
3623
4078
|
if (!menu) {
|
|
3624
4079
|
return;
|
|
@@ -3628,7 +4083,17 @@
|
|
|
3628
4083
|
menu.hidden = true;
|
|
3629
4084
|
return;
|
|
3630
4085
|
}
|
|
4086
|
+
const usage = commands.some(command => command.kind === 'compact') ? getContextUsageDisplay().usage : null;
|
|
4087
|
+
const percent = usage?.contextWindow > 0 ? usage.usedTokens / usage.contextWindow * 100 : null;
|
|
4088
|
+
let group = '';
|
|
3631
4089
|
commands.forEach((command, index) => {
|
|
4090
|
+
if (command.group && command.group !== group) {
|
|
4091
|
+
group = command.group;
|
|
4092
|
+
const label = document.createElement('div');
|
|
4093
|
+
label.className = 'codex-slash-menu-group';
|
|
4094
|
+
label.textContent = tr(SLASH_MENU_GROUPS[group]);
|
|
4095
|
+
menu.append(label);
|
|
4096
|
+
}
|
|
3632
4097
|
const button = document.createElement('button');
|
|
3633
4098
|
button.type = 'button';
|
|
3634
4099
|
button.dataset.slashCommand = command.id;
|
|
@@ -3640,16 +4105,58 @@
|
|
|
3640
4105
|
button.dataset.slashSkillScope = command.scope;
|
|
3641
4106
|
}
|
|
3642
4107
|
button.dataset.active = index === 0 ? 'true' : 'false';
|
|
4108
|
+
const icon = document.createElement('span');
|
|
4109
|
+
icon.className = 'codex-slash-menu-icon';
|
|
4110
|
+
icon.innerHTML = Modules.PanelIcons.svg(SLASH_MENU_ICONS[command.kind] || 'file');
|
|
4111
|
+
const text = document.createElement('span');
|
|
4112
|
+
text.className = 'codex-slash-menu-text';
|
|
3643
4113
|
const title = document.createElement('span');
|
|
3644
|
-
title.
|
|
4114
|
+
title.className = 'codex-slash-menu-title';
|
|
4115
|
+
appendHighlightedText(title, command.title, query);
|
|
4116
|
+
if (command.slug) {
|
|
4117
|
+
const slug = document.createElement('code');
|
|
4118
|
+
appendHighlightedText(slug, command.slug, query);
|
|
4119
|
+
title.append(slug);
|
|
4120
|
+
}
|
|
3645
4121
|
const subtitle = document.createElement('small');
|
|
3646
4122
|
subtitle.textContent = command.subtitle;
|
|
3647
|
-
|
|
4123
|
+
subtitle.title = command.subtitle;
|
|
4124
|
+
text.append(title, subtitle);
|
|
4125
|
+
button.append(icon, text);
|
|
4126
|
+
if (command.kind === 'compact' && percent !== null) {
|
|
4127
|
+
const meta = document.createElement('span');
|
|
4128
|
+
meta.className = 'codex-slash-menu-meta';
|
|
4129
|
+
meta.dataset.level = Modules.ContextUsage.level(percent);
|
|
4130
|
+
meta.title = tr('contextUsageTitle');
|
|
4131
|
+
meta.textContent = new Intl.NumberFormat(getLocale() === 'zh' ? 'zh-CN' : 'en-US', { maximumFractionDigits: 0 }).format(percent) + '%';
|
|
4132
|
+
button.append(meta);
|
|
4133
|
+
}
|
|
3648
4134
|
menu.append(button);
|
|
3649
4135
|
});
|
|
4136
|
+
const foot = document.createElement('div');
|
|
4137
|
+
foot.className = 'codex-slash-menu-foot';
|
|
4138
|
+
foot.dataset.slashMenuFoot = '';
|
|
4139
|
+
for (const [keys, label] of [[['↑', '↓'], 'slashKeyMove'], [['Enter'], 'slashKeyRun'], [['Esc'], 'slashKeyClose']]) {
|
|
4140
|
+
const hint = document.createElement('span');
|
|
4141
|
+
for (const key of keys) hint.append(Object.assign(document.createElement('kbd'), { textContent: key }));
|
|
4142
|
+
hint.append(tr(label));
|
|
4143
|
+
foot.append(hint);
|
|
4144
|
+
}
|
|
4145
|
+
menu.append(foot);
|
|
3650
4146
|
renderedSlashCommands = new Map(commands.map(command => [command.id, command]));
|
|
3651
4147
|
menu.dataset.activeIndex = '0';
|
|
3652
4148
|
menu.hidden = false;
|
|
4149
|
+
menu.scrollTop = 0;
|
|
4150
|
+
updateSlashMenuLayout();
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
function updateSlashMenuLayout() {
|
|
4154
|
+
const menu = panel?.querySelector('[data-slash-menu]');
|
|
4155
|
+
const composer = panel?.querySelector('[data-composer-form]');
|
|
4156
|
+
if (!menu || menu.hidden || !composer) return;
|
|
4157
|
+
const visibleTop = Math.max(0, root.visualViewport?.offsetTop || 0, panel.getBoundingClientRect().top);
|
|
4158
|
+
const available = Math.max(0, Math.floor(composer.getBoundingClientRect().top - visibleTop - 12));
|
|
4159
|
+
menu.style.setProperty('--codex-slash-menu-space', `${available}px`);
|
|
3653
4160
|
}
|
|
3654
4161
|
|
|
3655
4162
|
function handleSlashMenuClick(event) {
|
|
@@ -3708,9 +4215,28 @@
|
|
|
3708
4215
|
buttons.forEach((button, buttonIndex) => {
|
|
3709
4216
|
button.dataset.active = buttonIndex === index ? 'true' : 'false';
|
|
3710
4217
|
});
|
|
4218
|
+
const active = buttons[index];
|
|
4219
|
+
if (!active || !menu.clientHeight) return;
|
|
4220
|
+
// Wrapping to the first item also reveals its group label.
|
|
4221
|
+
if (index === 0) {
|
|
4222
|
+
menu.scrollTop = 0;
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
const viewport = menu.getBoundingClientRect();
|
|
4226
|
+
const item = active.getBoundingClientRect();
|
|
4227
|
+
const top = viewport.top + menu.clientTop;
|
|
4228
|
+
// The sticky key-hint footer covers the bottom of the scroll area.
|
|
4229
|
+
const bottom = top + menu.clientHeight - (menu.querySelector('[data-slash-menu-foot]')?.offsetHeight || 0);
|
|
4230
|
+
// Scroll only this popup; scrollIntoView may move the chat or editor too.
|
|
4231
|
+
if (item.height > menu.clientHeight || item.top < top) {
|
|
4232
|
+
menu.scrollTop += item.top - top;
|
|
4233
|
+
} else if (item.bottom > bottom) {
|
|
4234
|
+
menu.scrollTop += item.bottom - bottom;
|
|
4235
|
+
}
|
|
3711
4236
|
}
|
|
3712
4237
|
|
|
3713
4238
|
function selectSlashCommand(commandId) {
|
|
4239
|
+
if (sessionManager.isTransitioning()) return;
|
|
3714
4240
|
const command = renderedSlashCommands.get(commandId);
|
|
3715
4241
|
if (!command) {
|
|
3716
4242
|
closeSlashMenu();
|
|
@@ -3723,13 +4249,18 @@
|
|
|
3723
4249
|
applyAtFileSelection(command);
|
|
3724
4250
|
return;
|
|
3725
4251
|
}
|
|
4252
|
+
if (command.kind === 'compact') {
|
|
4253
|
+
closeSlashMenu();
|
|
4254
|
+
safeRunTask({ operation: 'compact', consumeCompactDraft: true });
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
3726
4257
|
clearSlashTriggerFromTaskInput();
|
|
3727
4258
|
closeSlashMenu();
|
|
3728
4259
|
if (command.kind === 'installer') {
|
|
3729
4260
|
activateSkillInstallerComposerContext();
|
|
3730
4261
|
return;
|
|
3731
4262
|
}
|
|
3732
|
-
if (command.kind === 'codex-overleaf-skill') {
|
|
4263
|
+
if (command.kind === 'codex-overleaf-skill' || command.kind === 'codex-local-skill') {
|
|
3733
4264
|
activateCodexOverleafSkillComposerContext(command);
|
|
3734
4265
|
}
|
|
3735
4266
|
}
|
|
@@ -3756,6 +4287,7 @@
|
|
|
3756
4287
|
}
|
|
3757
4288
|
|
|
3758
4289
|
function activateSkillInstallerComposerContext() {
|
|
4290
|
+
composerDraftRevision += 1;
|
|
3759
4291
|
composerSkillInvocation = {
|
|
3760
4292
|
id: 'skill-installer',
|
|
3761
4293
|
title: tr('skillInstallerComposerLabel')
|
|
@@ -3766,6 +4298,7 @@
|
|
|
3766
4298
|
}
|
|
3767
4299
|
|
|
3768
4300
|
function activateCodexOverleafSkillComposerContext(command) {
|
|
4301
|
+
composerDraftRevision += 1;
|
|
3769
4302
|
const id = String(command?.skillId || '').trim();
|
|
3770
4303
|
if (!isSafeSkillId(id)) {
|
|
3771
4304
|
return;
|
|
@@ -3773,7 +4306,7 @@
|
|
|
3773
4306
|
composerSkillInvocation = {
|
|
3774
4307
|
id,
|
|
3775
4308
|
title: String(command?.title || id).trim().slice(0, 80) || id,
|
|
3776
|
-
scope: 'codex-overleaf'
|
|
4309
|
+
scope: command.scope === 'codex-local' ? 'codex-local' : 'codex-overleaf'
|
|
3777
4310
|
};
|
|
3778
4311
|
renderComposerSkillInvocation();
|
|
3779
4312
|
const input = panel?.querySelector('[data-task]');
|
|
@@ -3781,6 +4314,7 @@
|
|
|
3781
4314
|
}
|
|
3782
4315
|
|
|
3783
4316
|
function clearComposerSkillInvocation() {
|
|
4317
|
+
composerDraftRevision += 1;
|
|
3784
4318
|
composerSkillInvocation = null;
|
|
3785
4319
|
renderComposerSkillInvocation();
|
|
3786
4320
|
panel?.querySelector('[data-task]')?.focus?.();
|
|
@@ -3791,7 +4325,7 @@
|
|
|
3791
4325
|
if (!normalized) {
|
|
3792
4326
|
return null;
|
|
3793
4327
|
}
|
|
3794
|
-
if (normalized.id === 'skill-installer') {
|
|
4328
|
+
if (normalized.id === 'skill-installer' && !normalized.scope) {
|
|
3795
4329
|
return {
|
|
3796
4330
|
id: 'skill-installer',
|
|
3797
4331
|
title: tr('skillInstallerComposerLabel')
|
|
@@ -3806,13 +4340,9 @@
|
|
|
3806
4340
|
return null;
|
|
3807
4341
|
}
|
|
3808
4342
|
const title = String(value?.title || id).trim().slice(0, 80) || id;
|
|
3809
|
-
if (id === 'skill-installer') {
|
|
3810
|
-
|
|
3811
|
-
}
|
|
3812
|
-
if (value?.scope !== 'codex-overleaf') {
|
|
3813
|
-
return null;
|
|
3814
|
-
}
|
|
3815
|
-
return { id, title, scope: 'codex-overleaf' };
|
|
4343
|
+
if (id === 'skill-installer' && (!value?.scope || value.scope === 'builtin')) return { id, title };
|
|
4344
|
+
if (!['codex-overleaf', 'codex-local'].includes(value?.scope)) return null;
|
|
4345
|
+
return { id, title, scope: value.scope };
|
|
3816
4346
|
}
|
|
3817
4347
|
|
|
3818
4348
|
function isSafeSkillId(id) {
|
|
@@ -3833,7 +4363,7 @@
|
|
|
3833
4363
|
}
|
|
3834
4364
|
const clear = context.querySelector('[data-composer-skill-clear]');
|
|
3835
4365
|
if (clear) {
|
|
3836
|
-
const clearLabel = active?.id === 'skill-installer'
|
|
4366
|
+
const clearLabel = active?.id === 'skill-installer' && !active.scope
|
|
3837
4367
|
? tr('skillInstallerComposerClear')
|
|
3838
4368
|
: tr('composerSkillClear');
|
|
3839
4369
|
clear.title = clearLabel;
|
|
@@ -3996,18 +4526,17 @@
|
|
|
3996
4526
|
// it from cross-project queries (spec §5.2 storage rules).
|
|
3997
4527
|
window.codexOverleafDeriveAccountScopeId = () => cachedAccountScopeId;
|
|
3998
4528
|
var sharedSessionViewSync = Modules.SharedSessionViewSync.install({
|
|
3999
|
-
Bridge: Modules.SharedSessionBridge,
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
apply: sessions => {
|
|
4009
|
-
state = normalizePanelState({ ...state, sessions }, { restoreRunningRuns: false });
|
|
4529
|
+
Bridge: Modules.SharedSessionBridge, StorageDb: Modules.StorageDb,
|
|
4530
|
+
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId(), generation: spaRouteGeneration }),
|
|
4531
|
+
getState: () => state, setState: next => { state = next; },
|
|
4532
|
+
// An in-flight Accept/Undo keeps its view until it settles; refresh applies afterwards.
|
|
4533
|
+
isBusy: () => Boolean(pendingDraftSave || readLiveRunViewForSaveStateGuard() || trackedChangeInFlight.size
|
|
4534
|
+
|| runReviewController?.hasActiveReview?.() || document.querySelector('dialog[open]')),
|
|
4535
|
+
hasLocalDraft: current => Boolean(current.task) && composerDraftEditedHere,
|
|
4536
|
+
normalize: normalizePanelState,
|
|
4537
|
+
render: () => {
|
|
4010
4538
|
applyStateToPanel();
|
|
4539
|
+
composerDraftEditedHere = false;
|
|
4011
4540
|
},
|
|
4012
4541
|
notice: error => appendStorageNoticeOnce('shared-history-refresh', tx(
|
|
4013
4542
|
`Could not refresh shared history: ${error.message}`,
|
|
@@ -4092,7 +4621,7 @@
|
|
|
4092
4621
|
chromeApi: chrome, getState: () => state, setState: next => { state = next; }, getPanel: () => panel,
|
|
4093
4622
|
applyTheme: applyPanelTheme, applyLocale: () => { applyLocaleToPanel(); refreshDashboardSoon(); },
|
|
4094
4623
|
onPreloadChange: enabled => contextTrayController.scheduleContextPrefetch({ enabled }),
|
|
4095
|
-
onSkillsChange: () => { renderLocalSkillList(); updateSkillsEntrySummary(); },
|
|
4624
|
+
onSkillsChange: () => { renderLocalSkillList(); updateSkillsEntrySummary(); updateSlashMenuForTaskInput(); },
|
|
4096
4625
|
onError: error => showPluginToast(tx(
|
|
4097
4626
|
`Global settings could not be saved or loaded: ${error.message}. Please retry.`,
|
|
4098
4627
|
`全局设置未能保存或读取:${error.message}。请重试。`), { status: 'failed', sticky: true })
|
|
@@ -4108,6 +4637,8 @@
|
|
|
4108
4637
|
storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
|
|
4109
4638
|
const reloaded = await loadStoredState();
|
|
4110
4639
|
state = normalizeLoadedPanelState(reloaded);
|
|
4640
|
+
composerDraftEditedHere = false;
|
|
4641
|
+
await getRunReviewController().recoverInterruptedUndoStatuses();
|
|
4111
4642
|
applyStateToPanel();
|
|
4112
4643
|
} catch (_error) {
|
|
4113
4644
|
// Reload failures fall back to the in-memory state; the next saveState
|
|
@@ -4169,6 +4700,8 @@
|
|
|
4169
4700
|
function leaveActiveProject(newId) {
|
|
4170
4701
|
const prevId = activeProjectId;
|
|
4171
4702
|
activeProjectId = newId; // null for non-project URLs (spec §5.7.1)
|
|
4703
|
+
if (prevId !== newId) composerDraftEditedHere = false;
|
|
4704
|
+
getComposerDraftPersistence().syncScope();
|
|
4172
4705
|
if (!prevId || prevId === newId) {
|
|
4173
4706
|
return;
|
|
4174
4707
|
}
|
|
@@ -4194,7 +4727,15 @@
|
|
|
4194
4727
|
}
|
|
4195
4728
|
|
|
4196
4729
|
async function onSpaRouteChange() {
|
|
4730
|
+
// Snapshot the departing draft before hydration replaces state. The detached
|
|
4731
|
+
// writer retains its original account/project and never reads the new view's attachments.
|
|
4732
|
+
if (pendingDraftSave?.projectId === activeProjectId
|
|
4733
|
+
&& pendingDraftSave?.accountScopeId === cachedAccountScopeId) {
|
|
4734
|
+
void saveState({ draftScope: pendingDraftSave });
|
|
4735
|
+
}
|
|
4736
|
+
resetDraftSave();
|
|
4197
4737
|
const generation = ++spaRouteGeneration;
|
|
4738
|
+
activeTurnControl.cancelRecovery();
|
|
4198
4739
|
const url = new URL(window.location.href);
|
|
4199
4740
|
const pathname = url.pathname;
|
|
4200
4741
|
const previousProjectId = activeProjectId;
|
|
@@ -4209,6 +4750,7 @@
|
|
|
4209
4750
|
// project derivation: account menu is global chrome and should be
|
|
4210
4751
|
// readable on per-project URLs too).
|
|
4211
4752
|
await refreshAccountScopeId();
|
|
4753
|
+
getComposerDraftPersistence().syncScope();
|
|
4212
4754
|
if (generation !== spaRouteGeneration || pathname !== window.location.pathname) {
|
|
4213
4755
|
return;
|
|
4214
4756
|
}
|
|
@@ -4417,7 +4959,7 @@
|
|
|
4417
4959
|
operations: [],
|
|
4418
4960
|
status: tr('modeAsk')
|
|
4419
4961
|
});
|
|
4420
|
-
appendCompletionReport({
|
|
4962
|
+
appendCompletionReport({ conclusionKey: !notes ? 'ask_inspected' : undefined,
|
|
4421
4963
|
conclusion: notes || tx('This run only inspected and explained; no files were written.', '这轮只做了检查和说明,没有写入文件。'),
|
|
4422
4964
|
status: tr('modeAsk'),
|
|
4423
4965
|
notes,
|
|
@@ -4437,7 +4979,7 @@
|
|
|
4437
4979
|
const applied = await applyTaskOperations(project, operations);
|
|
4438
4980
|
applyResults.push(applied);
|
|
4439
4981
|
appendApplyResult(applied);
|
|
4440
|
-
recordUndoFromApply(project, applied);
|
|
4982
|
+
await recordUndoFromApply(project, applied);
|
|
4441
4983
|
const approved = await showPluginConfirm({
|
|
4442
4984
|
title: tx('Confirm delete plan?', '确认删除计划?'),
|
|
4443
4985
|
message: formatDeletePlan(result.deletePlan || []),
|
|
@@ -4451,7 +4993,7 @@
|
|
|
4451
4993
|
const deleteApplied = await applyTaskOperations(project, pendingOperations, { allowHighRisk: true });
|
|
4452
4994
|
applyResults.push(deleteApplied);
|
|
4453
4995
|
appendApplyResult(deleteApplied);
|
|
4454
|
-
recordUndoFromApply(project, deleteApplied);
|
|
4996
|
+
await recordUndoFromApply(project, deleteApplied);
|
|
4455
4997
|
appendLog(tx('Deleted files after confirmation.', '已按确认删除文件。'));
|
|
4456
4998
|
const summaryLine = appendChangeSummary({
|
|
4457
4999
|
notes,
|
|
@@ -4459,7 +5001,7 @@
|
|
|
4459
5001
|
applyResults,
|
|
4460
5002
|
status: 'applied with delete plan'
|
|
4461
5003
|
});
|
|
4462
|
-
appendCompletionReport({
|
|
5004
|
+
appendCompletionReport({ conclusionKey: !notes ? 'delete_handled' : undefined,
|
|
4463
5005
|
conclusion: notes || tx('Changes were written, and delete items were handled after your confirmation.', '已写入修改,并按你的确认处理删除项。'),
|
|
4464
5006
|
status: 'applied with delete plan',
|
|
4465
5007
|
notes,
|
|
@@ -4482,7 +5024,7 @@
|
|
|
4482
5024
|
status: 'applied without deletes',
|
|
4483
5025
|
deletePlanRejected: true
|
|
4484
5026
|
});
|
|
4485
|
-
appendCompletionReport({
|
|
5027
|
+
appendCompletionReport({ conclusionKey: 'delete_rejected',
|
|
4486
5028
|
conclusion: tx('Non-delete changes were kept; delete items were not written.', '已保留非删除修改;删除项没有写入。'),
|
|
4487
5029
|
status: 'applied without deletes',
|
|
4488
5030
|
notes,
|
|
@@ -4504,7 +5046,7 @@
|
|
|
4504
5046
|
appendOperationsPreview(operations, tx('Preparing to write', '准备写入'));
|
|
4505
5047
|
const applied = await applyTaskOperations(project, operations);
|
|
4506
5048
|
appendApplyResult(applied);
|
|
4507
|
-
recordUndoFromApply(project, applied);
|
|
5049
|
+
await recordUndoFromApply(project, applied);
|
|
4508
5050
|
appendLog(mode === 'auto' ? tx('Auto write task completed.', '自动写入任务完成。') : tx('Task completed.', '任务完成。'));
|
|
4509
5051
|
const summaryLine = appendChangeSummary({
|
|
4510
5052
|
notes,
|
|
@@ -4512,7 +5054,7 @@
|
|
|
4512
5054
|
applyResults: [applied],
|
|
4513
5055
|
status: result.status || 'completed'
|
|
4514
5056
|
});
|
|
4515
|
-
appendCompletionReport({
|
|
5057
|
+
appendCompletionReport({ conclusionKey: !notes ? 'run_complete' : undefined,
|
|
4516
5058
|
conclusion: notes || tx('This run is complete.', '这轮任务已完成。'),
|
|
4517
5059
|
status: result.status || 'completed',
|
|
4518
5060
|
notes,
|
|
@@ -4529,6 +5071,7 @@
|
|
|
4529
5071
|
}
|
|
4530
5072
|
|
|
4531
5073
|
async function applyTaskOperations(project, operations, options = {}) {
|
|
5074
|
+
const target = currentRunView;
|
|
4532
5075
|
const runRequireReviewing = typeof options.requireReviewing === 'boolean'
|
|
4533
5076
|
? options.requireReviewing
|
|
4534
5077
|
: state.requireReviewing === true;
|
|
@@ -4538,7 +5081,13 @@
|
|
|
4538
5081
|
title: tr('undoNoWritesTitle'),
|
|
4539
5082
|
status: 'completed'
|
|
4540
5083
|
});
|
|
4541
|
-
}
|
|
5084
|
+
}
|
|
5085
|
+
const reviewing = await ensureReviewingBeforeWrite(partitioned.safe, { requireReviewing: runRequireReviewing });
|
|
5086
|
+
if (!reviewing.ok) {
|
|
5087
|
+
return buildReviewingBlockedApplyResult(partitioned.safe, reviewing, partitioned.skipped);
|
|
5088
|
+
}
|
|
5089
|
+
if (partitioned.safe.length) await stageWritebackRecovery(partitioned.safe, target, runRequireReviewing, project);
|
|
5090
|
+
if (partitioned.safe.length) {
|
|
4542
5091
|
appendRunEvent({
|
|
4543
5092
|
title: tx('Saved a recoverable version before this run writes.', '已保存本轮写入前的可恢复版本。'),
|
|
4544
5093
|
status: 'completed',
|
|
@@ -4547,10 +5096,7 @@
|
|
|
4547
5096
|
}
|
|
4548
5097
|
});
|
|
4549
5098
|
}
|
|
4550
|
-
|
|
4551
|
-
if (!reviewing.ok) {
|
|
4552
|
-
return buildReviewingBlockedApplyResult(partitioned.safe, reviewing, partitioned.skipped);
|
|
4553
|
-
}
|
|
5099
|
+
|
|
4554
5100
|
const applied = partitioned.safe.length
|
|
4555
5101
|
? await callPageBridge('applyOperations', {
|
|
4556
5102
|
operations: partitioned.safe,
|
|
@@ -4558,7 +5104,7 @@
|
|
|
4558
5104
|
requireReviewing: runRequireReviewing,
|
|
4559
5105
|
requireEditing: !runRequireReviewing,
|
|
4560
5106
|
// Welcome-panel + write-guard:
|
|
4561
|
-
runProjectId:
|
|
5107
|
+
runProjectId: target?.runProjectId || ''
|
|
4562
5108
|
})
|
|
4563
5109
|
: { ok: true, applied: [], skipped: [] };
|
|
4564
5110
|
|
|
@@ -4590,10 +5136,19 @@
|
|
|
4590
5136
|
// Thread runProjectId so the page-side dispatcher gates the toggle. Same
|
|
4591
5137
|
// intent as the pre-flight check above: do not flip Track Changes /
|
|
4592
5138
|
// Editing in a different project than the run is bound to.
|
|
5139
|
+
const owner = currentRunView;
|
|
4593
5140
|
const result = await callPageBridge(method, {
|
|
4594
5141
|
waitMs: 1800,
|
|
4595
5142
|
runProjectId: currentRunView?.runProjectId || ''
|
|
4596
5143
|
});
|
|
5144
|
+
throwIfRunCancellationRequested();
|
|
5145
|
+
if (owner && (owner !== currentRunView || owner.runProjectId !== getCurrentProjectId()
|
|
5146
|
+
|| owner.runAccountScopeId !== cachedAccountScopeId)) throw createRunCancellationError();
|
|
5147
|
+
if (requireReviewing && (result?.ok || result?.code === 'reviewing_unavailable')) {
|
|
5148
|
+
projectSettingsCoordinator.observeReviewingAvailability(result.ok ? 'available' : 'unavailable',
|
|
5149
|
+
result.ok ? 'reviewing_confirmed' : 'upgrade_modal');
|
|
5150
|
+
syncProbeStatus();
|
|
5151
|
+
}
|
|
4597
5152
|
if (result?.ok) {
|
|
4598
5153
|
appendRunEvent({
|
|
4599
5154
|
title: requireReviewing
|
|
@@ -4625,6 +5180,7 @@
|
|
|
4625
5180
|
code: result?.code || (requireReviewing ? 'reviewing_not_enabled' : 'editing_not_confirmed'),
|
|
4626
5181
|
reason: result?.reason || (requireReviewing ? 'Reviewing/Track Changes was not enabled' : 'Editing mode was not confirmed'),
|
|
4627
5182
|
reviewing: result?.reviewing || null,
|
|
5183
|
+
evidence: result?.evidence, failure: result?.failure,
|
|
4628
5184
|
requireReviewing
|
|
4629
5185
|
};
|
|
4630
5186
|
}
|
|
@@ -4639,8 +5195,9 @@
|
|
|
4639
5195
|
operation,
|
|
4640
5196
|
result: {
|
|
4641
5197
|
ok: false,
|
|
4642
|
-
code: blockedByEditing ? 'editing_not_confirmed' : 'reviewing_not_enabled',
|
|
4643
|
-
|
|
5198
|
+
code: reviewing?.code || (blockedByEditing ? 'editing_not_confirmed' : 'reviewing_not_enabled'),
|
|
5199
|
+
evidence: reviewing?.evidence, failure: reviewing?.failure,
|
|
5200
|
+
reason: reviewing?.code === 'reviewing_unavailable' ? tr('failureReason_reviewing_unavailable_user') : blockedByEditing
|
|
4644
5201
|
? tx(
|
|
4645
5202
|
'Track is off, but Overleaf Editing mode was not verified before writing. Codex did not write this file.',
|
|
4646
5203
|
'已关闭“留痕”,但写入前没有确认 Overleaf 正在用 Editing 模式。Codex 没有写入这个文件。'
|
|
@@ -4694,13 +5251,13 @@
|
|
|
4694
5251
|
|
|
4695
5252
|
async function refreshProbe(options = {}) {
|
|
4696
5253
|
const projectId = getCurrentProjectId();
|
|
5254
|
+
const accountScopeId = cachedAccountScopeId;
|
|
4697
5255
|
const userInitiated = options.userInitiated === true;
|
|
4698
5256
|
if (userInitiated) {
|
|
4699
5257
|
setRefreshProbeLoading(true);
|
|
4700
5258
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4701
5259
|
if (status) {
|
|
4702
|
-
status
|
|
4703
|
-
status.dataset.ok = 'false';
|
|
5260
|
+
renderProbeStatusMessage(status, tr('refreshProbeLoading'));
|
|
4704
5261
|
status.dataset.refreshing = 'true';
|
|
4705
5262
|
}
|
|
4706
5263
|
}
|
|
@@ -4710,13 +5267,14 @@
|
|
|
4710
5267
|
manualOverride: state?.requireReviewing === false
|
|
4711
5268
|
});
|
|
4712
5269
|
if (projectId !== getCurrentProjectId()) return null;
|
|
5270
|
+
if (accountScopeId !== cachedAccountScopeId) return null;
|
|
5271
|
+
if (probe.capabilities?.reviewing?.confirmed === true) {
|
|
5272
|
+
projectSettingsCoordinator.observeReviewingAvailability('available', 'reviewing_confirmed');
|
|
5273
|
+
}
|
|
4713
5274
|
probeStatusSnapshot = { projectId, probe, userInitiated };
|
|
4714
5275
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4715
5276
|
if (status) {
|
|
4716
|
-
status
|
|
4717
|
-
? tr('refreshProbeDone', { status: formatProbeStatusBar(probe) })
|
|
4718
|
-
: formatProbeStatusBar(probe);
|
|
4719
|
-
status.dataset.ok = isProbeReadyForCurrentMode(probe) ? 'true' : 'false';
|
|
5277
|
+
renderProbeStatusLine(status, probe, { refreshed: userInitiated });
|
|
4720
5278
|
status.dataset.refreshing = 'false';
|
|
4721
5279
|
}
|
|
4722
5280
|
if (!options.quiet && !userInitiated) {
|
|
@@ -4733,8 +5291,7 @@
|
|
|
4733
5291
|
probeStatusSnapshot = { projectId, failed: true };
|
|
4734
5292
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4735
5293
|
if (status) {
|
|
4736
|
-
status
|
|
4737
|
-
status.dataset.ok = 'false';
|
|
5294
|
+
renderProbeStatusMessage(status, tr('refreshProbeFailed'), 'warn');
|
|
4738
5295
|
status.dataset.refreshing = 'false';
|
|
4739
5296
|
}
|
|
4740
5297
|
console.warn('[codex-overleaf] refresh probe failed', error);
|
|
@@ -4754,23 +5311,81 @@
|
|
|
4754
5311
|
button.dataset.loading = loading ? 'true' : 'false';
|
|
4755
5312
|
button.disabled = Boolean(loading);
|
|
4756
5313
|
button.setAttribute('aria-busy', loading ? 'true' : 'false');
|
|
5314
|
+
setElementTitleAndAria('[data-refresh]', tr(loading ? 'refreshProbeLoading' : 'refreshProbe'),
|
|
5315
|
+
tr(loading ? 'refreshProbeLoading' : 'refreshProbeTitle'));
|
|
4757
5316
|
}
|
|
4758
5317
|
|
|
4759
5318
|
function syncProbeStatus(mode = state?.mode) {
|
|
4760
5319
|
const status = panel?.querySelector('[data-probe-status]');
|
|
4761
5320
|
if (!status) return;
|
|
4762
5321
|
if (status.dataset.refreshing === 'true') {
|
|
4763
|
-
status
|
|
5322
|
+
renderProbeStatusMessage(status, tr('refreshProbeLoading'));
|
|
4764
5323
|
return;
|
|
4765
5324
|
}
|
|
4766
5325
|
if (probeStatusSnapshot?.projectId !== getCurrentProjectId()) return;
|
|
4767
5326
|
const { failed, probe, userInitiated } = probeStatusSnapshot;
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
status.dataset.ok = !failed && isProbeReadyForCurrentMode(probe, mode) ? 'true' : 'false';
|
|
5327
|
+
if (failed) renderProbeStatusMessage(status, tr('refreshProbeFailed'), 'warn');
|
|
5328
|
+
else renderProbeStatusLine(status, probe, { refreshed: userInitiated });
|
|
4771
5329
|
if (!failed) updateExistingProbeNotice(probe, mode);
|
|
4772
5330
|
}
|
|
4773
5331
|
|
|
5332
|
+
// Keep the read scope visible; a known Track limitation adds its own warning segment.
|
|
5333
|
+
function getProbeScope(probe) {
|
|
5334
|
+
const selection = selectionContextView.capture();
|
|
5335
|
+
const focusFiles = getActiveFocusFiles();
|
|
5336
|
+
// Match submission priority: an edit selection, then explicit focus files,
|
|
5337
|
+
// then reference context, ahead of the editor's implicit current-file hint.
|
|
5338
|
+
if (selection?.mode === 'edit') return { icon: 'file', key: 'probeScopeSelection' };
|
|
5339
|
+
if (focusFiles.some(file => /\.tex$/i.test(file))) return { icon: 'file', key: 'probeScopeFiles' };
|
|
5340
|
+
if (focusFiles.length || selection) return { icon: 'attach', key: 'probeScopeContext' };
|
|
5341
|
+
if (getProbeRunReadiness(probe).editorOk) return { icon: 'file', key: 'probeScopeCurrentFile' };
|
|
5342
|
+
return { icon: 'folder', key: 'probeScopeProject' };
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
function isReviewingKnownUnavailable() {
|
|
5346
|
+
return Modules.Reviewing.knownUnavailable(projectSettingsCoordinator.getReviewingAvailability());
|
|
5347
|
+
}
|
|
5348
|
+
|
|
5349
|
+
function syncReviewingTooltip() {
|
|
5350
|
+
const toggle = panel?.querySelector('[data-require-reviewing]');
|
|
5351
|
+
const title = tr(isReviewingKnownUnavailable() ? 'reviewingUnavailableTooltip' : 'requireReviewingTitle');
|
|
5352
|
+
if (toggle) toggle.title = title;
|
|
5353
|
+
const label = toggle?.closest('.codex-review-toggle');
|
|
5354
|
+
if (label) label.title = title;
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
function renderProbeStatusLine(status, probe, options = {}) {
|
|
5358
|
+
const scope = getProbeScope(probe);
|
|
5359
|
+
const parts = [];
|
|
5360
|
+
if (options.refreshed) parts.push(tr('refreshProbeDone', { status: '' }).trim());
|
|
5361
|
+
const label = document.createElement('span');
|
|
5362
|
+
label.className = 'codex-probe-scope';
|
|
5363
|
+
label.innerHTML = Modules.PanelIcons?.svg?.(scope.icon) || ''; // Static, repository-owned icon path.
|
|
5364
|
+
label.append(tr(scope.key));
|
|
5365
|
+
parts.push(label);
|
|
5366
|
+
if (isExperimentalOtEnabled()) {
|
|
5367
|
+
const separator = document.createElement('span');
|
|
5368
|
+
separator.className = 'codex-probe-sep';
|
|
5369
|
+
separator.textContent = '·';
|
|
5370
|
+
parts.push(separator, 'OT ' + formatOtStatusLabel(getCurrentOtStatus()));
|
|
5371
|
+
}
|
|
5372
|
+
if (state.requireReviewing && isReviewingKnownUnavailable()) {
|
|
5373
|
+
const warning = document.createElement('span');
|
|
5374
|
+
warning.dataset.tone = 'warn';
|
|
5375
|
+
warning.textContent = ' · ' + tr('reviewingUnavailableStatus');
|
|
5376
|
+
parts.push(warning);
|
|
5377
|
+
}
|
|
5378
|
+
syncReviewingTooltip();
|
|
5379
|
+
status.replaceChildren(...parts);
|
|
5380
|
+
delete status.dataset.tone;
|
|
5381
|
+
}
|
|
5382
|
+
|
|
5383
|
+
function renderProbeStatusMessage(status, text, tone = '') {
|
|
5384
|
+
status.replaceChildren(text);
|
|
5385
|
+
if (tone) status.dataset.tone = tone;
|
|
5386
|
+
else delete status.dataset.tone;
|
|
5387
|
+
}
|
|
5388
|
+
|
|
4774
5389
|
function formatProbeStatusBar(probe, mode = state?.mode) {
|
|
4775
5390
|
const readiness = getProbeRunReadiness(probe);
|
|
4776
5391
|
const reviewingOk = readiness.reviewingOk;
|
|
@@ -4804,17 +5419,7 @@
|
|
|
4804
5419
|
if (!status || status.dataset.refreshing === 'true') {
|
|
4805
5420
|
return;
|
|
4806
5421
|
}
|
|
4807
|
-
|
|
4808
|
-
if (!text) {
|
|
4809
|
-
return;
|
|
4810
|
-
}
|
|
4811
|
-
const base = text.split(' · OT ')[0];
|
|
4812
|
-
status.textContent = appendOtStatusToProbeStatus(base);
|
|
4813
|
-
}
|
|
4814
|
-
|
|
4815
|
-
function isProbeReadyForCurrentMode(probe, mode = state?.mode) {
|
|
4816
|
-
const readiness = getProbeRunReadiness(probe);
|
|
4817
|
-
return mode === 'ask' || readiness.reviewingOk;
|
|
5422
|
+
syncProbeStatus();
|
|
4818
5423
|
}
|
|
4819
5424
|
|
|
4820
5425
|
function getProbeRunReadiness(probe) {
|
|
@@ -4919,8 +5524,8 @@
|
|
|
4919
5524
|
ready: true,
|
|
4920
5525
|
message: manualOverride
|
|
4921
5526
|
? tx(
|
|
4922
|
-
`Ready:
|
|
4923
|
-
|
|
5527
|
+
`Ready: Codex writes without tracked changes and ${readiness.contextLabel}.`,
|
|
5528
|
+
`可以运行:Codex 写入时不使用留痕,并且${readiness.contextLabel}。`
|
|
4924
5529
|
)
|
|
4925
5530
|
: tx(
|
|
4926
5531
|
`Ready: Codex verified Overleaf Reviewing/Track Changes is on and ${readiness.contextLabel}.`,
|
|
@@ -4929,21 +5534,13 @@
|
|
|
4929
5534
|
};
|
|
4930
5535
|
}
|
|
4931
5536
|
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
ready: false,
|
|
4935
|
-
message: tx(
|
|
4936
|
-
`Not safe to write yet: Codex ${readiness.contextLabel}, but Overleaf Reviewing/Track Changes was not verified. Turn on Reviewing first, or disable Track below.`,
|
|
4937
|
-
`还不能安全写入:Codex ${readiness.contextLabel},但没有确认 Overleaf 已开启 Reviewing/Track Changes。请先打开 Reviewing,或关闭下方的安全检查。`
|
|
4938
|
-
)
|
|
4939
|
-
};
|
|
4940
|
-
}
|
|
4941
|
-
|
|
5537
|
+
// ensureReviewingBeforeWrite switches Overleaf to Reviewing before the first
|
|
5538
|
+
// write and blocks the write only if that cannot be confirmed.
|
|
4942
5539
|
return {
|
|
4943
|
-
ready:
|
|
5540
|
+
ready: true,
|
|
4944
5541
|
message: tx(
|
|
4945
|
-
|
|
4946
|
-
|
|
5542
|
+
`Ready: Codex ${readiness.contextLabel} and will switch Overleaf to Reviewing/Track Changes before writing.`,
|
|
5543
|
+
`可以运行:Codex ${readiness.contextLabel},写入前会把 Overleaf 切到 Reviewing/Track Changes。`
|
|
4947
5544
|
)
|
|
4948
5545
|
};
|
|
4949
5546
|
}
|
|
@@ -4962,7 +5559,7 @@
|
|
|
4962
5559
|
focusFiles: getActiveFocusFiles()
|
|
4963
5560
|
});
|
|
4964
5561
|
const sanitizedProject = sanitizeRunProjectSnapshot(project);
|
|
4965
|
-
if (sanitizedProject?.capabilities?.fullProjectSnapshot) {
|
|
5562
|
+
if (sanitizedProject?.capabilities?.fullProjectSnapshot || sanitizedProject?.capabilities?.pathIdentityComplete === false) {
|
|
4966
5563
|
return sanitizedProject;
|
|
4967
5564
|
}
|
|
4968
5565
|
const pageStateProject = await callPageBridge('getProjectSnapshot', {
|
|
@@ -4983,25 +5580,37 @@
|
|
|
4983
5580
|
}
|
|
4984
5581
|
|
|
4985
5582
|
function sanitizeRunProjectSnapshot(project) {
|
|
4986
|
-
if (!project || typeof project !== 'object')
|
|
4987
|
-
|
|
4988
|
-
}
|
|
4989
|
-
const activePath = normalizeSnapshotPath(project.activePath || '');
|
|
5583
|
+
if (!project || typeof project !== 'object') return project;
|
|
5584
|
+
const normalize = Modules.ProjectFiles.normalizeSafeProjectPath;
|
|
4990
5585
|
const files = Array.isArray(project.files) ? project.files : [];
|
|
4991
|
-
const
|
|
4992
|
-
const
|
|
4993
|
-
|
|
4994
|
-
|
|
5586
|
+
const counts = new Map();
|
|
5587
|
+
for (const file of files) {
|
|
5588
|
+
const path = normalize(file?.path);
|
|
5589
|
+
if (path) counts.set(path, (counts.get(path) || 0) + 1);
|
|
4995
5590
|
}
|
|
5591
|
+
const skipped = [];
|
|
5592
|
+
const safeFiles = files.filter(file => {
|
|
5593
|
+
const path = normalize(file?.path);
|
|
5594
|
+
if (path && counts.get(path) === 1) return true;
|
|
5595
|
+
skipped.push({ path: typeof file?.path === 'string' ? file.path : '',
|
|
5596
|
+
reason: path ? 'snapshot_path_duplicate' : 'snapshot_path_invalid' });
|
|
5597
|
+
return false;
|
|
5598
|
+
});
|
|
5599
|
+
const activePath = normalize(project.activePath || '');
|
|
5600
|
+
const activePathKnown = activePath && safeFiles.some(file => file.path === activePath);
|
|
5601
|
+
const rejectActive = Boolean(project.activePath && !activePathKnown);
|
|
5602
|
+
if (!skipped.length && !rejectActive) return project;
|
|
4996
5603
|
return {
|
|
4997
5604
|
...project,
|
|
4998
|
-
activePath: '',
|
|
4999
|
-
files:
|
|
5605
|
+
activePath: rejectActive ? '' : project.activePath,
|
|
5606
|
+
files: safeFiles.map(file => rejectActive && file?.active ? { ...file, active: false } : file),
|
|
5000
5607
|
capabilities: {
|
|
5001
5608
|
...(project.capabilities || {}),
|
|
5609
|
+
...(skipped.length ? { fullProjectSnapshot: false, contentComplete: false, pathIdentityComplete: false,
|
|
5610
|
+
skipped: [...(project.capabilities?.skipped || []), ...skipped] } : {}),
|
|
5002
5611
|
diagnostics: {
|
|
5003
5612
|
...(project.capabilities?.diagnostics || {}),
|
|
5004
|
-
rejectedActivePath: project.activePath
|
|
5613
|
+
...(rejectActive ? { rejectedActivePath: project.activePath } : {})
|
|
5005
5614
|
}
|
|
5006
5615
|
}
|
|
5007
5616
|
};
|
|
@@ -5087,11 +5696,8 @@
|
|
|
5087
5696
|
}
|
|
5088
5697
|
|
|
5089
5698
|
function normalizeSnapshotPath(path) {
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
.replace(/\\/g, '/')
|
|
5093
|
-
.trim()
|
|
5094
|
-
.replace(/^\/+/, '');
|
|
5699
|
+
// Snapshot/overlay/focus arrays already carry canonical names from file chips.
|
|
5700
|
+
return Modules.ProjectFiles.normalizeSafeProjectPath(path);
|
|
5095
5701
|
}
|
|
5096
5702
|
|
|
5097
5703
|
function mergeProjectWithSyncChangeBaseFiles(project = {}, syncChanges = []) {
|
|
@@ -5303,8 +5909,10 @@
|
|
|
5303
5909
|
legacyKey,
|
|
5304
5910
|
accountScopeId
|
|
5305
5911
|
);
|
|
5912
|
+
const composerAttachments = await loadComposerAttachmentsForProject(accountScopeId, projectId);
|
|
5306
5913
|
|
|
5307
5914
|
return {
|
|
5915
|
+
composerAttachments,
|
|
5308
5916
|
model: prefs.model || '', providerId: prefs.providerId || 'builtin',
|
|
5309
5917
|
reasoningEffort: prefs.reasoningEffort || '',
|
|
5310
5918
|
speedTier: prefs.speedTier || '',
|
|
@@ -5316,8 +5924,10 @@
|
|
|
5316
5924
|
loadCodexLocalSkills: prefs.loadCodexLocalSkills !== false,
|
|
5317
5925
|
loadCodexOverleafSkills: prefs.loadCodexOverleafSkills !== false,
|
|
5318
5926
|
codexOverleafSkillEnabled: prefs.codexOverleafSkillEnabled || {},
|
|
5927
|
+
codexLocalSkillEnabled: prefs.codexLocalSkillEnabled || {},
|
|
5319
5928
|
experimentalOtByProject: prefs.experimentalOtByProject || {},
|
|
5320
5929
|
customInstructionsByProject: prefs.customInstructionsByProject || {},
|
|
5930
|
+
reviewingAvailabilityByProject: prefs.reviewingAvailabilityByProject || {},
|
|
5321
5931
|
governanceRulesByProject: normalizeGovernanceRulesByProject(prefs.governanceRulesByProject),
|
|
5322
5932
|
panelWidth: prefs.panelWidth || PANEL_DEFAULT_WIDTH,
|
|
5323
5933
|
sessions: sessions.map(session => ({
|
|
@@ -5328,6 +5938,9 @@
|
|
|
5328
5938
|
focusFiles: session.focusFiles || [], selectionContext: session.selectionContext?.projectId === projectId ? session.selectionContext : null,
|
|
5329
5939
|
projectReferenceFiles: session.projectReferenceFiles || [],
|
|
5330
5940
|
codexThreadId: session.codexThreadId || '',
|
|
5941
|
+
contextUsage: session.contextUsage || null,
|
|
5942
|
+
forkedFromSessionId: session.forkedFromSessionId || '',
|
|
5943
|
+
forkedFromRunId: session.forkedFromRunId || '',
|
|
5331
5944
|
createdAt: session.createdAt,
|
|
5332
5945
|
updatedAt: session.updatedAt,
|
|
5333
5946
|
runs: Array.isArray(session.runs) ? session.runs : [],
|
|
@@ -5368,8 +5981,10 @@
|
|
|
5368
5981
|
loadCodexLocalSkills: fallback.loadCodexLocalSkills !== false,
|
|
5369
5982
|
loadCodexOverleafSkills: fallback.loadCodexOverleafSkills !== false,
|
|
5370
5983
|
codexOverleafSkillEnabled: fallback.codexOverleafSkillEnabled || {},
|
|
5984
|
+
codexLocalSkillEnabled: fallback.codexLocalSkillEnabled || {},
|
|
5371
5985
|
experimentalOtByProject: fallback.experimentalOtByProject || {},
|
|
5372
5986
|
customInstructionsByProject: fallback.customInstructionsByProject || {},
|
|
5987
|
+
reviewingAvailabilityByProject: fallback.reviewingAvailabilityByProject || {},
|
|
5373
5988
|
governanceRulesByProject: normalizeGovernanceRulesByProject(fallback.governanceRulesByProject),
|
|
5374
5989
|
panelWidth: fallback.panelWidth || PANEL_DEFAULT_WIDTH,
|
|
5375
5990
|
sessions: [],
|
|
@@ -5401,30 +6016,15 @@
|
|
|
5401
6016
|
try {
|
|
5402
6017
|
const StorageDb = Modules.StorageDb;
|
|
5403
6018
|
const Migration = Modules.StorageMigration;
|
|
5404
|
-
//
|
|
5405
|
-
//
|
|
5406
|
-
//
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
// projects entry and surface bogus disabled rows. Two safety lanes:
|
|
5410
|
-
// 1. `options.projectIdOverride` lets a caller that legitimately
|
|
5411
|
-
// needs to persist to a non-active project supply the projectId
|
|
5412
|
-
// explicitly (the only such caller today is the post-navigation
|
|
5413
|
-
// settlement path in `persistPostNavigationRunStatus`, which goes
|
|
5414
|
-
// directly through StorageDb — but the override is in place so
|
|
5415
|
-
// future callers do not have to monkey-patch around saveState).
|
|
5416
|
-
// 2. When no override is set AND a navigation-divergent run is in
|
|
5417
|
-
// flight (currentRunView.runProjectId !== URL-derived id), log a
|
|
5418
|
-
// warning and skip persistence. The settlement path owns that
|
|
5419
|
-
// run's persistence; routing through saveState would write to the
|
|
5420
|
-
// WRONG projectId record.
|
|
5421
|
-
const projectIdOverride = options && typeof options.projectIdOverride === 'string'
|
|
5422
|
-
? options.projectIdOverride
|
|
5423
|
-
: '';
|
|
6019
|
+
// Settlement owns a run after navigation away from its project. Ordinary
|
|
6020
|
+
// view saves must not write that run into the current route's project.
|
|
6021
|
+
// An explicit override is reserved for callers that own a detached scope.
|
|
6022
|
+
const projectIdOverride = options?.draftScope?.projectId || (options && typeof options.projectIdOverride === 'string'
|
|
6023
|
+
? options.projectIdOverride : '');
|
|
5424
6024
|
const urlProjectId = getCurrentProjectId();
|
|
5425
6025
|
const projectId = projectIdOverride || urlProjectId;
|
|
5426
6026
|
if (!projectId) {
|
|
5427
|
-
if (options?.reviewRunIds?.length) throw new Error('
|
|
6027
|
+
if (options?.requireDurable || options?.reviewRunIds?.length) throw new Error('Persistence lost its project.'); return;
|
|
5428
6028
|
}
|
|
5429
6029
|
// Read currentRunView through a defensive accessor so historical test
|
|
5430
6030
|
// harnesses that inline `saveState` without re-declaring the module-
|
|
@@ -5436,28 +6036,41 @@
|
|
|
5436
6036
|
'Skipped saveState: run is in flight on a different project than the current URL; settlement owns persistence.',
|
|
5437
6037
|
'已跳过 saveState:本次运行所属项目与当前 URL 不一致,写回由 settlement 负责。'
|
|
5438
6038
|
));
|
|
5439
|
-
if (options?.reviewRunIds?.length) throw new Error('
|
|
6039
|
+
if (options?.requireDurable || options?.reviewRunIds?.length) throw new Error('Persistence project changed.'); return;
|
|
5440
6040
|
}
|
|
6041
|
+
const savedDraft = !projectIdOverride && pendingDraftSave?.scope === getComposerDraftScope()
|
|
6042
|
+
? pendingDraftSave : null;
|
|
5441
6043
|
const compactState = prepareStateForStorage(state, { onAggressive: notifyAggressiveCompactionOnce });
|
|
5442
6044
|
compactState.autoRecompile = state.autoRecompile;
|
|
5443
6045
|
compactState.loadCodexLocalSkills = state.loadCodexLocalSkills !== false;
|
|
5444
6046
|
compactState.loadCodexOverleafSkills = state.loadCodexOverleafSkills !== false;
|
|
5445
6047
|
compactState.codexOverleafSkillEnabled = getCodexOverleafSkillEnabled();
|
|
6048
|
+
compactState.codexLocalSkillEnabled = getCodexLocalSkillEnabled();
|
|
5446
6049
|
compactState.experimentalOtByProject = state.experimentalOtByProject;
|
|
5447
6050
|
compactState.customInstructionsByProject = state.customInstructionsByProject;
|
|
6051
|
+
compactState.reviewingAvailabilityByProject = state.reviewingAvailabilityByProject;
|
|
5448
6052
|
compactState.governanceRulesByProject = state.governanceRulesByProject;
|
|
5449
6053
|
const stateSnapshot = typeof structuredClone === 'function'
|
|
5450
6054
|
? structuredClone(state)
|
|
5451
6055
|
: JSON.parse(JSON.stringify(state));
|
|
6056
|
+
// Keep consumption provenance on the live run owner across ordinary
|
|
6057
|
+
// session normalization; it is not a composer edit or a queued input.
|
|
6058
|
+
const consumedSession = liveRunView?.draftConsumption && liveRunView.runProjectId === projectId
|
|
6059
|
+
&& liveRunView.runAccountScopeId === cachedAccountScopeId
|
|
6060
|
+
? stateSnapshot.sessions?.find(session => session.id === liveRunView.sessionId) : null;
|
|
6061
|
+
if (consumedSession?.task === '') consumedSession.draftConsumption = { ...liveRunView.draftConsumption };
|
|
5452
6062
|
const compactStateSnapshot = typeof structuredClone === 'function'
|
|
5453
6063
|
? structuredClone(compactState)
|
|
5454
6064
|
: JSON.parse(JSON.stringify(compactState));
|
|
5455
6065
|
|
|
5456
|
-
const
|
|
6066
|
+
const retentionScope = { accountScopeId: typeof cachedAccountScopeId === 'string' ? cachedAccountScopeId : '', projectId, generation: typeof spaRouteGeneration === 'number' ? spaRouteGeneration : null };
|
|
6067
|
+
let written;
|
|
6068
|
+
const persistScopedState = async persistenceContext => {
|
|
6069
|
+
written = await Modules.ScopedPersistenceCoordinator.persistPanelState({
|
|
5457
6070
|
state: stateSnapshot,
|
|
5458
6071
|
compactState: compactStateSnapshot,
|
|
5459
6072
|
projectId,
|
|
5460
|
-
deletedSessionIds: options?.deletedSessionIds, reviewRunIds: options?.reviewRunIds,
|
|
6073
|
+
deletedSessionIds: options?.deletedSessionIds, reviewRunIds: options?.reviewRunIds, recoveryRuns: options?.recoveryRuns,
|
|
5461
6074
|
Migration,
|
|
5462
6075
|
StorageDb,
|
|
5463
6076
|
SessionPersistence: Modules.SessionPersistence,
|
|
@@ -5465,7 +6078,11 @@
|
|
|
5465
6078
|
normalizeGovernanceRulesByProject,
|
|
5466
6079
|
normalizeCustomInstructionsByProject,
|
|
5467
6080
|
persistenceContext
|
|
5468
|
-
|
|
6081
|
+
});
|
|
6082
|
+
if (!options?.draftScope) {
|
|
6083
|
+
await getComposerDraftPersistence().save({ accountScopeId: persistenceContext.scope.accountScopeId, projectId });
|
|
6084
|
+
}
|
|
6085
|
+
};
|
|
5469
6086
|
|
|
5470
6087
|
const persistenceCoordinator = getScopedPersistenceCoordinator();
|
|
5471
6088
|
if (!persistenceCoordinator) {
|
|
@@ -5473,19 +6090,18 @@
|
|
|
5473
6090
|
code: 'scoped_persistence_unavailable'
|
|
5474
6091
|
});
|
|
5475
6092
|
} else {
|
|
5476
|
-
const
|
|
5477
|
-
projectId,
|
|
5478
|
-
{
|
|
6093
|
+
const commitOptions = {
|
|
5479
6094
|
detached: Boolean(projectIdOverride),
|
|
5480
6095
|
rebase: true,
|
|
5481
6096
|
queueMutation: options?.queueMutation,
|
|
5482
6097
|
queueMutations: options?.queueMutations,
|
|
5483
6098
|
sessionTombstones: options?.deletedSessionIds
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
6099
|
+
};
|
|
6100
|
+
const committed = options?.draftScope
|
|
6101
|
+
? await persistenceCoordinator.commitDetached(projectId, options.draftScope.accountScopeId, commitOptions, persistScopedState)
|
|
6102
|
+
: await persistenceCoordinator.commit(projectId, commitOptions, persistScopedState);
|
|
5487
6103
|
if (!committed.ok && (
|
|
5488
|
-
options?.reviewRunIds?.length || options?.queueMutation
|
|
6104
|
+
options?.requireDurable || options?.reviewRunIds?.length || options?.recoveryRuns?.length || options?.queueMutation
|
|
5489
6105
|
|| options?.queueMutations?.length
|
|
5490
6106
|
|| committed.reason !== 'stale_view'
|
|
5491
6107
|
)) {
|
|
@@ -5493,8 +6109,35 @@
|
|
|
5493
6109
|
code: committed.reason
|
|
5494
6110
|
});
|
|
5495
6111
|
}
|
|
6112
|
+
if (committed.ok) {
|
|
6113
|
+
if (savedDraft && pendingDraftSave === savedDraft) {
|
|
6114
|
+
const capturedTask = stateSnapshot.sessions.find(item => item.id === stateSnapshot.activeSessionId)?.task;
|
|
6115
|
+
const storedTask = StorageDb.buildSessionRecord({ task: capturedTask }).task;
|
|
6116
|
+
if (!(written || []).some(session => session.id === stateSnapshot.activeSessionId && session.task === storedTask)) {
|
|
6117
|
+
throw Object.assign(new Error(tx('The latest draft could not be saved. It is still available in this page.',
|
|
6118
|
+
'最新草稿未能保存,内容仍保留在当前页面。')), { code: 'draft_state_not_persisted' });
|
|
6119
|
+
}
|
|
6120
|
+
// A newer input revision keeps its own timer, even if this save finishes later.
|
|
6121
|
+
resetDraftSave();
|
|
6122
|
+
}
|
|
6123
|
+
const retentionUpdates = StorageDb.projectRetentionUpdates?.({ liveState: state, capturedState: stateSnapshot, written,
|
|
6124
|
+
capturedScope: retentionScope, currentScope: { accountScopeId: typeof cachedAccountScopeId === 'string' ? cachedAccountScopeId : '',
|
|
6125
|
+
projectId: getCurrentProjectId(), generation: typeof spaRouteGeneration === 'number' ? spaRouteGeneration : null } }) || [];
|
|
6126
|
+
for (const update of retentionUpdates) for (const run of [...(state.runs || []), ...(state.sessions || []).flatMap(session => session.runs || [])]) {
|
|
6127
|
+
if (run.id !== update.runId) continue;
|
|
6128
|
+
if (update.retention) run.recoveryRetention = update.retention; else delete run.recoveryRetention;
|
|
6129
|
+
}
|
|
6130
|
+
if (retentionUpdates.some(update => update.retention)) appendStorageNoticeOnce('undo-retention', tx(
|
|
6131
|
+
'Some recovery evidence was not retained durably. After reloading, check Overleaf history or Review.',
|
|
6132
|
+
'部分恢复材料未持久保留;刷新后请核对 Overleaf 历史或审阅。'));
|
|
6133
|
+
}
|
|
6134
|
+
if (options?.requireDurable || options?.recoveryRuns?.length) return true;
|
|
5496
6135
|
}
|
|
5497
6136
|
} catch (error) {
|
|
6137
|
+
if (options?.requireDurable) throw error;
|
|
6138
|
+
// A departing draft must never fall back using the destination page's state/scope.
|
|
6139
|
+
if (options?.draftScope) return;
|
|
6140
|
+
if (error?.code === 'recovery_scope_changed') return;
|
|
5498
6141
|
if (
|
|
5499
6142
|
error?.code === 'account_scope_unavailable' &&
|
|
5500
6143
|
!options?.reviewRunIds?.length && !options?.queueMutation &&
|
|
@@ -5522,7 +6165,7 @@
|
|
|
5522
6165
|
emitStorageQuotaFailure(error, fallbackError);
|
|
5523
6166
|
}
|
|
5524
6167
|
if (typeof appendStorageNoticeOnce === 'function') {
|
|
5525
|
-
appendStorageNoticeOnce('save-failed', tx(`Failed to save session state: ${error.message}`, `保存会话状态失败:${error.message}`));
|
|
6168
|
+
appendStorageNoticeOnce('save-failed', tx(`Failed to save session state: ${localizeVisibleReason(error.message)}`, `保存会话状态失败:${localizeVisibleReason(error.message)}`));
|
|
5526
6169
|
} else {
|
|
5527
6170
|
throw error;
|
|
5528
6171
|
}
|
|
@@ -5531,12 +6174,12 @@
|
|
|
5531
6174
|
}
|
|
5532
6175
|
}
|
|
5533
6176
|
|
|
5534
|
-
|
|
5535
6177
|
function prepareCompactFallbackState(inputState) {
|
|
5536
6178
|
const compact = prepareStateForStorage(inputState);
|
|
5537
6179
|
return {
|
|
5538
6180
|
...compact,
|
|
5539
6181
|
__codexOverleafCompactFallback: true,
|
|
6182
|
+
composerAttachments: [],
|
|
5540
6183
|
task: '',
|
|
5541
6184
|
session: null,
|
|
5542
6185
|
sessions: [],
|
|
@@ -5611,6 +6254,65 @@
|
|
|
5611
6254
|
}, delayMs);
|
|
5612
6255
|
}
|
|
5613
6256
|
|
|
6257
|
+
function getComposerDraftScope() {
|
|
6258
|
+
return JSON.stringify([cachedAccountScopeId, getCurrentProjectId(), spaRouteGeneration, state.activeSessionId]);
|
|
6259
|
+
}
|
|
6260
|
+
|
|
6261
|
+
function resetDraftSave() {
|
|
6262
|
+
if (draftSaveTimer) clearTimeout(draftSaveTimer);
|
|
6263
|
+
draftSaveTimer = null;
|
|
6264
|
+
pendingDraftSave = null;
|
|
6265
|
+
}
|
|
6266
|
+
|
|
6267
|
+
function captureComposerDraft() {
|
|
6268
|
+
const input = panel?.querySelector('[data-task]');
|
|
6269
|
+
if (!input || !state.activeSessionId) return;
|
|
6270
|
+
const next = updateActiveSessionDraft(state, input.value);
|
|
6271
|
+
if (next === state) return;
|
|
6272
|
+
if (next.task !== state.task) {
|
|
6273
|
+
composerDraftEditedHere = true;
|
|
6274
|
+
if (currentRunView?.sessionId === state.activeSessionId) delete currentRunView.draftConsumption;
|
|
6275
|
+
}
|
|
6276
|
+
state = next;
|
|
6277
|
+
const now = Date.now(), scope = getComposerDraftScope();
|
|
6278
|
+
pendingDraftSave = {
|
|
6279
|
+
scope,
|
|
6280
|
+
projectId: getCurrentProjectId(),
|
|
6281
|
+
accountScopeId: cachedAccountScopeId,
|
|
6282
|
+
firstAt: pendingDraftSave?.scope === scope ? pendingDraftSave.firstAt : now
|
|
6283
|
+
};
|
|
6284
|
+
if (draftSaveTimer) clearTimeout(draftSaveTimer);
|
|
6285
|
+
const delay = Math.max(0, Math.min(DRAFT_SAVE_DELAY_MS, pendingDraftSave.firstAt + DRAFT_SAVE_MAX_WAIT_MS - now));
|
|
6286
|
+
draftSaveTimer = setTimeout(() => {
|
|
6287
|
+
draftSaveTimer = null;
|
|
6288
|
+
if (pendingDraftSave?.scope !== getComposerDraftScope()) { resetDraftSave(); return; }
|
|
6289
|
+
pendingDraftSave.firstAt = Date.now();
|
|
6290
|
+
// Join the existing writer immediately; never reset its timer on every key.
|
|
6291
|
+
saveStateSoon(0);
|
|
6292
|
+
}, delay);
|
|
6293
|
+
}
|
|
6294
|
+
|
|
6295
|
+
async function flushComposerDraft({ requireDurable = false } = {}) {
|
|
6296
|
+
if (!pendingDraftSave) return;
|
|
6297
|
+
const scope = getComposerDraftScope();
|
|
6298
|
+
if (pendingDraftSave.scope !== scope) { resetDraftSave(); return; }
|
|
6299
|
+
if (draftSaveTimer) clearTimeout(draftSaveTimer);
|
|
6300
|
+
draftSaveTimer = null;
|
|
6301
|
+
pendingDraftSave.firstAt = Date.now();
|
|
6302
|
+
await flushQueuedSaveState(requireDurable ? { requireDurable: true } : null);
|
|
6303
|
+
if (requireDurable && scope !== getComposerDraftScope()) {
|
|
6304
|
+
throw new Error(tx('The active session changed while saving the draft. Try again.',
|
|
6305
|
+
'保存草稿时活动会话已改变,请重试。'));
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
|
|
6309
|
+
function flushComposerDraftOnLeave() {
|
|
6310
|
+
// Lifecycle events cannot await completion; periodic saves remain the primary protection.
|
|
6311
|
+
flushComposerDraft().catch(error => appendStorageNoticeOnce('save-failed', tx(
|
|
6312
|
+
`Failed to save session state: ${localizeVisibleReason(error.message)}`,
|
|
6313
|
+
`保存会话状态失败:${localizeVisibleReason(error.message)}`)));
|
|
6314
|
+
}
|
|
6315
|
+
|
|
5614
6316
|
function runQueuedSaveState() {
|
|
5615
6317
|
if (saveStateInFlightPromise) {
|
|
5616
6318
|
saveStateRunAfterFlight = true;
|
|
@@ -5629,7 +6331,7 @@
|
|
|
5629
6331
|
// here (e.g. fallback chrome.storage.local.set rethrew).
|
|
5630
6332
|
emitStorageQuotaFailure(error, null);
|
|
5631
6333
|
}
|
|
5632
|
-
if (persistenceOptions?.reviewRunIds?.length) throw error;
|
|
6334
|
+
if (persistenceOptions?.requireDurable || persistenceOptions?.reviewRunIds?.length) throw error;
|
|
5633
6335
|
appendPlainLog(tx(`Failed to save session state: ${formatStateSaveError(error)}`, `保存会话状态失败:${formatStateSaveError(error)}`));
|
|
5634
6336
|
})
|
|
5635
6337
|
.finally(() => {
|
|
@@ -5705,19 +6407,17 @@
|
|
|
5705
6407
|
if (isStorageQuotaError(error)) {
|
|
5706
6408
|
return tx('Local session history is too large. Delete some old tasks and retry.', '本地会话记录太大,请删除一些旧任务后重试。');
|
|
5707
6409
|
}
|
|
5708
|
-
return error?.message || String(error);
|
|
6410
|
+
return localizeVisibleReason(error?.message || String(error));
|
|
5709
6411
|
}
|
|
5710
6412
|
|
|
5711
6413
|
async function persistPanelInputs(event) {
|
|
5712
6414
|
const globalWrite = getGlobalPreferences().handleInput(event);
|
|
5713
6415
|
if (globalWrite) return globalWrite;
|
|
5714
|
-
//
|
|
5715
|
-
//
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
readPanelInputs();
|
|
5720
|
-
saveStateSoon();
|
|
6416
|
+
// Draft edits retain run/event identity. Composition updates also capture the
|
|
6417
|
+
// current text; compositionend captures its committed value if no final input fires.
|
|
6418
|
+
if (event?.target?.matches?.('[data-task]')) {
|
|
6419
|
+
captureComposerDraft();
|
|
6420
|
+
if (event.type === 'change') await flushComposerDraft();
|
|
5721
6421
|
return;
|
|
5722
6422
|
}
|
|
5723
6423
|
if (event?.target?.matches?.('[data-speed]')) {
|
|
@@ -5729,8 +6429,18 @@
|
|
|
5729
6429
|
}
|
|
5730
6430
|
renderSpeedOptions(getRenderedModelEntries());
|
|
5731
6431
|
readPanelInputs();
|
|
6432
|
+
// Only a user re-enabling Track requests another capability check. The
|
|
6433
|
+
// preceding input event and programmatic session renders must not reset it.
|
|
6434
|
+
if (event?.type === 'change' && event.isTrusted === true
|
|
6435
|
+
&& event.target?.matches?.('[data-require-reviewing]') && event.target.checked
|
|
6436
|
+
&& isReviewingKnownUnavailable()) {
|
|
6437
|
+
projectSettingsCoordinator.observeReviewingAvailability('unknown', 'user_retry');
|
|
6438
|
+
syncReviewingTooltip();
|
|
6439
|
+
syncProbeStatus();
|
|
6440
|
+
}
|
|
5732
6441
|
renderModelConfigChoices();
|
|
5733
6442
|
updateModelDisplay();
|
|
6443
|
+
renderContextUsage();
|
|
5734
6444
|
// Save feedback is per-card now (settingsPanel flashSaved); the global
|
|
5735
6445
|
// header chip and its Saving/Saved lifecycle are gone.
|
|
5736
6446
|
await saveState();
|
|
@@ -5817,21 +6527,64 @@
|
|
|
5817
6527
|
return speedSelect?.value || state?.speedTier || 'standard';
|
|
5818
6528
|
}
|
|
5819
6529
|
|
|
5820
|
-
|
|
5821
|
-
let composerAttachmentsRestoredOnce = false;
|
|
6530
|
+
let composerDraftPersistence = null;
|
|
5822
6531
|
let lastRunActivityBumpMs = 0;
|
|
5823
6532
|
|
|
6533
|
+
function getComposerDraftPersistence() {
|
|
6534
|
+
if (!composerDraftPersistence) composerDraftPersistence = Modules.ComposerAttachmentDraft.createController({
|
|
6535
|
+
getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId() }),
|
|
6536
|
+
getAttachments: () => composerAttachmentController.serializePersistableAttachments(),
|
|
6537
|
+
setAttachments: value => { state.composerAttachments = value; },
|
|
6538
|
+
clear: () => composerAttachmentController.clear({ notify: false }),
|
|
6539
|
+
restore: value => composerAttachmentController.restorePersistedAttachments(value),
|
|
6540
|
+
read: scope => Modules.StorageDb.readComposerAttachmentDraft(scope),
|
|
6541
|
+
write: (scope, value) => Modules.StorageDb.writeComposerAttachmentDraft(scope, value),
|
|
6542
|
+
scheduleSave: () => saveStateSoon()
|
|
6543
|
+
});
|
|
6544
|
+
return composerDraftPersistence;
|
|
6545
|
+
}
|
|
6546
|
+
|
|
6547
|
+
async function loadComposerAttachmentsForProject(accountScopeId, projectId) {
|
|
6548
|
+
if (!accountScopeId || !projectId) return [];
|
|
6549
|
+
try {
|
|
6550
|
+
return await getComposerDraftPersistence().load({ accountScopeId, projectId });
|
|
6551
|
+
} catch (error) {
|
|
6552
|
+
if (error?.code === 'stale_hydration' || error?.code === 'account_scope_unavailable') throw error;
|
|
6553
|
+
appendPlainLog(tx('Saved attachments could not be restored. Reattach the files before sending.',
|
|
6554
|
+
'已保存的附件无法恢复,请在发送前重新添加文件。'));
|
|
6555
|
+
return [];
|
|
6556
|
+
}
|
|
6557
|
+
}
|
|
6558
|
+
|
|
5824
6559
|
function persistComposerAttachmentsToState() {
|
|
5825
6560
|
if (typeof composerAttachmentController?.serializePersistableAttachments === 'function') {
|
|
5826
6561
|
state.composerAttachments = composerAttachmentController.serializePersistableAttachments();
|
|
5827
6562
|
}
|
|
5828
6563
|
}
|
|
5829
6564
|
|
|
5830
|
-
function clearTaskComposer({ keepAttachments = false } = {}) {
|
|
6565
|
+
function clearTaskComposer({ keepAttachments = false, submission = null } = {}) {
|
|
5831
6566
|
const taskInput = panel?.querySelector('[data-task]');
|
|
6567
|
+
if (submission) {
|
|
6568
|
+
if (submission.projectId !== getCurrentProjectId()
|
|
6569
|
+
|| submission.accountScopeId !== cachedAccountScopeId) return false;
|
|
6570
|
+
if (findSessionById(submission.sessionId)?.task !== submission.task) return false;
|
|
6571
|
+
if (state.activeSessionId !== submission.sessionId) {
|
|
6572
|
+
updateSessionById(submission.sessionId, { task: '' });
|
|
6573
|
+
recordSubmittedDraftConsumption(submission.sessionId, submission.task);
|
|
6574
|
+
saveStateSoon();
|
|
6575
|
+
return false;
|
|
6576
|
+
}
|
|
6577
|
+
if (composerAttachmentController.getAttachmentByteUsage?.().pending > 0
|
|
6578
|
+
|| selectionContextView.getRevision?.() !== submission.selectionRevision) return false;
|
|
6579
|
+
if (!runController.isSubmittedComposerUnchanged(submission, {
|
|
6580
|
+
task: taskInput?.value, attachments: getComposerAttachmentsForRun(),
|
|
6581
|
+
skillInvocation: getComposerSkillInvocationForRun(), selectionContext: selectionContextView.capture()
|
|
6582
|
+
})) return false;
|
|
6583
|
+
}
|
|
5832
6584
|
if (taskInput) {
|
|
5833
6585
|
taskInput.value = '';
|
|
5834
6586
|
}
|
|
6587
|
+
composerDraftEditedHere = false;
|
|
5835
6588
|
autosizeTaskTextarea();
|
|
5836
6589
|
if (!keepAttachments) {
|
|
5837
6590
|
composerAttachmentController.clear();
|
|
@@ -5840,10 +6593,47 @@
|
|
|
5840
6593
|
renderComposerSkillInvocation();
|
|
5841
6594
|
selectionContextView.clear();
|
|
5842
6595
|
state = updateActiveSession(state, { task: '' });
|
|
6596
|
+
if (submission) recordSubmittedDraftConsumption(submission.sessionId, submission.task);
|
|
6597
|
+
else if (currentRunView?.sessionId === state.activeSessionId) delete currentRunView.draftConsumption;
|
|
6598
|
+
saveStateSoon();
|
|
6599
|
+
return true;
|
|
6600
|
+
}
|
|
6601
|
+
|
|
6602
|
+
function recordSubmittedDraftConsumption(sessionId, task) {
|
|
6603
|
+
if (task && currentRunView?.sessionId === sessionId
|
|
6604
|
+
&& currentRunView.runProjectId === getCurrentProjectId()
|
|
6605
|
+
&& currentRunView.runAccountScopeId === cachedAccountScopeId) {
|
|
6606
|
+
currentRunView.draftConsumption = { runId: currentRunView.recordId, task };
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
|
|
6610
|
+
function restoreRejectedLocalSkillSubmission(submission, checkpoint) {
|
|
6611
|
+
const input = panel?.querySelector('[data-task]');
|
|
6612
|
+
if (!checkpoint || !input || submission.accountScopeId !== cachedAccountScopeId
|
|
6613
|
+
|| submission.projectId !== getCurrentProjectId() || submission.sessionId !== state.activeSessionId
|
|
6614
|
+
|| !findSessionById(submission.sessionId) || input.value || state.task
|
|
6615
|
+
|| composerDraftRevision !== checkpoint.draftRevision
|
|
6616
|
+
|| getComposerAttachmentsForRun().length || composerAttachmentController.getAttachmentByteUsage?.().pending
|
|
6617
|
+
|| composerAttachmentController.getRevision?.() !== checkpoint.attachmentRevision
|
|
6618
|
+
|| selectionContextView.getRevision?.() !== checkpoint.selectionRevision
|
|
6619
|
+
|| selectionContextView.capture() || getComposerSkillInvocationForRun()) return false;
|
|
6620
|
+
if (!composerAttachmentController.restoreSubmittedAttachments?.(submission.attachments, checkpoint.attachmentRevision)) return false;
|
|
6621
|
+
composerDraftRevision += 1;
|
|
6622
|
+
composerSkillInvocation = submission.skillInvocation;
|
|
6623
|
+
input.value = submission.task;
|
|
6624
|
+
state = updateActiveSession(state, { task: submission.task, selectionContext: submission.selectionContext });
|
|
6625
|
+
renderComposerSkillInvocation();
|
|
6626
|
+
selectionContextView.render();
|
|
6627
|
+
syncProbeStatus();
|
|
6628
|
+
autosizeTaskTextarea();
|
|
6629
|
+
syncComposerSendAvailability();
|
|
5843
6630
|
saveStateSoon();
|
|
6631
|
+
return true;
|
|
5844
6632
|
}
|
|
5845
6633
|
|
|
5846
6634
|
function applyStateToPanel(options = {}) {
|
|
6635
|
+
if (pendingDraftSave && pendingDraftSave.scope !== getComposerDraftScope()) resetDraftSave();
|
|
6636
|
+
contextUsageView?.close();
|
|
5847
6637
|
selectionContextView.render();
|
|
5848
6638
|
writingStyleSettings.sync();
|
|
5849
6639
|
applyPanelTheme(getThemePreference());
|
|
@@ -5866,6 +6656,7 @@
|
|
|
5866
6656
|
autosizeTaskTextarea();
|
|
5867
6657
|
syncComposerSendAvailability();
|
|
5868
6658
|
panel.querySelector('[data-require-reviewing]').checked = state.requireReviewing;
|
|
6659
|
+
syncReviewingTooltip();
|
|
5869
6660
|
const recompileCheckbox = panel?.querySelector('[data-auto-recompile]');
|
|
5870
6661
|
if (recompileCheckbox) {
|
|
5871
6662
|
recompileCheckbox.checked = state.autoRecompile !== false;
|
|
@@ -5882,23 +6673,14 @@
|
|
|
5882
6673
|
applySessionLabel();
|
|
5883
6674
|
renderSessionList();
|
|
5884
6675
|
renderRunHistory(options);
|
|
6676
|
+
renderPendingInputs();
|
|
5885
6677
|
getTrackedChangeCaptureController().schedule();
|
|
5886
6678
|
renderContextSelection();
|
|
5887
6679
|
renderContextSummary();
|
|
5888
|
-
|
|
5889
|
-
// restored into the (empty) tray ONCE per page load, before the render
|
|
5890
|
-
// below. Re-running the gate on later renders would resurrect files the
|
|
5891
|
-
// user explicitly removed (fleet P2).
|
|
5892
|
-
if (!composerAttachmentsRestoredOnce
|
|
5893
|
-
&& Array.isArray(state.composerAttachments) && state.composerAttachments.length
|
|
5894
|
-
&& !composerAttachmentController.getAttachmentsForRun?.().length
|
|
5895
|
-
&& typeof composerAttachmentController.restorePersistedAttachments === 'function') {
|
|
5896
|
-
composerAttachmentController.restorePersistedAttachments(state.composerAttachments);
|
|
5897
|
-
}
|
|
5898
|
-
composerAttachmentsRestoredOnce = true;
|
|
6680
|
+
getComposerDraftPersistence().hydrate(state.composerAttachments);
|
|
5899
6681
|
renderComposerAttachments();
|
|
5900
6682
|
renderComposerSkillInvocation();
|
|
5901
|
-
applyLocaleToPanel();
|
|
6683
|
+
applyLocaleToPanel({ reprojectDynamic: false });
|
|
5902
6684
|
if (!panel.querySelector('[data-context-tray]')?.hidden) {
|
|
5903
6685
|
renderContextFiles();
|
|
5904
6686
|
}
|
|
@@ -5929,6 +6711,47 @@
|
|
|
5929
6711
|
if (record) configureWritebackButton(root, record, { running });
|
|
5930
6712
|
});
|
|
5931
6713
|
renderPendingInputs();
|
|
6714
|
+
renderContextUsage();
|
|
6715
|
+
}
|
|
6716
|
+
|
|
6717
|
+
function renderContextUsage() {
|
|
6718
|
+
if (!contextUsageView || !state) return;
|
|
6719
|
+
contextUsageView.setState(getContextUsageDisplay());
|
|
6720
|
+
}
|
|
6721
|
+
|
|
6722
|
+
function getContextUsageDisplay() {
|
|
6723
|
+
const session = getActiveSession(state);
|
|
6724
|
+
const run = currentRunView?.sessionId === session?.id
|
|
6725
|
+
? findRunRecord(currentRunView.recordId, currentRunView.sessionId) : null;
|
|
6726
|
+
const selection = providerSettingsCoordinator.getRunSelection(state.providerId);
|
|
6727
|
+
const runningSelection = run?.status === 'running' && run.model === state.model
|
|
6728
|
+
&& (run.providerId || 'builtin') === (state.providerId || 'builtin');
|
|
6729
|
+
const scope = {
|
|
6730
|
+
threadId: session?.codexThreadId || '',
|
|
6731
|
+
providerId: state.providerId || 'builtin',
|
|
6732
|
+
providerRevision: runningSelection ? Number(run.providerRevision || 0)
|
|
6733
|
+
: selection?.providerRevision ?? ((state.providerId || 'builtin') === 'builtin' ? 0 : null),
|
|
6734
|
+
model: state.model || ''
|
|
6735
|
+
};
|
|
6736
|
+
// The composer can select the next turn's model while this turn runs.
|
|
6737
|
+
// Only show statistics produced by the selected model and provider.
|
|
6738
|
+
const matchesRun = run && run.model === scope.model
|
|
6739
|
+
&& (run.providerId || 'builtin') === scope.providerId
|
|
6740
|
+
&& Number(run.providerRevision || 0) === scope.providerRevision;
|
|
6741
|
+
// Starting another turn must not revive statistics from before an automatic
|
|
6742
|
+
// compaction. Retain the watermark across this thread's saved normal runs.
|
|
6743
|
+
const compactedAt = (session?.runs || []).reduce((latest, candidate) => candidate && !candidate.forkSnapshot
|
|
6744
|
+
&& candidate.operation !== 'compact' && scope.threadId && candidate.codexThreadId === scope.threadId
|
|
6745
|
+
? Math.max(latest, Modules.RunActivityModel.getCompactedAt(candidate)) : latest, 0);
|
|
6746
|
+
const { usage, pending, compacted } = Modules.ContextUsage.status(session, { ...scope, compactedAt });
|
|
6747
|
+
const compacting = Boolean(matchesRun && Modules.RunActivityModel.project(run).compacting);
|
|
6748
|
+
return { usage, pending, compacted, compacting, compactingKind: compacting ? (run.operation === 'compact' ? 'manual' : 'auto') : '' };
|
|
6749
|
+
}
|
|
6750
|
+
|
|
6751
|
+
function getCompactAvailability() {
|
|
6752
|
+
if (sessionManager.isTransitioning() || updateIdleClient?.isApplying() || currentRunView || runSubmissionPending) return { ok: false, reason: 'compactBusy' };
|
|
6753
|
+
if (!getActiveSession(state)?.codexThreadId) return { ok: false, reason: 'compactNoThread' };
|
|
6754
|
+
return { ok: true };
|
|
5932
6755
|
}
|
|
5933
6756
|
|
|
5934
6757
|
function prepareRetryReplacement(run, task) {
|
|
@@ -5959,6 +6782,10 @@
|
|
|
5959
6782
|
}
|
|
5960
6783
|
|
|
5961
6784
|
function startRunView({
|
|
6785
|
+
sessionId = state.activeSessionId,
|
|
6786
|
+
sessionContext = runController.captureSessionContext(state),
|
|
6787
|
+
codexThreadId = '',
|
|
6788
|
+
operation,
|
|
5962
6789
|
task,
|
|
5963
6790
|
mode,
|
|
5964
6791
|
model,
|
|
@@ -5975,6 +6802,7 @@
|
|
|
5975
6802
|
resetAutoFollow();
|
|
5976
6803
|
const record = {
|
|
5977
6804
|
id: createRunId(),
|
|
6805
|
+
...(operation === 'compact' ? { operation } : {}),
|
|
5978
6806
|
task,
|
|
5979
6807
|
mode,
|
|
5980
6808
|
...providerSettingsCoordinator.getProviderSnapshot(executionSnapshot?.providerId || state?.providerId),
|
|
@@ -5999,63 +6827,54 @@
|
|
|
5999
6827
|
executionSnapshot: executionSnapshot
|
|
6000
6828
|
? RunExecutionSnapshot.cloneSnapshot(executionSnapshot)
|
|
6001
6829
|
: undefined,
|
|
6002
|
-
//
|
|
6003
|
-
// Immutable per-run capture of the project this run was submitted
|
|
6004
|
-
// against. Every writeback / accept / undo dispatch attaches this id
|
|
6005
|
-
// to its page-bridge params so the page-side guard can verify the run
|
|
6006
|
-
// is still acting on the same project before mutating the editor. No
|
|
6007
|
-
// language-level enforcement — the field is treated as immutable by
|
|
6008
|
-
// convention and never reassigned anywhere downstream.
|
|
6830
|
+
// Writeback/Accept/Undo carry this immutable project identity to Page.
|
|
6009
6831
|
runProjectId: getCurrentProjectId() || '',
|
|
6010
6832
|
runAccountScopeId: cachedAccountScopeId || ''
|
|
6011
6833
|
};
|
|
6012
|
-
const active =
|
|
6013
|
-
const titlePatch = active?.titleSource !== 'manual'
|
|
6834
|
+
const active = findSessionById(sessionId);
|
|
6835
|
+
const titlePatch = operation !== 'compact' && active?.titleSource !== 'manual'
|
|
6014
6836
|
? {
|
|
6015
6837
|
title: deriveSessionTitle([], task),
|
|
6016
6838
|
titleSource: 'auto'
|
|
6017
6839
|
}
|
|
6018
6840
|
: {};
|
|
6019
|
-
|
|
6020
|
-
|
|
6841
|
+
// Establish the in-memory owner before publishing or rendering the record.
|
|
6842
|
+
// If presentation throws, runTask still has the identity needed to settle it.
|
|
6843
|
+
const view = currentRunView = {
|
|
6844
|
+
operation, sessionId, sessionContext, codexThreadId, recordId: record.id,
|
|
6845
|
+
runProjectId: record.runProjectId, runAccountScopeId: record.runAccountScopeId,
|
|
6846
|
+
executionSnapshot: record.executionSnapshot, startedAt: Date.now(), queueItemId,
|
|
6847
|
+
nativeRequestId: '', activeTurn: null
|
|
6848
|
+
};
|
|
6849
|
+
updateSessionById(sessionId, {
|
|
6850
|
+
runs: runController.replaceRunForRetry(active?.runs, replaceRunId, record),
|
|
6021
6851
|
...titlePatch
|
|
6022
6852
|
});
|
|
6023
6853
|
saveStateSoon(replaceRunId ? 0 : 120);
|
|
6024
6854
|
|
|
6025
6855
|
const log = panel.querySelector('[data-log]');
|
|
6026
|
-
removeEmptyRunsMessage(log);
|
|
6027
|
-
if (replaceRunId) log.querySelector(`[data-run-id="${cssEscape(replaceRunId)}"]`)?.remove();
|
|
6028
6856
|
const root = renderRunCard(record);
|
|
6029
|
-
|
|
6030
|
-
scrollLogToBottom({ force: true });
|
|
6031
|
-
startRunElapsedTick();
|
|
6032
|
-
renderSessionList();
|
|
6033
|
-
applySessionLabel();
|
|
6034
|
-
|
|
6035
|
-
return {
|
|
6036
|
-
sessionId: state.activeSessionId,
|
|
6037
|
-
recordId: record.id,
|
|
6038
|
-
// Mirror the immutable runProjectId on the view so writeback /
|
|
6039
|
-
// accept / undo dispatches don't need to re-look up the record.
|
|
6040
|
-
runProjectId: record.runProjectId,
|
|
6041
|
-
runAccountScopeId: record.runAccountScopeId,
|
|
6042
|
-
executionSnapshot: record.executionSnapshot,
|
|
6857
|
+
Object.assign(view, {
|
|
6043
6858
|
root,
|
|
6044
6859
|
runProcess: root.querySelector('[data-run-process]'),
|
|
6045
6860
|
processLabel: root.querySelector('[data-run-process-summary]'),
|
|
6046
6861
|
events: root.querySelector('[data-run-events]'),
|
|
6047
6862
|
report: root.querySelector('[data-run-report]'),
|
|
6048
6863
|
status: root.querySelector('[data-run-status]'),
|
|
6049
|
-
//
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6864
|
+
// Seed references before warm-mirror resolution produces its snapshot.
|
|
6865
|
+
projectFiles: getCurrentProjectReferenceFiles()
|
|
6866
|
+
});
|
|
6867
|
+
if (state.activeSessionId === sessionId) {
|
|
6868
|
+
removeEmptyRunsMessage(log);
|
|
6869
|
+
if (replaceRunId) log.querySelector(`[data-run-id="${cssEscape(replaceRunId)}"]`)?.remove();
|
|
6870
|
+
log.append(root);
|
|
6871
|
+
scrollLogToBottom({ force: true });
|
|
6872
|
+
}
|
|
6873
|
+
startRunElapsedTick();
|
|
6874
|
+
renderSessionList();
|
|
6875
|
+
applySessionLabel();
|
|
6876
|
+
|
|
6877
|
+
return view;
|
|
6059
6878
|
}
|
|
6060
6879
|
|
|
6061
6880
|
function touchSessionForTerminalRun(sessionId, finishedAt) {
|
|
@@ -6118,6 +6937,7 @@
|
|
|
6118
6937
|
].filter(Boolean).join(' · ');
|
|
6119
6938
|
collapseRunProcess(visibleView, statusText);
|
|
6120
6939
|
}
|
|
6940
|
+
renderContextUsage();
|
|
6121
6941
|
}
|
|
6122
6942
|
|
|
6123
6943
|
|
|
@@ -6132,6 +6952,11 @@
|
|
|
6132
6952
|
}
|
|
6133
6953
|
|
|
6134
6954
|
const owner = currentRunView && findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
6955
|
+
if (storeContextUsage(event, owner, findSessionById(currentRunView?.sessionId), journalSeq)) {
|
|
6956
|
+
scheduleRunStateSave('stream');
|
|
6957
|
+
renderContextUsage();
|
|
6958
|
+
return;
|
|
6959
|
+
}
|
|
6135
6960
|
if (owner && Modules.SubagentActivityModel.ingest(owner, event, {
|
|
6136
6961
|
locale: getLocale(), sanitize: sanitizeAssistantVisibleText, nativeEventSeq: journalSeq
|
|
6137
6962
|
})) {
|
|
@@ -6160,6 +6985,20 @@
|
|
|
6160
6985
|
replaceText: activity.replaceText,
|
|
6161
6986
|
nativeEventSeq: journalSeq
|
|
6162
6987
|
});
|
|
6988
|
+
if (activity.kind !== 'stream') renderContextUsage();
|
|
6989
|
+
}
|
|
6990
|
+
|
|
6991
|
+
function storeContextUsage(event, record, session, sequence) {
|
|
6992
|
+
if (!record) return false;
|
|
6993
|
+
const usage = Modules.ContextUsage.fromEvent(event, {
|
|
6994
|
+
...record, providerRevision: Number(record.providerRevision || 0), eventSeq: sequence
|
|
6995
|
+
});
|
|
6996
|
+
if (!usage || !Modules.ContextUsage.isNewer(usage, record.contextUsage)) return false;
|
|
6997
|
+
record.contextUsage = usage;
|
|
6998
|
+
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(sequence || 0));
|
|
6999
|
+
if (session?.codexThreadId === usage.threadId
|
|
7000
|
+
&& Modules.ContextUsage.isNewer(usage, session.contextUsage)) session.contextUsage = usage;
|
|
7001
|
+
return true;
|
|
6163
7002
|
}
|
|
6164
7003
|
|
|
6165
7004
|
function bindActiveTurn(detail = {}, journalSeq = 0) {
|
|
@@ -6168,16 +7007,24 @@
|
|
|
6168
7007
|
}
|
|
6169
7008
|
const threadId = String(detail.threadId || '');
|
|
6170
7009
|
const turnId = String(detail.turnId || '');
|
|
6171
|
-
currentRunView.activeTurn = { threadId, turnId };
|
|
7010
|
+
currentRunView.activeTurn = currentRunView.operation === 'compact' ? null : { threadId, turnId };
|
|
6172
7011
|
const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
6173
7012
|
if (record) {
|
|
6174
7013
|
record.codexThreadId = threadId; record.codexTurnId = turnId;
|
|
7014
|
+
if (record.operation === 'compact') {
|
|
7015
|
+
const session = findSessionById(currentRunView.sessionId);
|
|
7016
|
+
if (session?.contextUsage?.threadId === threadId
|
|
7017
|
+
&& Date.parse(session.contextUsage.observedAt) < Date.parse(record.startedAt)) {
|
|
7018
|
+
session.contextUsage = null;
|
|
7019
|
+
}
|
|
7020
|
+
}
|
|
6175
7021
|
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(journalSeq || 0));
|
|
6176
7022
|
}
|
|
6177
7023
|
if (threadId) {
|
|
6178
7024
|
updateSessionById(currentRunView.sessionId, { codexThreadId: threadId });
|
|
6179
7025
|
}
|
|
6180
7026
|
renderPendingInputs();
|
|
7027
|
+
renderContextUsage();
|
|
6181
7028
|
saveState().catch(() => {});
|
|
6182
7029
|
}
|
|
6183
7030
|
|
|
@@ -6198,7 +7045,7 @@
|
|
|
6198
7045
|
function appendRunEvent(input = {}) {
|
|
6199
7046
|
const { title, status = 'info', detail, timestamp } = input;
|
|
6200
7047
|
const safeTitle = sanitizeAssistantVisibleText(title) || 'Event';
|
|
6201
|
-
if (!currentRunView
|
|
7048
|
+
if (!currentRunView) {
|
|
6202
7049
|
appendPlainLog(safeTitle);
|
|
6203
7050
|
return;
|
|
6204
7051
|
}
|
|
@@ -6242,21 +7089,20 @@
|
|
|
6242
7089
|
})).filter(item => item.path)
|
|
6243
7090
|
: undefined
|
|
6244
7091
|
};
|
|
7092
|
+
// Keep the activity timestamp current without retaining a run reference
|
|
7093
|
+
// across session normalization, which replaces run and event objects.
|
|
7094
|
+
const nowMs = Date.now();
|
|
7095
|
+
if (nowMs - lastRunActivityBumpMs > 60000
|
|
7096
|
+
&& findRunRecord(currentRunView.recordId, currentRunView.sessionId)) {
|
|
7097
|
+
lastRunActivityBumpMs = nowMs;
|
|
7098
|
+
state = updateActiveSession(state, {});
|
|
7099
|
+
}
|
|
6245
7100
|
const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
|
|
6246
7101
|
let renderedEvent = event;
|
|
6247
7102
|
if (record) {
|
|
6248
7103
|
if (Number.isFinite(Number(input.nativeEventSeq))) {
|
|
6249
7104
|
record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(input.nativeEventSeq || 0));
|
|
6250
7105
|
}
|
|
6251
|
-
// v1.8.1: keep the session's activity timestamp honest DURING a run —
|
|
6252
|
-
// it was pinned to run start, so the dashboard's 30-minute zombie
|
|
6253
|
-
// heuristic mislabeled any long live run as interrupted (fleet P1).
|
|
6254
|
-
// Throttled to once a minute; updateActiveSession stamps updatedAt.
|
|
6255
|
-
const nowMs = Date.now();
|
|
6256
|
-
if (nowMs - lastRunActivityBumpMs > 60000) {
|
|
6257
|
-
lastRunActivityBumpMs = nowMs;
|
|
6258
|
-
state = updateActiveSession(state, {});
|
|
6259
|
-
}
|
|
6260
7106
|
renderedEvent = event.kind === 'stream'
|
|
6261
7107
|
? upsertRunStreamRecordEvent(record, event)
|
|
6262
7108
|
: event;
|
|
@@ -6592,311 +7438,34 @@
|
|
|
6592
7438
|
);
|
|
6593
7439
|
}
|
|
6594
7440
|
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
if (!
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|| (run.trackedChangeStatus === 'needs_review' && run.undoOperations?.some(operation => operation?.undoCreatedFile?.v === 1))) {
|
|
6610
|
-
await undoRunTrackedChanges(runId, run);
|
|
6611
|
-
return;
|
|
6612
|
-
}
|
|
6613
|
-
|
|
6614
|
-
const undoRestore = buildNoTraceUndoRestore(run);
|
|
6615
|
-
const undoOperations = undoRestore.operations;
|
|
6616
|
-
const unsafeFullFileUndo = findUnsafeFullFileUndoOperation(undoOperations, {
|
|
6617
|
-
allowSnapshotRestore: undoRestore.snapshotRestore
|
|
6618
|
-
});
|
|
6619
|
-
if (unsafeFullFileUndo) {
|
|
6620
|
-
appendRunRecordEvent(runId, {
|
|
6621
|
-
title: tr('undoUnsafeFullFileTitle'),
|
|
6622
|
-
status: 'failed',
|
|
6623
|
-
detail: {
|
|
6624
|
-
[tr('detailFile')]: unsafeFullFileUndo.path,
|
|
6625
|
-
[tr('detailReason')]: tr('undoUnsafeFullFileReason')
|
|
6626
|
-
}
|
|
6627
|
-
});
|
|
6628
|
-
return;
|
|
6629
|
-
}
|
|
6630
|
-
|
|
6631
|
-
// v1.8.0 C3: multi-file undos confirm through a per-file selection —
|
|
6632
|
-
// undoing a subset leaves undoStatus 'partial' and the button usable;
|
|
6633
|
-
// re-undoing an already-restored file is skipped by the base check.
|
|
6634
|
-
const undoPaths = Array.from(new Set(undoOperations.map(operation => operation?.path).filter(Boolean)));
|
|
6635
|
-
let selectedOperations = undoOperations;
|
|
6636
|
-
if (undoPaths.length > 1 && typeof showUndoFileSelection === 'function') {
|
|
6637
|
-
const selectedPaths = await showUndoFileSelection({
|
|
6638
|
-
title: tr('undoFileSelectTitle'),
|
|
6639
|
-
task: truncateRunTitle(run.task),
|
|
6640
|
-
confirmLabel: tr('undoFileSelectConfirm'),
|
|
6641
|
-
cancelLabel: tr('confirmDefaultCancel'),
|
|
6642
|
-
selectAllLabel: tr('undoFileSelectAll'),
|
|
6643
|
-
paths: undoPaths
|
|
6644
|
-
});
|
|
6645
|
-
if (!selectedPaths) {
|
|
6646
|
-
return;
|
|
6647
|
-
}
|
|
6648
|
-
const selectedSet = new Set(selectedPaths);
|
|
6649
|
-
selectedOperations = undoOperations.filter(operation => selectedSet.has(operation?.path));
|
|
6650
|
-
} else {
|
|
6651
|
-
const approved = await showPluginConfirm({
|
|
6652
|
-
title: tr('undoNoTraceTitle'),
|
|
6653
|
-
message: [
|
|
6654
|
-
truncateRunTitle(run.task),
|
|
6655
|
-
'',
|
|
6656
|
-
tr('undoNoTraceMessage', { files: formatOperationFiles(undoOperations) })
|
|
6657
|
-
].join('\n'),
|
|
6658
|
-
confirmLabel: tr('undoConfirm'),
|
|
6659
|
-
cancelLabel: tr('confirmDefaultCancel'),
|
|
6660
|
-
destructive: true
|
|
6661
|
-
});
|
|
6662
|
-
if (!approved) {
|
|
6663
|
-
return;
|
|
6664
|
-
}
|
|
6665
|
-
}
|
|
6666
|
-
|
|
6667
|
-
const deadlineAt = Date.now() + getPageBridgeTimeoutMs('rejectTrackedChanges');
|
|
6668
|
-
setRunUndoStatus(runId, 'running');
|
|
6669
|
-
appendRunRecordEvent(runId, {
|
|
6670
|
-
title: tr('undoNoTraceStarted'),
|
|
6671
|
-
status: 'running',
|
|
6672
|
-
detail: { [tr('detailWillUndo')]: formatOperationFiles(selectedOperations) }
|
|
7441
|
+
let runReviewController = null;
|
|
7442
|
+
function getRunReviewController() {
|
|
7443
|
+
if (!runReviewController) runReviewController = Modules.RunReviewController.create({
|
|
7444
|
+
Modules, WritebackSettlement, trackedChangeInFlight, findRunRecord,
|
|
7445
|
+
getRunUndoCount, isTrackedChangeLifecycleRun, getPendingMirrorRefresh, hasTrackedEditorUndo, buildNoTraceUndoRestore,
|
|
7446
|
+
findUnsafeFullFileUndoOperation, showUndoFileSelection, showPluginConfirm, tr,
|
|
7447
|
+
truncateRunTitle, formatOperationFiles, formatTrackedChangeFiles, formatTrackedUndoFiles,
|
|
7448
|
+
getPageBridgeTimeoutMs, callPageBridge, setRunUndoStatus, appendRunRecordEvent, getRunProjectIdForWriteback,
|
|
7449
|
+
isUndoResultEffectivelyApplied, writebackOrchestrator, appendUndoReviewingPolicyEvent, formatOperationType,
|
|
7450
|
+
formatApplyResultReason, buildTrackedUndoPostFiles, getTrackedChangeCaptureController, buildContentFailure,
|
|
7451
|
+
applyTrackedChangeSettlement, refreshRunCardControls, formatBridgeResultReason, advanceRunUndoProgress,
|
|
7452
|
+
saveStateSoon, flushQueuedSaveState,
|
|
7453
|
+
getState: () => state, getCurrentProjectId, getAccountScopeId: () => cachedAccountScopeId,
|
|
7454
|
+
getRouteGeneration: () => spaRouteGeneration, tx, notify: message => showPluginToast(message, { status: 'warning', sticky: true })
|
|
6673
7455
|
});
|
|
7456
|
+
return runReviewController;
|
|
7457
|
+
}
|
|
6674
7458
|
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
baseFiles: run.undoBaseFiles || [],
|
|
6678
|
-
reviewingPolicy: 'no-trace-undo',
|
|
6679
|
-
// Undo remains bound to its original project, including after navigation.
|
|
6680
|
-
runProjectId: getRunProjectIdForWriteback(run)
|
|
6681
|
-
});
|
|
6682
|
-
const undoApplied = isUndoResultEffectivelyApplied(run, result);
|
|
6683
|
-
writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
|
|
6684
|
-
appendUndoReviewingPolicyEvent(runId, result.reviewingPolicy);
|
|
6685
|
-
appendRunRecordEvent(runId, {
|
|
6686
|
-
title: tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
|
|
6687
|
-
status: undoApplied ? 'completed' : result.skipped?.length ? 'failed' : 'completed',
|
|
6688
|
-
detail: {
|
|
6689
|
-
[tr('detailUndone')]: (result.applied || []).map(item => ({
|
|
6690
|
-
[tr('detailAction')]: formatOperationType(item.operation?.type),
|
|
6691
|
-
[tr('detailFile')]: item.operation?.path
|
|
6692
|
-
})),
|
|
6693
|
-
[tr('detailSkipped')]: (result.skipped || []).map(item => ({
|
|
6694
|
-
[tr('detailAction')]: formatOperationType(item.operation?.type),
|
|
6695
|
-
[tr('detailFile')]: item.operation?.path,
|
|
6696
|
-
[tr('detailReason')]: formatApplyResultReason(item)
|
|
6697
|
-
}))
|
|
6698
|
-
}
|
|
6699
|
-
});
|
|
6700
|
-
// A subset undo never closes the ledger: unselected files can still
|
|
6701
|
-
// be undone later, so the run stays 'partial' and the button usable.
|
|
6702
|
-
const fullSelection = selectedOperations.length === undoOperations.length;
|
|
6703
|
-
applyLegacyUndoSettlement(
|
|
6704
|
-
runId,
|
|
6705
|
-
undoApplied && fullSelection ? 'applied' : 'partial',
|
|
6706
|
-
result
|
|
6707
|
-
);
|
|
7459
|
+
async function undoRun(runId) {
|
|
7460
|
+
return getRunReviewController().undoRun(runId);
|
|
6708
7461
|
}
|
|
6709
7462
|
|
|
6710
7463
|
async function undoRunTrackedChanges(runId, run) {
|
|
6711
|
-
|
|
6712
|
-
const trackedUndo = Array.isArray(run.undoTrackedChanges) && run.undoTrackedChanges.length > 0;
|
|
6713
|
-
const hasTextUndo = trackedUndo || buildTrackedUndoPostFiles(run).length > 0;
|
|
6714
|
-
const approved = await showPluginConfirm({
|
|
6715
|
-
title: !hasTextUndo ? tr('undoNoTraceTitle') : trackedUndo ? tr('undoTrackedTitle') : tr('undoNativeTitle'),
|
|
6716
|
-
message: [
|
|
6717
|
-
truncateRunTitle(run.task),
|
|
6718
|
-
'',
|
|
6719
|
-
...(hasTextUndo ? [trackedUndo
|
|
6720
|
-
? tr('undoTrackedMessage', { files: formatTrackedChangeFiles(run.undoTrackedChanges) })
|
|
6721
|
-
: tr('undoNativeMessage', { files: formatTrackedUndoFiles(run) })] : []),
|
|
6722
|
-
...(createdUndoOperations.length ? [tr('undoNoTraceMessage', { files: formatOperationFiles(createdUndoOperations) })] : [])
|
|
6723
|
-
].join('\n'),
|
|
6724
|
-
confirmLabel: trackedUndo ? tr('undoTrackedConfirm') : tr('undoConfirm'),
|
|
6725
|
-
cancelLabel: tr('confirmDefaultCancel'),
|
|
6726
|
-
destructive: true
|
|
6727
|
-
});
|
|
6728
|
-
if (!approved) {
|
|
6729
|
-
return;
|
|
6730
|
-
}
|
|
6731
|
-
|
|
6732
|
-
const deadlineAt = Date.now() + getPageBridgeTimeoutMs('rejectTrackedChanges');
|
|
6733
|
-
// Keep the review lifecycle until every pending part has settled.
|
|
6734
|
-
// Its local lock stays active while verified recovery progress is persisted.
|
|
6735
|
-
const lifecycleReject = (trackedUndo || createdUndoOperations.length > 0) && isTrackedChangeLifecycleRun(run);
|
|
6736
|
-
if (lifecycleReject) {
|
|
6737
|
-
trackedChangeInFlight.set(runId, 'reject');
|
|
6738
|
-
refreshRunCardControls(runId);
|
|
6739
|
-
} else {
|
|
6740
|
-
setRunUndoStatus(runId, 'running');
|
|
6741
|
-
}
|
|
6742
|
-
appendRunRecordEvent(runId, {
|
|
6743
|
-
title: !hasTextUndo ? tr('undoNoTraceStarted') : trackedUndo ? tr('undoTrackedStarted') : tr('undoNativeStarted'),
|
|
6744
|
-
status: 'running',
|
|
6745
|
-
detail: { [tr('detailWillUndo')]: !hasTextUndo ? formatOperationFiles(createdUndoOperations)
|
|
6746
|
-
: trackedUndo ? formatTrackedChangeFiles(run.undoTrackedChanges) : formatTrackedUndoFiles(run) }
|
|
6747
|
-
});
|
|
6748
|
-
|
|
6749
|
-
try {
|
|
6750
|
-
const trackedUndoPostFilesAtDispatch = buildTrackedUndoPostFiles(run);
|
|
6751
|
-
let result = hasTextUndo ? await callPageBridge('rejectTrackedChanges', {
|
|
6752
|
-
trackedChanges: run.undoTrackedChanges || [], deadlineAt,
|
|
6753
|
-
expectedFiles: run.undoExpectedFiles || [],
|
|
6754
|
-
postFiles: trackedUndoPostFilesAtDispatch,
|
|
6755
|
-
untrackedUndo: run.executionSnapshot
|
|
6756
|
-
? run.executionSnapshot.requireReviewing === false && ['submitted', 'legacy-captured'].includes(run.executionSnapshot.source)
|
|
6757
|
-
: getTrackedChangeCaptureController().hasLegacyUntrackedCheckpoint(run),
|
|
6758
|
-
// The page-side project guard still refuses writes after navigation.
|
|
6759
|
-
runProjectId: getRunProjectIdForWriteback(run)
|
|
6760
|
-
}) : { ok: true, applied: [], skipped: [] };
|
|
6761
|
-
if (createdUndoOperations.length && lifecycleReject && hasTextUndo) {
|
|
6762
|
-
WritebackSettlement.attachUndoNotVerifiedFailure(run, result, { buildFailure: buildContentFailure, postFiles: trackedUndoPostFilesAtDispatch });
|
|
6763
|
-
await applyTrackedChangeSettlement(runId, 'reject', { ...result, ok: false });
|
|
6764
|
-
}
|
|
6765
|
-
if (createdUndoOperations.length) {
|
|
6766
|
-
let createdResult;
|
|
6767
|
-
try {
|
|
6768
|
-
if (result.ok === false || result.skipped?.length) {
|
|
6769
|
-
throw new Error('Text changes remain unresolved; created files were kept for the next undo attempt.');
|
|
6770
|
-
}
|
|
6771
|
-
createdResult = await callPageBridge('applyOperations', {
|
|
6772
|
-
operations: createdUndoOperations, deadlineAt,
|
|
6773
|
-
baseFiles: run.undoBaseFiles || [],
|
|
6774
|
-
reviewingPolicy: 'no-trace-undo',
|
|
6775
|
-
runProjectId: getRunProjectIdForWriteback(run)
|
|
6776
|
-
});
|
|
6777
|
-
} catch (error) {
|
|
6778
|
-
createdResult = { ok: false, applied: [], skipped: createdUndoOperations.map(operation => ({
|
|
6779
|
-
operation, result: { ok: false, code: 'undo_operation_failed', reason: error.message }
|
|
6780
|
-
})) };
|
|
6781
|
-
}
|
|
6782
|
-
const applied = [...(result.applied || []), ...(createdResult.applied || [])];
|
|
6783
|
-
const skipped = [...(result.skipped || []), ...(createdResult.skipped || [])];
|
|
6784
|
-
for (const operation of createdUndoOperations) {
|
|
6785
|
-
if (!(createdResult.applied || []).some(entry => entry.operation?.path === operation.path
|
|
6786
|
-
&& entry.result?.ok === true && entry.result?.verified === true)
|
|
6787
|
-
&& !(createdResult.skipped || []).some(entry => entry.operation?.path === operation.path)) {
|
|
6788
|
-
skipped.push({ operation, result: { ok: false, code: 'undo_operation_failed',
|
|
6789
|
-
reason: 'The created file has no verified deletion receipt.' } });
|
|
6790
|
-
}
|
|
6791
|
-
}
|
|
6792
|
-
result = { ...result, ok: result.ok !== false && createdResult.ok !== false && !skipped.length, applied, skipped };
|
|
6793
|
-
}
|
|
6794
|
-
writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
|
|
6795
|
-
appendRunRecordEvent(runId, {
|
|
6796
|
-
title: createdUndoOperations.length ? tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }) : trackedUndo
|
|
6797
|
-
? tr('undoTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 })
|
|
6798
|
-
: tr('undoNativeResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
|
|
6799
|
-
status: result.skipped?.length ? 'failed' : 'completed',
|
|
6800
|
-
detail: {
|
|
6801
|
-
[trackedUndo && !createdUndoOperations.length ? tr('detailRejected') : tr('detailUndone')]: (result.applied || []).map(item => ({
|
|
6802
|
-
[tr('detailFile')]: item.trackedChange?.path || item.operation?.path || tr('unknownFile'),
|
|
6803
|
-
[tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key || item.operation?.type
|
|
6804
|
-
})),
|
|
6805
|
-
[tr('detailSkipped')]: (result.skipped || []).map(item => ({
|
|
6806
|
-
[tr('detailFile')]: item.trackedChange?.path || item.operation?.path || tr('unknownFile'),
|
|
6807
|
-
[tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key || item.operation?.type || '',
|
|
6808
|
-
[tr('detailReason')]: formatBridgeResultReason(item.result, item.trackedChange?.path || item.operation?.path)
|
|
6809
|
-
}))
|
|
6810
|
-
}
|
|
6811
|
-
});
|
|
6812
|
-
if (lifecycleReject) {
|
|
6813
|
-
WritebackSettlement.attachUndoNotVerifiedFailure(run, result, {
|
|
6814
|
-
buildFailure: buildContentFailure,
|
|
6815
|
-
postFiles: trackedUndoPostFilesAtDispatch
|
|
6816
|
-
});
|
|
6817
|
-
await applyTrackedChangeSettlement(runId, 'reject', result);
|
|
6818
|
-
return;
|
|
6819
|
-
}
|
|
6820
|
-
applyLegacyUndoSettlement(runId, result.skipped?.length ? 'partial' : 'applied', result);
|
|
6821
|
-
} finally {
|
|
6822
|
-
if (lifecycleReject) {
|
|
6823
|
-
trackedChangeInFlight.delete(runId);
|
|
6824
|
-
refreshRunCardControls(runId);
|
|
6825
|
-
}
|
|
6826
|
-
}
|
|
7464
|
+
return getRunReviewController().undoRunTrackedChanges(runId, run);
|
|
6827
7465
|
}
|
|
6828
7466
|
|
|
6829
7467
|
async function acceptRun(runId) {
|
|
6830
|
-
|
|
6831
|
-
if (!run || !isTrackedChangeLifecycleRun(run)) {
|
|
6832
|
-
return;
|
|
6833
|
-
}
|
|
6834
|
-
const status = run.trackedChangeStatus || '';
|
|
6835
|
-
// pending and needs_review are both actionable per §7: needs_review means
|
|
6836
|
-
// the prior attempt could not prove a clean post-action state, and the
|
|
6837
|
-
// user is supposed to be able to retry after inspecting Overleaf.
|
|
6838
|
-
if (status !== 'pending' && status !== 'needs_review') {
|
|
6839
|
-
return;
|
|
6840
|
-
}
|
|
6841
|
-
if (!Array.isArray(run.undoTrackedChanges) || !run.undoTrackedChanges.length) {
|
|
6842
|
-
return;
|
|
6843
|
-
}
|
|
6844
|
-
if (trackedChangeInFlight.get(runId)) {
|
|
6845
|
-
return;
|
|
6846
|
-
}
|
|
6847
|
-
|
|
6848
|
-
trackedChangeInFlight.set(runId, 'accept');
|
|
6849
|
-
refreshRunCardControls(runId);
|
|
6850
|
-
appendRunRecordEvent(runId, {
|
|
6851
|
-
title: tr('runAcceptTrackedStarted'),
|
|
6852
|
-
status: 'running',
|
|
6853
|
-
detail: { [tr('detailAccepted')]: formatTrackedChangeFiles(run.undoTrackedChanges) }
|
|
6854
|
-
});
|
|
6855
|
-
|
|
6856
|
-
try {
|
|
6857
|
-
const result = await callPageBridge('acceptTrackedChanges', {
|
|
6858
|
-
trackedChanges: run.undoTrackedChanges || [],
|
|
6859
|
-
expectedFiles: run.undoExpectedFiles || [],
|
|
6860
|
-
postFiles: buildTrackedUndoPostFiles(run),
|
|
6861
|
-
appliedOperations: Array.isArray(run.appliedOperations) ? run.appliedOperations : [],
|
|
6862
|
-
runProjectId: getRunProjectIdForWriteback(run)
|
|
6863
|
-
});
|
|
6864
|
-
appendAcceptDiagnosticEvents(runId, Array.isArray(result.diagnostics) ? result.diagnostics : []);
|
|
6865
|
-
appendRunRecordEvent(runId, {
|
|
6866
|
-
title: tr('runAcceptTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
|
|
6867
|
-
status: result.skipped?.length ? 'failed' : 'completed',
|
|
6868
|
-
detail: {
|
|
6869
|
-
[tr('detailAccepted')]: (result.applied || []).map(item => ({
|
|
6870
|
-
[tr('detailFile')]: item.trackedChange?.path || tr('unknownFile'),
|
|
6871
|
-
[tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key
|
|
6872
|
-
})),
|
|
6873
|
-
[tr('detailSkipped')]: (result.skipped || []).map(item => ({
|
|
6874
|
-
[tr('detailFile')]: item.trackedChange?.path || tr('unknownFile'),
|
|
6875
|
-
[tr('detailRecord')]: item.trackedChange?.label || item.trackedChange?.id || item.trackedChange?.key || '',
|
|
6876
|
-
[tr('detailReason')]: formatBridgeResultReason(item.result, item.trackedChange?.path)
|
|
6877
|
-
}))
|
|
6878
|
-
}
|
|
6879
|
-
});
|
|
6880
|
-
if (result.ok === false) {
|
|
6881
|
-
appendRunRecordEvent(runId, {
|
|
6882
|
-
title: tr('runAcceptTrackedFailed'),
|
|
6883
|
-
status: 'failed',
|
|
6884
|
-
detail: {
|
|
6885
|
-
[tr('detailReason')]: tr('runAcceptTrackedFailedReason')
|
|
6886
|
-
}
|
|
6887
|
-
});
|
|
6888
|
-
if (!WritebackSettlement.isSuccessfulTrackedChangeSettlement(result)) {
|
|
6889
|
-
refreshRunCardControls(runId);
|
|
6890
|
-
}
|
|
6891
|
-
}
|
|
6892
|
-
WritebackSettlement.attachAcceptNotVerifiedFailure(run, result, {
|
|
6893
|
-
buildFailure: buildContentFailure
|
|
6894
|
-
});
|
|
6895
|
-
await applyTrackedChangeSettlement(runId, 'accept', result);
|
|
6896
|
-
} finally {
|
|
6897
|
-
trackedChangeInFlight.delete(runId);
|
|
6898
|
-
refreshRunCardControls(runId);
|
|
6899
|
-
}
|
|
7468
|
+
return getRunReviewController().acceptRun(runId);
|
|
6900
7469
|
}
|
|
6901
7470
|
|
|
6902
7471
|
function getRunProjectIdForWriteback(run) {
|
|
@@ -6905,32 +7474,6 @@
|
|
|
6905
7474
|
: '';
|
|
6906
7475
|
}
|
|
6907
7476
|
|
|
6908
|
-
// Translates the page bridge's Accept All `diagnostics` array into one
|
|
6909
|
-
// run-card event row per step. Each entry is `{ step, info }`; the step
|
|
6910
|
-
// names map 1:1 to the runAcceptTrackedStep* i18n keys. We render `info`
|
|
6911
|
-
// as the event detail so the user can copy-paste the full trace without
|
|
6912
|
-
// any extra page-state query.
|
|
6913
|
-
function appendAcceptDiagnosticEvents(runId, diagnostics) {
|
|
6914
|
-
for (const entry of diagnostics || []) {
|
|
6915
|
-
const step = entry && entry.step;
|
|
6916
|
-
const info = (entry && entry.info) || {};
|
|
6917
|
-
const titleKey = WritebackSettlement.ACCEPT_DIAGNOSTIC_TITLE_KEYS[step];
|
|
6918
|
-
if (!titleKey) {
|
|
6919
|
-
continue;
|
|
6920
|
-
}
|
|
6921
|
-
const title = titleKey === 'runAcceptTrackedStepReplayStart' || titleKey === 'runAcceptTrackedStepReplayDone'
|
|
6922
|
-
? tr(titleKey, { path: info.path || tr('unknownFile') })
|
|
6923
|
-
: tr(titleKey);
|
|
6924
|
-
const status = WritebackSettlement.acceptDiagnosticStatus(step, info);
|
|
6925
|
-
appendRunRecordEvent(runId, {
|
|
6926
|
-
title,
|
|
6927
|
-
kind: 'activity',
|
|
6928
|
-
status,
|
|
6929
|
-
detail: info
|
|
6930
|
-
});
|
|
6931
|
-
}
|
|
6932
|
-
}
|
|
6933
|
-
|
|
6934
7477
|
function buildContentFailure(code, operation, overrides) {
|
|
6935
7478
|
return WritebackSettlement.buildContentFailure(
|
|
6936
7479
|
code,
|
|
@@ -6940,27 +7483,29 @@
|
|
|
6940
7483
|
);
|
|
6941
7484
|
}
|
|
6942
7485
|
|
|
6943
|
-
async function applyTrackedChangeSettlement(runId, kind, result) {
|
|
7486
|
+
async function applyTrackedChangeSettlement(runId, kind, result, options = {}) {
|
|
6944
7487
|
const record = findRunRecord(runId);
|
|
6945
|
-
const
|
|
6946
|
-
const run = kind === 'reject' && createdUndo ? advanceRunUndoProgress(record, result) : record;
|
|
7488
|
+
const run = kind === 'reject' ? advanceRunUndoProgress(record, result) : record;
|
|
6947
7489
|
const settlement = WritebackSettlement.settleTrackedChangeLifecycle({
|
|
6948
7490
|
kind,
|
|
6949
7491
|
run,
|
|
6950
7492
|
result,
|
|
6951
7493
|
failureReasons: FailureReasons
|
|
6952
7494
|
});
|
|
6953
|
-
if (settlement.decision === 'blocked'
|
|
7495
|
+
if (settlement.decision === 'blocked'
|
|
7496
|
+
&& !(kind === 'reject' && (run !== record || record?.undoOperations?.some(Modules.UndoOperations.isFileUndoOperation)))) {
|
|
6954
7497
|
return;
|
|
6955
7498
|
}
|
|
6956
7499
|
await Modules.ScopedPersistenceCoordinator.applyReviewTransition({
|
|
6957
7500
|
getState: () => state, runId, transition: WritebackSettlement.applySettlementTransition(run, settlement),
|
|
6958
|
-
persist: flushQueuedSaveState
|
|
7501
|
+
persist: flushQueuedSaveState, assertCurrent: options.assertCurrent, SessionState: Modules.SessionState
|
|
6959
7502
|
});
|
|
6960
7503
|
}
|
|
6961
7504
|
|
|
6962
7505
|
|
|
6963
7506
|
function advanceRunUndoProgress(run, result) {
|
|
7507
|
+
getDirectoryCleanupRecovery().capture({ recordId: run.id, sessionId: state.activeSessionId,
|
|
7508
|
+
runProjectId: run.runProjectId, runAccountScopeId: run.runAccountScopeId }, result);
|
|
6964
7509
|
const postFiles = buildTrackedUndoPostFiles(run);
|
|
6965
7510
|
return Modules.UndoOperations.advanceUndoCheckpoint(run, result, file =>
|
|
6966
7511
|
WritebackSettlement.isUndoVerifiedContentMatching({ undoExpectedFiles: [file] }, result, { postFiles })
|
|
@@ -6970,11 +7515,12 @@
|
|
|
6970
7515
|
if (!run) {
|
|
6971
7516
|
return 0;
|
|
6972
7517
|
}
|
|
7518
|
+
run = Modules.UndoOperations.withAvailableUndo(run);
|
|
6973
7519
|
const trackedEditorUndoCount = hasTrackedEditorUndo(run) ? 1 : 0;
|
|
6974
7520
|
if (trackedEditorUndoCount) {
|
|
6975
|
-
return trackedEditorUndoCount +
|
|
7521
|
+
return trackedEditorUndoCount + Modules.UndoOperations.getFileUndoOperations(run).length;
|
|
6976
7522
|
}
|
|
6977
|
-
return (run.undoOperations
|
|
7523
|
+
return Modules.UndoOperations.prepareRunUndo(run).undoOperations.length
|
|
6978
7524
|
+ (run.undoTrackedChanges?.length || 0);
|
|
6979
7525
|
}
|
|
6980
7526
|
|
|
@@ -6986,29 +7532,55 @@
|
|
|
6986
7532
|
sessionId: target.sessionId, runId: target.recordId, createdAt: new Date().toISOString() };
|
|
6987
7533
|
}
|
|
6988
7534
|
|
|
7535
|
+
let directoryCleanupRecovery = null;
|
|
7536
|
+
function getDirectoryCleanupRecovery() {
|
|
7537
|
+
return directoryCleanupRecovery || (directoryCleanupRecovery = Modules.DirectoryCleanupRecovery.create({
|
|
7538
|
+
getProjectId: getCurrentProjectId, getAccountScopeId: () => cachedAccountScopeId,
|
|
7539
|
+
getRouteGeneration: () => spaRouteGeneration, findRunRecord, refresh: refreshRunCardControls,
|
|
7540
|
+
onOmitted: (runId, code) => appendRunRecordEvent(runId, { kind: 'technical', status: 'warning',
|
|
7541
|
+
title: tx('Some folder observations exceeded the storage limit; existing observations were kept.',
|
|
7542
|
+
'部分文件夹观测记录超过存储上限,已有记录已保留。'), detail: { code } })
|
|
7543
|
+
}));
|
|
7544
|
+
}
|
|
6989
7545
|
async function stageWritebackRecovery(operations, target, requireReviewing) {
|
|
6990
|
-
const
|
|
6991
|
-
|
|
7546
|
+
const owner = recoveryOwner(target), generation = spaRouteGeneration, view = currentRunView;
|
|
7547
|
+
const assertCurrent = () => {
|
|
7548
|
+
const record = findRunRecord(target.recordId, target.sessionId);
|
|
7549
|
+
if (owner.projectId !== getCurrentProjectId() || owner.accountScopeId !== cachedAccountScopeId
|
|
7550
|
+
|| generation !== spaRouteGeneration || view !== currentRunView || !view
|
|
7551
|
+
|| view.recordId !== owner.runId || view.sessionId !== owner.sessionId
|
|
7552
|
+
|| (target.runAccountScopeId && target.runAccountScopeId !== owner.accountScopeId)
|
|
7553
|
+
|| !record || record.forkSnapshot || record.runProjectId !== owner.projectId) {
|
|
7554
|
+
throw new Error('The project, account or writeback run changed before dispatch.');
|
|
7555
|
+
}
|
|
7556
|
+
return record;
|
|
7557
|
+
};
|
|
7558
|
+
const record = assertCurrent();
|
|
6992
7559
|
const creates = operations.filter(op => op.type === 'create' && typeof op.content === 'string');
|
|
6993
|
-
record.retryRequireReviewing = requireReviewing === true;
|
|
7560
|
+
record.retryRequireReviewing = requireReviewing === true;
|
|
6994
7561
|
if (!creates.length) return;
|
|
6995
|
-
const candidate = Modules.WritebackIntent.normalize({ ...
|
|
7562
|
+
const candidate = Modules.WritebackIntent.normalize({ ...owner, id: crypto.randomUUID(),
|
|
6996
7563
|
operations: creates, baseFiles: [], requireReviewing });
|
|
6997
7564
|
const safe = Modules.SessionState.pickWritebackRecovery({ ...record, retryWriteback: candidate }).retryWriteback;
|
|
6998
7565
|
if (safe) record.retryWriteback = safe; else delete record.retryWriteback;
|
|
6999
7566
|
await flushQueuedSaveState();
|
|
7567
|
+
assertCurrent();
|
|
7000
7568
|
}
|
|
7001
7569
|
|
|
7002
|
-
async function settleWritebackRecovery(applied, project, verification, target) {
|
|
7570
|
+
async function settleWritebackRecovery(applied, project, verification, target, requireReviewing) {
|
|
7003
7571
|
const record = findRunRecord(target.recordId, target.sessionId);
|
|
7004
7572
|
if (!record) return;
|
|
7573
|
+
// retryRequireReviewing is in-memory only; a renormalized run copy loses it.
|
|
7574
|
+
const retryReviewing = typeof requireReviewing === 'boolean' ? requireReviewing
|
|
7575
|
+
: typeof record.retryRequireReviewing === 'boolean' ? record.retryRequireReviewing
|
|
7576
|
+
: record.executionSnapshot?.requireReviewing === true;
|
|
7005
7577
|
const owner = recoveryOwner(target);
|
|
7006
7578
|
const skippedPaths = new Set((applied.skipped || []).map(entry => entry.operation?.path));
|
|
7007
7579
|
const creates = (record.retryWriteback?.operations || []).filter(op => skippedPaths.has(op.path));
|
|
7008
7580
|
// Untouched edits (never opened / rejected) and deletes whose server check failed are safe to replay.
|
|
7009
7581
|
const edits = (applied.skipped || []).filter(({ operation: op, result }) => result?.failure?.changedDocument === false && ((op?.type === 'edit'
|
|
7010
7582
|
&& (result.failure.stage === 'navigation' || result.failure.code === 'write_operation_failed')) || (op?.type === 'delete' && result.failure.code === 'source_zip_unavailable'))).map(entry => entry.operation);
|
|
7011
|
-
const base = { ...(record.retryWriteback || { ...owner, id: crypto.randomUUID(), requireReviewing:
|
|
7583
|
+
const base = { ...(record.retryWriteback || { ...owner, id: crypto.randomUUID() }), requireReviewing: retryReviewing,
|
|
7012
7584
|
baseFiles: project?.files || [], requestId: applied.receiptUnconfirmed ? applied.requestId || '' : '' };
|
|
7013
7585
|
// An oversized edit pre-image must not cost the create retries (the intent has a 300 KiB ceiling).
|
|
7014
7586
|
const safeRetry = [[...creates, ...edits], creates].filter(ops => ops.length).map(ops => Modules.SessionState
|
|
@@ -7040,32 +7612,43 @@
|
|
|
7040
7612
|
|| owner.accountScopeId !== cachedAccountScopeId || owner.sessionId !== session.id
|
|
7041
7613
|
|| owner.runId !== record.id || record.undoStatus === 'applied' || record.trackedChangeStatus === 'rejected') return;
|
|
7042
7614
|
const originalStatus = record.status;
|
|
7615
|
+
const retryAutoRecompile = typeof record.executionSnapshot?.autoRecompile === 'boolean'
|
|
7616
|
+
? record.executionSnapshot.autoRecompile : undefined;
|
|
7043
7617
|
// State is renormalized during the awaits below (probes, session sync), which
|
|
7044
7618
|
// replaces run objects; always write through the current copy.
|
|
7045
7619
|
const live = () => findRunRecord(runId, session.id) || record;
|
|
7046
7620
|
let finished = false;
|
|
7047
7621
|
runCancellationRequested = false;
|
|
7048
7622
|
runCancellationController = new AbortController();
|
|
7049
|
-
|
|
7050
|
-
record.
|
|
7051
|
-
|
|
7052
|
-
const
|
|
7053
|
-
currentRunView = { root, recordId: runId, sessionId: session.id, runProjectId: owner.projectId,
|
|
7623
|
+
const generation = spaRouteGeneration;
|
|
7624
|
+
const requestId = record.nativeRequestId || `writeback:${owner.sessionId}:${runId}`;
|
|
7625
|
+
const ownerToken = crypto.randomUUID();
|
|
7626
|
+
const retryView = { recordId: runId, sessionId: session.id, runProjectId: owner.projectId,
|
|
7054
7627
|
runAccountScopeId: owner.accountScopeId, executionSnapshot: record.executionSnapshot,
|
|
7055
|
-
startedAt: Date.now(), writebackRetry: true
|
|
7056
|
-
|
|
7057
|
-
runProcess: root?.querySelector('[data-run-process]'), processLabel: root?.querySelector('[data-run-process-summary]') };
|
|
7628
|
+
startedAt: Date.now(), writebackRetry: true };
|
|
7629
|
+
currentRunView = retryView;
|
|
7058
7630
|
setRunning(true);
|
|
7059
7631
|
const assertOwner = () => {
|
|
7060
|
-
if (runCancellationRequested || currentRunView
|
|
7632
|
+
if (runCancellationRequested || currentRunView !== retryView) {
|
|
7061
7633
|
throw Object.assign(new Error('Save checking was cancelled.'), { code: 'codex_cancelled' });
|
|
7062
7634
|
}
|
|
7063
|
-
if (owner.projectId !== getCurrentProjectId() || owner.accountScopeId !== cachedAccountScopeId) {
|
|
7635
|
+
if (generation !== spaRouteGeneration || owner.projectId !== getCurrentProjectId() || owner.accountScopeId !== cachedAccountScopeId) {
|
|
7064
7636
|
throw Object.assign(new Error('The project or account changed.'), { code: 'aborted_project_changed' });
|
|
7065
7637
|
}
|
|
7066
7638
|
};
|
|
7067
7639
|
try {
|
|
7640
|
+
const bound = await activeTurnControl.bindBrowserRun({ requestId, projectKey: owner.projectId,
|
|
7641
|
+
clientRunId: runId, sessionId: session.id, ownerToken });
|
|
7068
7642
|
assertOwner();
|
|
7643
|
+
if (!bound) throw new Error(tx('Could not start sync. Reload the page and try again.', '无法开始同步,请刷新页面后重试。'));
|
|
7644
|
+
// Publish running only after another page can observe this browser owner.
|
|
7645
|
+
live().status = 'running';
|
|
7646
|
+
live().retryingWriteback = true;
|
|
7647
|
+
renderRunHistory({ preserveScroll: true });
|
|
7648
|
+
const root = panel.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
|
|
7649
|
+
Object.assign(retryView, { root, events: root?.querySelector('[data-run-events]'),
|
|
7650
|
+
report: root?.querySelector('[data-run-report]'), status: root?.querySelector('[data-run-status]'),
|
|
7651
|
+
runProcess: root?.querySelector('[data-run-process]'), processLabel: root?.querySelector('[data-run-process-summary]') });
|
|
7069
7652
|
if (intent?.requestId) {
|
|
7070
7653
|
const receipt = await callPageBridge('getWritebackReceipt', { requestId: intent.requestId, runProjectId: owner.projectId });
|
|
7071
7654
|
if (receipt?.state === 'running') throw new Error(tx('The previous upload is still finishing. Try again shortly.', '上一轮上传仍在收尾,请稍后再试。'));
|
|
@@ -7074,8 +7657,11 @@
|
|
|
7074
7657
|
// Edits replay against the run's original base files, so the stale guard still refuses changed documents.
|
|
7075
7658
|
const result = await applySyncChangesToOverleaf(intent.operations.map(op => op.type === 'edit'
|
|
7076
7659
|
? { type: 'write', path: op.path, patches: op.patches, content: op.replaceAll } : op.type === 'delete' ? { type: 'delete', path: op.path } : { ...op, type: 'create' }),
|
|
7077
|
-
|
|
7078
|
-
|
|
7660
|
+
// The submitted snapshot owns the write mode; older intents may carry a lost value.
|
|
7661
|
+
{ id: owner.projectId, files: intent.baseFiles }, { mode: 'auto', requireReviewing: typeof record.executionSnapshot?.requireReviewing === 'boolean'
|
|
7662
|
+
? record.executionSnapshot.requireReviewing : intent.requireReviewing,
|
|
7663
|
+
autoRecompile: retryAutoRecompile,
|
|
7664
|
+
retryCreates: true, assistantMessageKey: 'retry_sync', assistantMessage: tx('Retried the remaining file transfers for this task.', '已重试本轮未完成的文件同步。') });
|
|
7079
7665
|
assertOwner();
|
|
7080
7666
|
if (result.settlement?.facts) live().settlement = WritebackSettlement.mergeRetrySettlement(live().settlement, result.settlement.facts); // Keep files outside the retry.
|
|
7081
7667
|
await finishRunView(result.hasSkippedOperations ? tx('Sync incomplete', '同步未完成') : tx('Synced', '已同步'),
|
|
@@ -7100,11 +7686,19 @@
|
|
|
7100
7686
|
} finally {
|
|
7101
7687
|
// Only finishRunView settles a new terminal status; every other exit
|
|
7102
7688
|
// returns the card to the status it had before the retry.
|
|
7689
|
+
if (currentRunView !== retryView) {
|
|
7690
|
+
activeTurnControl.release(requestId, ownerToken);
|
|
7691
|
+
return;
|
|
7692
|
+
}
|
|
7103
7693
|
for (const copy of new Set([record, live()])) {
|
|
7104
7694
|
delete copy.retryingWriteback;
|
|
7105
7695
|
if (!finished && copy.status === 'running') copy.status = originalStatus;
|
|
7106
7696
|
}
|
|
7107
|
-
|
|
7697
|
+
saveStateSoon();
|
|
7698
|
+
await flushQueuedSaveState().catch(() => {});
|
|
7699
|
+
activeTurnControl.release(requestId, ownerToken);
|
|
7700
|
+
if (currentRunView !== retryView) return;
|
|
7701
|
+
currentRunView = null;
|
|
7108
7702
|
runCancellationRequested = false;
|
|
7109
7703
|
runCancellationController = null;
|
|
7110
7704
|
setRunning(false);
|
|
@@ -7195,7 +7789,9 @@
|
|
|
7195
7789
|
});
|
|
7196
7790
|
}
|
|
7197
7791
|
|
|
7198
|
-
function recordUndoFromApply(project, applyResult) {
|
|
7792
|
+
async function recordUndoFromApply(project, applyResult) {
|
|
7793
|
+
const directoryRecovery = Modules.DirectoryCleanupRecovery ? getDirectoryCleanupRecovery() : null;
|
|
7794
|
+
directoryRecovery?.capture(currentRunView, applyResult);
|
|
7199
7795
|
const appliedEntries = getAppliedEntries(applyResult).filter(entry => entry.result?.idempotent !== true);
|
|
7200
7796
|
if (!currentRunView?.recordId || !appliedEntries.length) {
|
|
7201
7797
|
return;
|
|
@@ -7210,11 +7806,18 @@
|
|
|
7210
7806
|
return;
|
|
7211
7807
|
}
|
|
7212
7808
|
|
|
7213
|
-
|
|
7809
|
+
let combinedAppliedOperations = [
|
|
7214
7810
|
...(Array.isArray(record.appliedOperations) ? record.appliedOperations : []),
|
|
7215
7811
|
...appliedOperations
|
|
7216
7812
|
];
|
|
7813
|
+
const checkpoint = buildUndoCheckpoint(project, combinedAppliedOperations);
|
|
7814
|
+
combinedAppliedOperations = checkpoint.appliedOperations;
|
|
7217
7815
|
record.appliedOperations = combinedAppliedOperations;
|
|
7816
|
+
if (checkpoint.undoUnavailable.length) {
|
|
7817
|
+
appendRunEvent({ title: tr('failureReason_snapshot_undo_unavailable_user'), status: 'warning',
|
|
7818
|
+
detail: checkpoint.undoUnavailable.map(item => ({ [tr('detailFile')]: item.path,
|
|
7819
|
+
[tr('detailReason')]: item.code, [tr('detailNext')]: tr('failureReason_snapshot_undo_unavailable_next') })) });
|
|
7820
|
+
}
|
|
7218
7821
|
const createdCheckpoint = buildCreatedFileUndoCheckpoint(project, combinedAppliedOperations, record);
|
|
7219
7822
|
|
|
7220
7823
|
if ((currentRunView?.executionSnapshot?.requireReviewing ?? state.requireReviewing) === true) {
|
|
@@ -7231,6 +7834,7 @@
|
|
|
7231
7834
|
combinedTrackedChanges,
|
|
7232
7835
|
record.undoExpectedFiles
|
|
7233
7836
|
);
|
|
7837
|
+
record.undoOperations = Modules.UndoOperations.getFileUndoOperations(record);
|
|
7234
7838
|
record.undoStatus = '';
|
|
7235
7839
|
record.partialWriteback = skippedEntries.length > 0;
|
|
7236
7840
|
// Recording tracked-change refs enters the run into the tracked-change
|
|
@@ -7276,7 +7880,6 @@
|
|
|
7276
7880
|
return;
|
|
7277
7881
|
}
|
|
7278
7882
|
|
|
7279
|
-
const checkpoint = buildUndoCheckpoint(project, combinedAppliedOperations);
|
|
7280
7883
|
const createdPaths = new Set(createdCheckpoint.undoOperations.map(operation => operation.path));
|
|
7281
7884
|
checkpoint.undoOperations = [
|
|
7282
7885
|
...checkpoint.undoOperations.filter(operation => !createdPaths.has(operation.path)),
|
|
@@ -7286,10 +7889,7 @@
|
|
|
7286
7889
|
...checkpoint.undoBaseFiles.filter(file => !createdPaths.has(file.path)),
|
|
7287
7890
|
...createdCheckpoint.undoBaseFiles
|
|
7288
7891
|
];
|
|
7289
|
-
if (!checkpoint.undoOperations.length)
|
|
7290
|
-
return;
|
|
7291
|
-
}
|
|
7292
|
-
|
|
7892
|
+
if (!checkpoint.undoOperations.length && !checkpoint.undoUnavailable.length) return;
|
|
7293
7893
|
record.undoOperations = checkpoint.undoOperations;
|
|
7294
7894
|
record.undoBaseFiles = checkpoint.undoBaseFiles;
|
|
7295
7895
|
record.undoTrackedChanges = [];
|
|
@@ -7302,6 +7902,7 @@
|
|
|
7302
7902
|
record.undoStatus = '';
|
|
7303
7903
|
record.partialWriteback = skippedEntries.length > 0;
|
|
7304
7904
|
refreshRunCardControls(record.id);
|
|
7905
|
+
if (!checkpoint.undoOperations.length) return;
|
|
7305
7906
|
appendRunEvent({
|
|
7306
7907
|
title: tr('undoCheckpointPlain', { count: record.undoOperations.length }),
|
|
7307
7908
|
status: 'completed',
|
|
@@ -7363,6 +7964,8 @@
|
|
|
7363
7964
|
activity: Modules.RunActivityModel.capture(event, sanitizeAssistantVisibleText),
|
|
7364
7965
|
guidanceId: sanitizeAssistantVisibleText(event.guidanceId)
|
|
7365
7966
|
};
|
|
7967
|
+
if (event.failure) normalized.failure = sanitizeAssistantVisibleValue(event.failure);
|
|
7968
|
+
if (event.detailStructured) normalized.detailStructured = sanitizeAssistantVisibleValue(event.detailStructured);
|
|
7366
7969
|
record.events = [...(record.events || []), normalized].slice(-MAX_RUN_EVENTS);
|
|
7367
7970
|
saveStateSoon();
|
|
7368
7971
|
|
|
@@ -7387,21 +7990,8 @@
|
|
|
7387
7990
|
refreshRunCardControls(runId);
|
|
7388
7991
|
}
|
|
7389
7992
|
|
|
7390
|
-
function applyLegacyUndoSettlement(runId, undoStatus, result) {
|
|
7391
|
-
|
|
7392
|
-
if (!run) return;
|
|
7393
|
-
const checkpoint = run.undoOperations?.some(operation => operation?.undoCreatedFile?.v === 1)
|
|
7394
|
-
? advanceRunUndoProgress(run, result) : run;
|
|
7395
|
-
Object.assign(run, WritebackSettlement.applySettlementTransition(
|
|
7396
|
-
checkpoint,
|
|
7397
|
-
WritebackSettlement.settleLegacyUndo({
|
|
7398
|
-
run: checkpoint,
|
|
7399
|
-
status: undoStatus,
|
|
7400
|
-
result
|
|
7401
|
-
})
|
|
7402
|
-
));
|
|
7403
|
-
saveStateSoon();
|
|
7404
|
-
refreshRunCardControls(runId);
|
|
7993
|
+
async function applyLegacyUndoSettlement(runId, undoStatus, result) {
|
|
7994
|
+
return getRunReviewController().applyLegacyUndoSettlement(runId, undoStatus, result);
|
|
7405
7995
|
}
|
|
7406
7996
|
|
|
7407
7997
|
function refreshRunCardControls(runId) {
|
|
@@ -7825,8 +8415,14 @@
|
|
|
7825
8415
|
return;
|
|
7826
8416
|
}
|
|
7827
8417
|
|
|
8418
|
+
const groupKey = String(options.groupKey || '');
|
|
8419
|
+
const grouped = groupKey && Array.from(region.children).find(item => item.dataset.groupKey === groupKey);
|
|
8420
|
+
if (grouped) {
|
|
8421
|
+
updateToastGroup(grouped);
|
|
8422
|
+
return;
|
|
8423
|
+
}
|
|
7828
8424
|
const last = region.lastElementChild;
|
|
7829
|
-
if (last?.classList?.contains('codex-toast') && last.dataset.baseText === value) {
|
|
8425
|
+
if (!groupKey && !last?.dataset.groupKey && last?.classList?.contains('codex-toast') && last.dataset.baseText === value) {
|
|
7830
8426
|
const repeatCount = Number(last.dataset.repeatCount || '1') + 1;
|
|
7831
8427
|
last.dataset.repeatCount = String(repeatCount);
|
|
7832
8428
|
const textNode = last.querySelector('[data-toast-text]');
|
|
@@ -7844,15 +8440,34 @@
|
|
|
7844
8440
|
|
|
7845
8441
|
const body = document.createElement('div');
|
|
7846
8442
|
body.className = 'codex-toast-body';
|
|
7847
|
-
|
|
7848
|
-
|
|
8443
|
+
if (options.title) {
|
|
8444
|
+
const title = document.createElement('div');
|
|
8445
|
+
title.className = 'codex-toast-title';
|
|
8446
|
+
title.textContent = options.title;
|
|
8447
|
+
body.append(title);
|
|
8448
|
+
}
|
|
8449
|
+
const textNode = document.createElement('div');
|
|
8450
|
+
textNode.dataset.toastText = 'true';
|
|
8451
|
+
textNode.textContent = value;
|
|
8452
|
+
body.append(textNode);
|
|
8453
|
+
if (groupKey) {
|
|
8454
|
+
item.dataset.groupKey = groupKey;
|
|
8455
|
+
const details = document.createElement('details');
|
|
8456
|
+
details.className = 'codex-toast-group';
|
|
8457
|
+
const summary = document.createElement('summary');
|
|
8458
|
+
const list = document.createElement('ul');
|
|
8459
|
+
list.dataset.toastGroupItems = 'true';
|
|
8460
|
+
details.append(summary, list);
|
|
8461
|
+
body.append(details);
|
|
8462
|
+
}
|
|
7849
8463
|
item.append(body);
|
|
8464
|
+
if (groupKey) updateToastGroup(item);
|
|
7850
8465
|
|
|
7851
8466
|
const close = document.createElement('button');
|
|
7852
8467
|
close.type = 'button';
|
|
7853
8468
|
close.className = 'codex-toast-close';
|
|
7854
8469
|
close.setAttribute('aria-label', tr('dismissNotification'));
|
|
7855
|
-
close.
|
|
8470
|
+
close.innerHTML = Modules.PanelIcons.svg('close', 'codex-close-glyph');
|
|
7856
8471
|
close.addEventListener('click', () => item.remove());
|
|
7857
8472
|
item.append(close);
|
|
7858
8473
|
|
|
@@ -7866,6 +8481,20 @@
|
|
|
7866
8481
|
item.remove();
|
|
7867
8482
|
}, 5200);
|
|
7868
8483
|
}
|
|
8484
|
+
|
|
8485
|
+
function updateToastGroup(toast) {
|
|
8486
|
+
const list = toast.querySelector('[data-toast-group-items]');
|
|
8487
|
+
const value = String(options.groupItem || '').trim();
|
|
8488
|
+
if (value && !Array.from(list.children).some(item => item.textContent === value)) {
|
|
8489
|
+
const item = document.createElement('li');
|
|
8490
|
+
const code = document.createElement('code');
|
|
8491
|
+
code.textContent = value;
|
|
8492
|
+
item.append(code);
|
|
8493
|
+
list.append(item);
|
|
8494
|
+
}
|
|
8495
|
+
toast.querySelector('.codex-toast-group summary').textContent =
|
|
8496
|
+
options.groupSummary?.(list.children.length) || String(list.children.length);
|
|
8497
|
+
}
|
|
7869
8498
|
}
|
|
7870
8499
|
|
|
7871
8500
|
function updateProbeNotice(text) {
|