codex-overleaf-link 2.2.1 → 2.3.1
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 +25 -23
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/runtime-manifest.json +5 -71
- package/extension/src/background.js +67 -63
- package/extension/src/backgroundUpdateCoordinator.js +128 -66
- package/extension/src/content/applyResultFormatters.js +2 -3
- package/extension/src/content/composerPanel.js +0 -2
- package/extension/src/content/contentRuntime.js +757 -1558
- package/extension/src/content/contextTray.js +4 -6
- package/extension/src/content/diagnosticsController.js +1 -2
- package/extension/src/content/diffReviewPanel.js +14 -13
- package/extension/src/content/generated/content.bundle.js +41605 -0
- package/extension/src/content/generated/content.bundle.meta.json +103 -0
- package/extension/src/content/legacyGlobalRegistry.js +129 -0
- package/extension/src/content/markdownText.js +17 -17
- package/extension/src/content/mathText.js +155 -9
- package/extension/src/content/modelPicker.js +1 -1
- package/extension/src/content/modelPickerSupport.js +10 -5
- package/extension/src/content/moduleRegistryKernel.js +125 -0
- package/extension/src/content/nativeCompatibilityController.js +313 -0
- package/extension/src/content/otWarmMirror.js +8 -6
- package/extension/src/content/pageBridgeClient.js +224 -0
- package/extension/src/content/panelMaintenance.js +8 -3
- package/extension/src/content/postNavigationSettlementPersistence.js +100 -0
- package/extension/src/content/projectProviderSelection.js +4 -12
- package/extension/src/content/projectSessionCleanup.js +3 -3
- package/extension/src/content/providerSettingsCoordinator.js +59 -26
- package/extension/src/content/providerSettingsDialog.js +30 -18
- package/extension/src/content/recentProjects.js +15 -23
- package/extension/src/content/runGuidanceController.js +1 -1
- package/extension/src/content/runQueueScheduler.js +129 -29
- package/extension/src/content/runSettlementPersistence.js +51 -0
- package/extension/src/content/runTimelineView.js +17 -8
- package/extension/src/content/scopedPersistenceCoordinator.js +100 -0
- package/extension/src/content/scopedPersistenceDetachedCommit.js +47 -0
- package/extension/src/content/scopedPersistencePanelState.js +111 -0
- package/extension/src/content/sessionManager.js +15 -12
- package/extension/src/content/sessionPersistence.js +67 -8
- package/extension/src/content/updateNotice.js +10 -4
- package/extension/src/content/writebackOrchestrator.js +89 -68
- package/extension/src/contentScript.js +5 -1
- package/extension/src/page/trackedChangesLifecycle.js +16 -5
- package/extension/src/page/writebackRouter.js +6 -2
- package/extension/src/pageBridge.js +47 -98
- package/extension/src/shared/agentTranscript.js +18 -8
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/governanceRules.js +1 -1
- package/extension/src/shared/i18n.js +2 -4
- package/extension/src/shared/managedUpdateProjection.js +337 -0
- package/extension/src/shared/nativeRequestIdentity.js +60 -0
- package/extension/src/shared/otText.js +0 -19
- package/extension/src/shared/pageRpcContract.js +136 -0
- package/extension/src/shared/runExecutionSnapshot.js +187 -0
- package/extension/src/shared/runExecutionSnapshotCodec.js +171 -0
- package/extension/src/shared/runInputQueue.js +81 -34
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +79 -0
- package/extension/src/shared/scopedPersistenceQueuePolicy.js +200 -0
- package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
- package/extension/src/shared/scopedPersistenceTransaction.js +241 -0
- package/extension/src/shared/sessionState.js +110 -59
- package/extension/src/shared/settlementFacts.js +351 -0
- package/extension/src/shared/storageDb.js +25 -10
- package/extension/src/shared/storageMigration.js +275 -61
- package/extension/src/shared/storageSessionClaims.js +60 -0
- package/extension/src/shared/updateConsent.js +4 -0
- package/extension/src/shared/updateRevocationIntent.js +109 -0
- package/extension/src/shared/writebackEvidenceProjection.js +150 -0
- package/extension/src/shared/writebackOperationEvidence.js +153 -0
- package/extension/src/shared/writebackSettlement.js +727 -0
- package/extension/styles/panel.css +18 -8
- package/native-host/src/codexPromptAssembly.js +1 -1
- package/native-host/src/codexRuntimeIdentity.js +1 -1
- package/native-host/src/codexSessionRunner.js +7 -13
- package/native-host/src/index.js +3 -69
- package/native-host/src/managedInstall.js +50 -8
- package/native-host/src/nativeTransportEnvelope.js +148 -0
- package/native-host/src/taskRunnerRuntime.js +14 -84
- package/native-host/src/updateManager.js +36 -1
- package/package.json +13 -5
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
(function initCodexOverleafContentRuntime(root) {
|
|
2
2
|
'use strict';
|
|
3
|
-
function init() {
|
|
3
|
+
function init(dependencies) {
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
+
dependencies = dependencies || {};
|
|
7
|
+
const Modules = dependencies.modules;
|
|
8
|
+
if (!Modules || typeof Modules !== 'object') {
|
|
9
|
+
throw Object.assign(new Error('Codex Overleaf content runtime requires validated dependencies.'), {
|
|
10
|
+
code: 'content_dependency_registry_required'
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
const RUNTIME_INSTALLED_FLAG = '__codexOverleafContentRuntimeInstalled';
|
|
7
15
|
const RUNTIME_STATE_KEY = '__codexOverleafContentRuntimeState';
|
|
8
16
|
if (root[RUNTIME_INSTALLED_FLAG]) {
|
|
@@ -24,9 +32,6 @@
|
|
|
24
32
|
const LEGACY_STORAGE_KEY = 'codexOverleafPanelState';
|
|
25
33
|
const RUN_PROJECT_SYNC_MAX_AGE_MS = 30000;
|
|
26
34
|
const RUN_SNAPSHOT_ZIP_TIMEOUT_MS = 30000;
|
|
27
|
-
const SNAPSHOT_PAGE_BRIDGE_TIMEOUT_MS = 70000;
|
|
28
|
-
const COMPILE_PAGE_BRIDGE_TIMEOUT_MS = 75000;
|
|
29
|
-
const CONTEXT_FILE_LIST_PAGE_BRIDGE_TIMEOUT_MS = 35000;
|
|
30
35
|
const STREAM_RENDER_FLUSH_MS = 80;
|
|
31
36
|
const STREAM_SAVE_DELAY_MS = 1000;
|
|
32
37
|
const MAX_RUN_EVENTS = 300;
|
|
@@ -45,7 +50,6 @@
|
|
|
45
50
|
'codex.providers.clear-secret',
|
|
46
51
|
'codex.providers.delete',
|
|
47
52
|
'task.run',
|
|
48
|
-
'task.confirm',
|
|
49
53
|
'mirror.sync',
|
|
50
54
|
'mirror.patchFiles',
|
|
51
55
|
'mirror.confirmWriteback',
|
|
@@ -55,31 +59,28 @@
|
|
|
55
59
|
'skills.install',
|
|
56
60
|
'skills.remove'
|
|
57
61
|
]);
|
|
58
|
-
const
|
|
59
|
-
'getProjectSnapshot',
|
|
60
|
-
'getProjectFileList',
|
|
61
|
-
'getCompileLog',
|
|
62
|
-
'ensureReviewing',
|
|
63
|
-
'ensureEditing',
|
|
64
|
-
'waitForSaveState',
|
|
65
|
-
'applyOperations',
|
|
66
|
-
'acceptTrackedChanges',
|
|
67
|
-
'rejectTrackedChanges'
|
|
68
|
-
]);
|
|
69
|
-
const activePageBridgeCancellationHandlers = new Map();
|
|
62
|
+
const PageRpcContract = Modules.PageRpcContract;
|
|
70
63
|
const PANEL_DEFAULT_WIDTH = 380;
|
|
71
64
|
const PANEL_MIN_WIDTH = 340;
|
|
72
65
|
const PANEL_MAX_WIDTH = 760;
|
|
73
66
|
const PAGE_MIN_WIDTH = 520;
|
|
74
|
-
const CodexOverleafCompatibility =
|
|
67
|
+
const CodexOverleafCompatibility = Modules.Compatibility;
|
|
75
68
|
const INSTALL_COMMAND = CodexOverleafCompatibility?.buildInstallCommand?.(
|
|
76
69
|
undefined,
|
|
77
70
|
undefined,
|
|
78
71
|
getCurrentExtensionId()
|
|
79
72
|
) || 'curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/main/install.sh | bash -s -- --extension-id <chrome-extension-id>';
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
const pageBridgeClient = Modules.PageBridgeClient.create({
|
|
74
|
+
root,
|
|
75
|
+
window,
|
|
76
|
+
document,
|
|
77
|
+
chromeApi: chrome,
|
|
78
|
+
crypto,
|
|
79
|
+
contract: PageRpcContract,
|
|
80
|
+
compatibility: CodexOverleafCompatibility,
|
|
81
|
+
isCancellationRequested: () => runCancellationRequested
|
|
82
|
+
});
|
|
83
|
+
pageBridgeClient.start();
|
|
83
84
|
const {
|
|
84
85
|
createSession,
|
|
85
86
|
deleteSession,
|
|
@@ -92,43 +93,45 @@
|
|
|
92
93
|
selectVisibleSessionsForList,
|
|
93
94
|
setActiveSession,
|
|
94
95
|
updateActiveSession
|
|
95
|
-
} =
|
|
96
|
-
const LineReferences =
|
|
97
|
-
const { getProjectStorageKey } =
|
|
98
|
-
const { HIGH_RISK_TYPES, buildChangeSummaryLine, hasSkippedApplyOperations } =
|
|
96
|
+
} = Modules.SessionState;
|
|
97
|
+
const LineReferences = Modules.LineReferences;
|
|
98
|
+
const { getProjectStorageKey } = Modules.StorageKeys;
|
|
99
|
+
const { HIGH_RISK_TYPES, buildChangeSummaryLine, hasSkippedApplyOperations } = Modules.Summary;
|
|
99
100
|
const {
|
|
100
101
|
buildExpectedFilesAfterOperations,
|
|
101
102
|
buildSnapshotRestoreUndo,
|
|
102
103
|
buildUndoCheckpoint
|
|
103
|
-
} =
|
|
104
|
+
} = Modules.UndoOperations;
|
|
104
105
|
const { buildHumanCompletionReport, mapAgentEventToActivity, stripEmptyHtmlCommentPlaceholders,
|
|
105
|
-
translateRawError } =
|
|
106
|
-
const i18n =
|
|
107
|
-
const nativeChannel =
|
|
106
|
+
translateRawError } = Modules.AgentTranscript;
|
|
107
|
+
const i18n = Modules.I18n;
|
|
108
|
+
const nativeChannel = Modules.NativeChannel.create({
|
|
108
109
|
chrome,
|
|
109
110
|
crypto
|
|
110
111
|
});
|
|
111
|
-
const writebackController =
|
|
112
|
-
const ReviewHunks =
|
|
113
|
-
const DiffReviewPanel =
|
|
114
|
-
const ContextTray =
|
|
115
|
-
const LocalSkillsPanel =
|
|
116
|
-
const runController =
|
|
117
|
-
const RunInputQueue =
|
|
118
|
-
const RunQueueScheduler =
|
|
119
|
-
const ActiveTurnControl =
|
|
120
|
-
const PendingInputView =
|
|
121
|
-
const mirrorHealth =
|
|
122
|
-
const otWarmMirrorController =
|
|
123
|
-
const PanelRenderer =
|
|
124
|
-
const SessionPanel =
|
|
125
|
-
const SettingsPanel =
|
|
126
|
-
const DiagnosticsPanel =
|
|
127
|
-
const ComposerPanel =
|
|
128
|
-
const GovernanceRules =
|
|
129
|
-
const SensitiveScan =
|
|
130
|
-
const AuditRecords =
|
|
131
|
-
const FailureReasons =
|
|
112
|
+
const writebackController = Modules.WritebackController;
|
|
113
|
+
const ReviewHunks = Modules.ReviewHunks;
|
|
114
|
+
const DiffReviewPanel = Modules.DiffReviewPanel;
|
|
115
|
+
const ContextTray = Modules.ContextTray;
|
|
116
|
+
const LocalSkillsPanel = Modules.LocalSkillsPanel;
|
|
117
|
+
const runController = Modules.RunController;
|
|
118
|
+
const RunInputQueue = Modules.RunInputQueue;
|
|
119
|
+
const RunQueueScheduler = Modules.RunQueueScheduler;
|
|
120
|
+
const ActiveTurnControl = Modules.ActiveTurnControl;
|
|
121
|
+
const PendingInputView = Modules.PendingInputView;
|
|
122
|
+
const mirrorHealth = Modules.MirrorHealth;
|
|
123
|
+
const otWarmMirrorController = Modules.OtWarmMirrorController;
|
|
124
|
+
const PanelRenderer = Modules.PanelRenderer;
|
|
125
|
+
const SessionPanel = Modules.SessionPanel;
|
|
126
|
+
const SettingsPanel = Modules.SettingsPanel;
|
|
127
|
+
const DiagnosticsPanel = Modules.DiagnosticsPanel;
|
|
128
|
+
const ComposerPanel = Modules.ComposerPanel;
|
|
129
|
+
const GovernanceRules = Modules.GovernanceRules;
|
|
130
|
+
const SensitiveScan = Modules.SensitiveScan;
|
|
131
|
+
const AuditRecords = Modules.AuditRecords;
|
|
132
|
+
const FailureReasons = Modules.FailureReasons;
|
|
133
|
+
const RunExecutionSnapshot = Modules.RunExecutionSnapshot;
|
|
134
|
+
const WritebackSettlement = Modules.WritebackSettlement;
|
|
132
135
|
|
|
133
136
|
// Module composition (hoisted-safe zone). These wiring blocks MUST run
|
|
134
137
|
// before any controller below consumes their destructured exports by
|
|
@@ -140,7 +143,6 @@
|
|
|
140
143
|
// `rejected`; `accepted` and `rejected` are terminal. Kept in sync with
|
|
141
144
|
// sessionState.js. A run is in the tracked-change lifecycle when it carries
|
|
142
145
|
// one of these values or still holds tracked-change refs.
|
|
143
|
-
const TERMINAL_TRACKED_CHANGE_STATUS = new Set(['accepted', 'rejected']);
|
|
144
146
|
|
|
145
147
|
// The UI-local in-flight lock for tracked-change accept/reject. Never written
|
|
146
148
|
// to trackedChangeStatus and never persisted — it lives only on the controller.
|
|
@@ -173,10 +175,14 @@
|
|
|
173
175
|
// runtime collaborators they need; the destructured consts keep every
|
|
174
176
|
// existing call site unchanged. Function declarations hoist, so passing them
|
|
175
177
|
// here is safe; mutable state is handed over as lazy getters.
|
|
176
|
-
const markdownText =
|
|
178
|
+
const markdownText = Modules.MarkdownText.create({
|
|
179
|
+
LineReferences: Modules.LineReferences,
|
|
180
|
+
MathText: Modules.MathText,
|
|
177
181
|
tx,
|
|
178
182
|
callPageBridge,
|
|
183
|
+
getCurrentProjectId,
|
|
179
184
|
getCurrentProjectReferenceFiles,
|
|
185
|
+
getCurrentProjectMathSources,
|
|
180
186
|
showPluginToast,
|
|
181
187
|
recordRenderingDiagnostic: detail => appendTechnicalEvent({ type: 'rendering.diagnostic', detail })
|
|
182
188
|
});
|
|
@@ -190,8 +196,8 @@
|
|
|
190
196
|
hasUnsafeRuntimePathSegments
|
|
191
197
|
} = markdownText;
|
|
192
198
|
|
|
193
|
-
const projectSettingsCoordinator =
|
|
194
|
-
CodexOverleafTheme:
|
|
199
|
+
const projectSettingsCoordinator = Modules.ProjectSettingsCoordinator.create({
|
|
200
|
+
CodexOverleafTheme: Modules.Theme,
|
|
195
201
|
GovernanceRules,
|
|
196
202
|
SettingsPanel,
|
|
197
203
|
closeContextTray: () => closeContextTray(),
|
|
@@ -248,7 +254,11 @@
|
|
|
248
254
|
updateSkillsEntrySummary
|
|
249
255
|
} = projectSettingsCoordinator;
|
|
250
256
|
|
|
251
|
-
const otWarmMirror =
|
|
257
|
+
const otWarmMirror = Modules.OtWarmMirror.create({
|
|
258
|
+
otWarmMirrorController,
|
|
259
|
+
runController,
|
|
260
|
+
mirrorHealth,
|
|
261
|
+
projectFiles: Modules.ProjectFiles,
|
|
252
262
|
tr,
|
|
253
263
|
tx,
|
|
254
264
|
closeDiagnosticsMenu,
|
|
@@ -308,7 +318,8 @@
|
|
|
308
318
|
prepareMirrorStaleRetry
|
|
309
319
|
} = otWarmMirror;
|
|
310
320
|
|
|
311
|
-
const diagnosticsController =
|
|
321
|
+
const diagnosticsController = Modules.DiagnosticsController.create({
|
|
322
|
+
Compatibility: Modules.Compatibility,
|
|
312
323
|
tr,
|
|
313
324
|
tx,
|
|
314
325
|
getExtensionCompatibilityMetadata,
|
|
@@ -352,7 +363,8 @@
|
|
|
352
363
|
// recovery-action handlers, change-history read path, history & storage
|
|
353
364
|
// card, aggressive-compaction notice. Instantiated BEFORE runTimelineView,
|
|
354
365
|
// whose deps reference the recovery handlers.
|
|
355
|
-
const panelMaintenance =
|
|
366
|
+
const panelMaintenance = Modules.PanelMaintenance.create({
|
|
367
|
+
StorageDb: Modules.StorageDb,
|
|
356
368
|
tx,
|
|
357
369
|
showPluginToast: (...args) => showPluginToast(...args),
|
|
358
370
|
showPluginConfirm: (...args) => showPluginConfirm(...args),
|
|
@@ -386,7 +398,8 @@
|
|
|
386
398
|
openStorageSettings
|
|
387
399
|
} = panelMaintenance;
|
|
388
400
|
|
|
389
|
-
const runTimelineView =
|
|
401
|
+
const runTimelineView = Modules.RunTimelineView.create({
|
|
402
|
+
RunGuidanceView: Modules.RunGuidanceView,
|
|
390
403
|
tr,
|
|
391
404
|
tx,
|
|
392
405
|
getLocale,
|
|
@@ -413,7 +426,8 @@
|
|
|
413
426
|
showNativeSetupGuidance: () => showNativeUpdateGuidanceModal({}),
|
|
414
427
|
openProjectFileForFailure,
|
|
415
428
|
openStorageSettings,
|
|
416
|
-
trackedChangeInFlight
|
|
429
|
+
trackedChangeInFlight,
|
|
430
|
+
projectRunSettlement: run => WritebackSettlement.projectRunSettlement(run)
|
|
417
431
|
});
|
|
418
432
|
const {
|
|
419
433
|
resetAutoFollow,
|
|
@@ -435,7 +449,10 @@
|
|
|
435
449
|
configureAcceptButton
|
|
436
450
|
} = runTimelineView;
|
|
437
451
|
|
|
438
|
-
const sessionManager =
|
|
452
|
+
const sessionManager = Modules.SessionManager.create({
|
|
453
|
+
SessionPanel: Modules.SessionPanel,
|
|
454
|
+
SessionState: Modules.SessionState,
|
|
455
|
+
SessionMenuView: Modules.SessionMenuView,
|
|
439
456
|
tr,
|
|
440
457
|
showPluginConfirm,
|
|
441
458
|
showPluginToast,
|
|
@@ -466,7 +483,9 @@
|
|
|
466
483
|
updateSessionById
|
|
467
484
|
} = sessionManager;
|
|
468
485
|
|
|
469
|
-
const applyResultFormatters =
|
|
486
|
+
const applyResultFormatters = Modules.ApplyResultFormatters.create({
|
|
487
|
+
i18n: Modules.I18n,
|
|
488
|
+
FailureReasons: Modules.FailureReasons,
|
|
470
489
|
getLocale,
|
|
471
490
|
tr,
|
|
472
491
|
tx,
|
|
@@ -484,12 +503,20 @@
|
|
|
484
503
|
formatOperationFiles
|
|
485
504
|
} = applyResultFormatters;
|
|
486
505
|
|
|
506
|
+
const runSettlementPersistence = Modules.RunSettlementPersistence.create({
|
|
507
|
+
writebackSettlement: WritebackSettlement,
|
|
508
|
+
getCurrentRunView: () => currentRunView,
|
|
509
|
+
findRunRecord,
|
|
510
|
+
getCurrentProjectId,
|
|
511
|
+
saveStateSoon
|
|
512
|
+
});
|
|
513
|
+
|
|
487
514
|
// v1.6.3 structural-debt phase 6: the sync-writeback orchestration
|
|
488
515
|
// (applySyncChangesToOverleaf + post-write verify/mirror/compile pipeline)
|
|
489
516
|
// lives in writebackOrchestrator.js. Every dep below is either a hoisted
|
|
490
517
|
// function declaration, an already-initialized const above, or a lazy
|
|
491
518
|
// accessor thunk — safe in the top-of-init() wiring zone.
|
|
492
|
-
const writebackOrchestrator =
|
|
519
|
+
const writebackOrchestrator = Modules.WritebackOrchestrator.create({
|
|
493
520
|
tr,
|
|
494
521
|
tx,
|
|
495
522
|
getLocale,
|
|
@@ -499,7 +526,6 @@
|
|
|
499
526
|
appendOperationsPreview,
|
|
500
527
|
appendPartialWritebackWarning,
|
|
501
528
|
appendApplyResult,
|
|
502
|
-
renderDiffReview,
|
|
503
529
|
renderReadOnlyDiffReview,
|
|
504
530
|
showPluginConfirm,
|
|
505
531
|
callPageBridge,
|
|
@@ -529,9 +555,12 @@
|
|
|
529
555
|
confirmBinaryOperations,
|
|
530
556
|
filterSyncChangesByOperations,
|
|
531
557
|
writebackController,
|
|
558
|
+
writebackSettlement: WritebackSettlement,
|
|
559
|
+
compileAdapter: Modules.CompileAdapter,
|
|
532
560
|
RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
|
|
533
561
|
getState: () => state,
|
|
534
|
-
getCurrentRunView: () => currentRunView
|
|
562
|
+
getCurrentRunView: () => currentRunView,
|
|
563
|
+
onMirrorRefreshSettled: runSettlementPersistence.settleMirrorRefresh
|
|
535
564
|
});
|
|
536
565
|
const {
|
|
537
566
|
applySyncChangesToOverleaf,
|
|
@@ -570,7 +599,8 @@
|
|
|
570
599
|
}
|
|
571
600
|
}
|
|
572
601
|
|
|
573
|
-
const modelPicker =
|
|
602
|
+
const modelPicker = Modules.ModelPicker.create({
|
|
603
|
+
Support: Modules.ModelPickerSupport,
|
|
574
604
|
tr,
|
|
575
605
|
tx,
|
|
576
606
|
getLocale,
|
|
@@ -603,7 +633,7 @@
|
|
|
603
633
|
updateModelDisplay
|
|
604
634
|
} = modelPicker;
|
|
605
635
|
|
|
606
|
-
const projectProviderSelection =
|
|
636
|
+
const projectProviderSelection = Modules.ProjectProviderSelection.create({
|
|
607
637
|
tx, tr, getRunningSessionIds, showToast: showPluginToast, showConfirm: showPluginConfirm,
|
|
608
638
|
getState: () => state, setState: next => { state = next; }, getPanel: () => panel,
|
|
609
639
|
readSelectedModel: readSelectedModelInput, readSelectedSpeed: readSelectedSpeedInput,
|
|
@@ -611,7 +641,9 @@
|
|
|
611
641
|
normalizeState: normalizePanelState, applyStateToPanel
|
|
612
642
|
});
|
|
613
643
|
|
|
614
|
-
const providerSettingsCoordinator =
|
|
644
|
+
const providerSettingsCoordinator = Modules.ProviderSettingsCoordinator.create({
|
|
645
|
+
ProviderProfiles: Modules.ProviderProfiles,
|
|
646
|
+
ProviderSettingsDialog: Modules.ProviderSettingsDialog,
|
|
615
647
|
tx,
|
|
616
648
|
sendBackgroundNative,
|
|
617
649
|
document,
|
|
@@ -658,7 +690,13 @@
|
|
|
658
690
|
}
|
|
659
691
|
});
|
|
660
692
|
|
|
661
|
-
const recentProjects =
|
|
693
|
+
const recentProjects = Modules.RecentProjects.create({
|
|
694
|
+
ProjectSessionCleanup: Modules.ProjectSessionCleanup,
|
|
695
|
+
SessionPersistence: Modules.SessionPersistence,
|
|
696
|
+
SessionState: Modules.SessionState,
|
|
697
|
+
StorageDb: Modules.StorageDb,
|
|
698
|
+
StorageKeys: Modules.StorageKeys,
|
|
699
|
+
StorageMigration: Modules.StorageMigration,
|
|
662
700
|
tr,
|
|
663
701
|
tx,
|
|
664
702
|
openCustomInstructionsSettings,
|
|
@@ -690,7 +728,7 @@
|
|
|
690
728
|
const MAX_COMPOSER_ATTACHMENT_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
691
729
|
const MAX_COMPOSER_ATTACHMENTS = 8;
|
|
692
730
|
const MAX_ATTACHMENT_PREVIEW_DATA_URL_CHARS = 768 * 1024;
|
|
693
|
-
const composerAttachmentController =
|
|
731
|
+
const composerAttachmentController = Modules.ComposerAttachments.createComposerAttachmentController({
|
|
694
732
|
getPanel: () => panel,
|
|
695
733
|
tr,
|
|
696
734
|
tx,
|
|
@@ -708,6 +746,7 @@
|
|
|
708
746
|
reviewHunks: ReviewHunks,
|
|
709
747
|
tr,
|
|
710
748
|
callPageBridge,
|
|
749
|
+
getCurrentProjectId: () => getCurrentProjectId(),
|
|
711
750
|
getRunEvents: () => currentRunView?.events,
|
|
712
751
|
appendRunEvent,
|
|
713
752
|
scrollLogToBottom,
|
|
@@ -736,7 +775,7 @@
|
|
|
736
775
|
closeDiagnosticsMenu,
|
|
737
776
|
closeCustomInstructionsSettings,
|
|
738
777
|
closeSlashMenu,
|
|
739
|
-
projectFiles:
|
|
778
|
+
projectFiles: Modules.ProjectFiles
|
|
740
779
|
});
|
|
741
780
|
const localSkillsPanel = LocalSkillsPanel.createLocalSkillsPanelController({
|
|
742
781
|
root: window,
|
|
@@ -795,6 +834,8 @@
|
|
|
795
834
|
// (captured at the start of the in-flight save) be the last writer.
|
|
796
835
|
let saveStateInFlight = false;
|
|
797
836
|
let saveStateRunAfterFlight = false;
|
|
837
|
+
let saveStateInFlightPromise = null;
|
|
838
|
+
let pendingSaveStateOptions = null;
|
|
798
839
|
let streamRenderTimer = null;
|
|
799
840
|
let pendingStreamRenderEvents = new Map();
|
|
800
841
|
let storageNoticeKeys = new Set();
|
|
@@ -806,7 +847,35 @@
|
|
|
806
847
|
let runCancellationRequested = false;
|
|
807
848
|
let runCancellationController = null;
|
|
808
849
|
let pluginConfirmController = null;
|
|
809
|
-
|
|
850
|
+
let scopedPersistenceCoordinator = null;
|
|
851
|
+
const nativeCompatibilityController = Modules.NativeCompatibilityController.create({
|
|
852
|
+
compatibility: CodexOverleafCompatibility,
|
|
853
|
+
nativeChannel,
|
|
854
|
+
gatedMethods: NATIVE_COMPATIBILITY_GATED_METHODS,
|
|
855
|
+
getExtensionCompatibilityMetadata,
|
|
856
|
+
fallbackNativeCompatibility,
|
|
857
|
+
getNativeCompatibilityClassification,
|
|
858
|
+
installCommand: INSTALL_COMMAND,
|
|
859
|
+
throwIfCancellationRequested: throwIfRunCancellationRequested,
|
|
860
|
+
getCurrentRunView: () => currentRunView,
|
|
861
|
+
appendRunEvent,
|
|
862
|
+
showPluginToast,
|
|
863
|
+
getPanel: () => panel,
|
|
864
|
+
ensurePanelOpen,
|
|
865
|
+
getPanelRendererInstance: () => panelRendererInstance,
|
|
866
|
+
setBadge: (...args) => PanelRenderer.setBadge(...args),
|
|
867
|
+
setDiagnosticsHealth,
|
|
868
|
+
getState: () => state,
|
|
869
|
+
localStorage: root.localStorage,
|
|
870
|
+
document,
|
|
871
|
+
navigator,
|
|
872
|
+
chromeApi: chrome,
|
|
873
|
+
onboardingTipStorageKey: ONBOARDING_TIP_STORAGE_KEY,
|
|
874
|
+
setTimeout: (...args) => root.setTimeout(...args),
|
|
875
|
+
tr,
|
|
876
|
+
tx
|
|
877
|
+
});
|
|
878
|
+
const updateIdleClient = Modules.UpdateIdle?.create({
|
|
810
879
|
document,
|
|
811
880
|
getBusyState: () => ({
|
|
812
881
|
run: Boolean(currentRunView),
|
|
@@ -878,6 +947,7 @@
|
|
|
878
947
|
});
|
|
879
948
|
|
|
880
949
|
async function init() {
|
|
950
|
+
await refreshAccountScopeId();
|
|
881
951
|
storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
|
|
882
952
|
state = normalizePanelState(await loadStoredState(), { restoreRunningRuns: true });
|
|
883
953
|
initializeRunQueueScheduler();
|
|
@@ -890,7 +960,7 @@
|
|
|
890
960
|
.catch(() => {});
|
|
891
961
|
}, 900);
|
|
892
962
|
ensurePanelOpen();
|
|
893
|
-
|
|
963
|
+
Modules.UpdateNotice?.mount?.(panel?.panelEl || panel, {
|
|
894
964
|
getLocale: () => state?.locale || 'en'
|
|
895
965
|
});
|
|
896
966
|
// v1.8.1: on a non-editor route the panel must NEVER first paint the
|
|
@@ -1441,9 +1511,6 @@
|
|
|
1441
1511
|
if (mode === 'ask') {
|
|
1442
1512
|
return 'Ask';
|
|
1443
1513
|
}
|
|
1444
|
-
if (mode === 'confirm') {
|
|
1445
|
-
return 'Confirm';
|
|
1446
|
-
}
|
|
1447
1514
|
if (mode === 'auto') {
|
|
1448
1515
|
return 'Auto';
|
|
1449
1516
|
}
|
|
@@ -1554,7 +1621,7 @@
|
|
|
1554
1621
|
}
|
|
1555
1622
|
|
|
1556
1623
|
async function getRecentAuditLogsForCurrentProject(limit = 12) {
|
|
1557
|
-
const StorageDb =
|
|
1624
|
+
const StorageDb = Modules.StorageDb;
|
|
1558
1625
|
if (!StorageDb?.getAllByIndex) {
|
|
1559
1626
|
return [];
|
|
1560
1627
|
}
|
|
@@ -1673,20 +1740,36 @@
|
|
|
1673
1740
|
await pendingMirror.catch(() => {});
|
|
1674
1741
|
}
|
|
1675
1742
|
readPanelInputs();
|
|
1676
|
-
// Freeze submitted run identity before the panel can change during the run.
|
|
1677
|
-
const submittedMode = state.mode;
|
|
1678
|
-
const submittedRequireReviewing = state.requireReviewing === true;
|
|
1679
|
-
const submittedCustomInstructions = getCustomInstructionsForCurrentProject();
|
|
1680
|
-
const submittedSkillLoadingSettings = getSkillLoadingSettings();
|
|
1681
|
-
const submittedAttachments = getComposerAttachmentsForRun();
|
|
1682
|
-
const submittedSkillInvocation = getComposerSkillInvocationForRun();
|
|
1683
|
-
|
|
1684
1743
|
const queuedInput = options.queuedInput || null;
|
|
1685
1744
|
const task = String(queuedInput?.text || state.task || '').trim();
|
|
1686
1745
|
if (!task) {
|
|
1687
1746
|
appendLog(tx('Enter a task first.', '请先输入任务。'));
|
|
1688
1747
|
return;
|
|
1689
1748
|
}
|
|
1749
|
+
const submittedPanelState = {
|
|
1750
|
+
mode: state.mode,
|
|
1751
|
+
providerId: state.providerId,
|
|
1752
|
+
model: state.model,
|
|
1753
|
+
reasoningEffort: state.reasoningEffort,
|
|
1754
|
+
speedTier: state.speedTier,
|
|
1755
|
+
autoRecompile: state.autoRecompile !== false,
|
|
1756
|
+
requireReviewing: state.requireReviewing === true,
|
|
1757
|
+
focusFiles: getActiveFocusFiles()
|
|
1758
|
+
};
|
|
1759
|
+
const submittedCustomInstructions = getCustomInstructionsForCurrentProject();
|
|
1760
|
+
const submittedSkillLoadingSettings = getSkillLoadingSettings();
|
|
1761
|
+
const submittedAttachments = getComposerAttachmentsForRun();
|
|
1762
|
+
const submittedSkillInvocation = getComposerSkillInvocationForRun();
|
|
1763
|
+
await providerSettingsCoordinator.ensureLoaded();
|
|
1764
|
+
const executionSnapshot = queuedInput
|
|
1765
|
+
? RunExecutionSnapshot.resolveForExecution(
|
|
1766
|
+
RunExecutionSnapshot.fromQueuePayload(queuedInput.payload),
|
|
1767
|
+
{ catalog: providerSettingsCoordinator.getCatalog() }
|
|
1768
|
+
)
|
|
1769
|
+
: captureCurrentExecutionSnapshot(submittedPanelState);
|
|
1770
|
+
// Freeze submitted run identity before the panel can change during the run.
|
|
1771
|
+
const submittedMode = executionSnapshot.mode;
|
|
1772
|
+
const submittedRequireReviewing = executionSnapshot.requireReviewing === true;
|
|
1690
1773
|
|
|
1691
1774
|
runCancellationRequested = false;
|
|
1692
1775
|
runCancellationController = new AbortController();
|
|
@@ -1694,24 +1777,26 @@
|
|
|
1694
1777
|
currentRunView = startRunView({
|
|
1695
1778
|
task,
|
|
1696
1779
|
mode: submittedMode,
|
|
1697
|
-
model:
|
|
1698
|
-
reasoningEffort:
|
|
1699
|
-
speedTier:
|
|
1780
|
+
model: executionSnapshot.model,
|
|
1781
|
+
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
1782
|
+
speedTier: executionSnapshot.speedTier,
|
|
1783
|
+
executionSnapshot,
|
|
1700
1784
|
attachments: submittedAttachments,
|
|
1701
1785
|
skillInvocation: submittedSkillInvocation,
|
|
1702
1786
|
queueItemId: queuedInput?.id || ''
|
|
1703
1787
|
});
|
|
1704
1788
|
const runSessionId = currentRunView.sessionId;
|
|
1705
|
-
if (queuedInput?.id) {
|
|
1706
|
-
await runQueueScheduler?.markExecuting(runSessionId, queuedInput.id, currentRunView.recordId);
|
|
1707
|
-
}
|
|
1708
1789
|
let runAuditDraft = null;
|
|
1709
1790
|
try {
|
|
1791
|
+
if (queuedInput?.id) {
|
|
1792
|
+
await runQueueScheduler?.markExecuting(runSessionId, queuedInput.id, currentRunView.recordId);
|
|
1793
|
+
}
|
|
1710
1794
|
runAuditDraft = await awaitRunStep(createAuditDraftForRun({
|
|
1711
1795
|
task,
|
|
1712
1796
|
sessionId: runSessionId,
|
|
1713
1797
|
mode: submittedMode,
|
|
1714
|
-
focusFiles:
|
|
1798
|
+
focusFiles: executionSnapshot.focusFiles,
|
|
1799
|
+
executionSnapshot
|
|
1715
1800
|
}));
|
|
1716
1801
|
announceCrossTabRunStart();
|
|
1717
1802
|
// Binary attachments belong to this submitted turn. The run record has
|
|
@@ -1726,13 +1811,13 @@
|
|
|
1726
1811
|
status: 'running',
|
|
1727
1812
|
detail: {
|
|
1728
1813
|
[tr('mode')]: formatModeLabel(submittedMode),
|
|
1729
|
-
[tx('Model', '模型')]:
|
|
1730
|
-
[tx('Reasoning effort', '推理强度')]:
|
|
1731
|
-
[tx('Speed', '速度')]:
|
|
1814
|
+
[tx('Model', '模型')]: executionSnapshot.model,
|
|
1815
|
+
[tx('Reasoning effort', '推理强度')]: executionSnapshot.reasoningEffort,
|
|
1816
|
+
[tx('Speed', '速度')]: executionSnapshot.speedTier,
|
|
1732
1817
|
[tx('Track required', '要求留痕')]: submittedRequireReviewing ? tx('yes', '是') : tx('no', '否'),
|
|
1733
1818
|
[tx('Skill', '技能')]: submittedSkillInvocation?.title || tr('noneValue'),
|
|
1734
1819
|
[tx('Attachments', '附件')]: submittedAttachments.map(attachment => attachment.name).join(listSeparator()) || tr('noneValue'),
|
|
1735
|
-
'@context': formatContextItems(
|
|
1820
|
+
'@context': formatContextItems(executionSnapshot.focusFiles)
|
|
1736
1821
|
}
|
|
1737
1822
|
});
|
|
1738
1823
|
if (submittedSkillInvocation?.id === 'skill-installer') {
|
|
@@ -1744,15 +1829,16 @@
|
|
|
1744
1829
|
submittedCustomInstructions,
|
|
1745
1830
|
submittedSkillLoadingSettings,
|
|
1746
1831
|
submittedAttachments,
|
|
1747
|
-
submittedSkillInvocation
|
|
1832
|
+
submittedSkillInvocation,
|
|
1833
|
+
executionSnapshot
|
|
1748
1834
|
}));
|
|
1749
1835
|
return;
|
|
1750
1836
|
}
|
|
1751
1837
|
appendRunEvent({
|
|
1752
1838
|
// @context prioritizes these files; complete project runs may still update related files.
|
|
1753
1839
|
title: tx(
|
|
1754
|
-
`This run will prioritize: ${formatContextItems(
|
|
1755
|
-
`本轮将优先处理:${formatContextItems(
|
|
1840
|
+
`This run will prioritize: ${formatContextItems(executionSnapshot.focusFiles)} (persists across turns; clear via +)`,
|
|
1841
|
+
`本轮将优先处理:${formatContextItems(executionSnapshot.focusFiles)}(跨轮保留,可在 + 中清除)`
|
|
1756
1842
|
),
|
|
1757
1843
|
status: 'completed'
|
|
1758
1844
|
});
|
|
@@ -1770,7 +1856,7 @@
|
|
|
1770
1856
|
return;
|
|
1771
1857
|
}
|
|
1772
1858
|
|
|
1773
|
-
let focusFiles =
|
|
1859
|
+
let focusFiles = [...executionSnapshot.focusFiles];
|
|
1774
1860
|
const warmStart = await awaitRunStep(resolveWarmRunStart({ focusFiles, mode: submittedMode }));
|
|
1775
1861
|
let project = warmStart.project || null;
|
|
1776
1862
|
let useExistingMirror = warmStart.useExistingMirror;
|
|
@@ -1787,7 +1873,7 @@
|
|
|
1787
1873
|
if (!useExistingMirror) {
|
|
1788
1874
|
appendLog(formatProjectSnapshotUserLog(project));
|
|
1789
1875
|
}
|
|
1790
|
-
|
|
1876
|
+
persistCurrentProjectReferenceFiles(project);
|
|
1791
1877
|
const snapshotWarnings = useExistingMirror
|
|
1792
1878
|
? { blocking: [], nonBlocking: [] }
|
|
1793
1879
|
: getProjectSnapshotWarnings(project);
|
|
@@ -1802,7 +1888,7 @@
|
|
|
1802
1888
|
project,
|
|
1803
1889
|
snapshotWarnings,
|
|
1804
1890
|
focusFiles,
|
|
1805
|
-
isUsableProjectFileContent:
|
|
1891
|
+
isUsableProjectFileContent: Modules.ProjectFiles.isUsableProjectFileContent
|
|
1806
1892
|
});
|
|
1807
1893
|
let restrictToFocusFiles = runController.shouldRestrictWritebackToFocus({ focusFiles, focusedPartialSnapshot });
|
|
1808
1894
|
if (snapshotWarnings.blocking.length && !warmMirrorReuse.useExistingMirror && !focusedPartialSnapshot) {
|
|
@@ -1834,7 +1920,7 @@
|
|
|
1834
1920
|
skippedFiles: [],
|
|
1835
1921
|
blockedFiles: snapshotWarnings.blocking.map(reason => ({ path: 'project', reason }))
|
|
1836
1922
|
});
|
|
1837
|
-
finishRunView(tx('Blocked: full project was not read', '已阻止:没有读到完整项目'), 'failed');
|
|
1923
|
+
await finishRunView(tx('Blocked: full project was not read', '已阻止:没有读到完整项目'), 'failed');
|
|
1838
1924
|
return;
|
|
1839
1925
|
}
|
|
1840
1926
|
|
|
@@ -1921,7 +2007,7 @@
|
|
|
1921
2007
|
reason: finding.detectorId || 'sensitive'
|
|
1922
2008
|
}))
|
|
1923
2009
|
});
|
|
1924
|
-
finishRunView(tx('Blocked: sensitive content review required', '已阻止:需要处理敏感内容'), 'failed');
|
|
2010
|
+
await finishRunView(tx('Blocked: sensitive content review required', '已阻止:需要处理敏感内容'), 'failed');
|
|
1925
2011
|
return;
|
|
1926
2012
|
}
|
|
1927
2013
|
if (sensitiveFindings.findings.length) {
|
|
@@ -1959,7 +2045,7 @@
|
|
|
1959
2045
|
return;
|
|
1960
2046
|
}
|
|
1961
2047
|
project = staleRetry.project;
|
|
1962
|
-
|
|
2048
|
+
persistCurrentProjectReferenceFiles(project);
|
|
1963
2049
|
useExistingMirror = false;
|
|
1964
2050
|
fileOverlays = null;
|
|
1965
2051
|
otWarmStart = false;
|
|
@@ -1986,7 +2072,7 @@
|
|
|
1986
2072
|
const userChoice = await showThreadResumeFailedPrompt();
|
|
1987
2073
|
if (userChoice === 'new') {
|
|
1988
2074
|
updateSessionById(runSessionId, { codexThreadId: '' });
|
|
1989
|
-
const StorageDb =
|
|
2075
|
+
const StorageDb = Modules.StorageDb;
|
|
1990
2076
|
if (StorageDb) {
|
|
1991
2077
|
const record = await StorageDb.getRecord('sessions', runSessionId);
|
|
1992
2078
|
if (record) {
|
|
@@ -2014,7 +2100,7 @@
|
|
|
2014
2100
|
} else {
|
|
2015
2101
|
appendRunEvent({ title: tx('Cancelled: user chose not to create a new thread.', '已取消:用户选择不新建线程。'), status: 'rejected' });
|
|
2016
2102
|
await finalizeAuditRecord(runAuditDraft, { resultStatus: 'rejected' });
|
|
2017
|
-
finishRunView(tx('Cancelled', '已取消'), 'rejected');
|
|
2103
|
+
await finishRunView(tx('Cancelled', '已取消'), 'rejected');
|
|
2018
2104
|
return;
|
|
2019
2105
|
}
|
|
2020
2106
|
}
|
|
@@ -2022,7 +2108,7 @@
|
|
|
2022
2108
|
if (!response.ok) {
|
|
2023
2109
|
if (runCancellationRequested || isRunCancellationError(response.error)) {
|
|
2024
2110
|
appendRunCancelledReport();
|
|
2025
|
-
finishRunView(tx('Cancelled', '已中断'), 'rejected');
|
|
2111
|
+
await finishRunView(tx('Cancelled', '已中断'), 'rejected');
|
|
2026
2112
|
void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
|
|
2027
2113
|
return;
|
|
2028
2114
|
}
|
|
@@ -2109,7 +2195,7 @@
|
|
|
2109
2195
|
sensitiveFindings: sensitiveFindings.findings,
|
|
2110
2196
|
blockedFiles: [{ path: 'codex.run', reason: response.error?.code || response.error?.message || 'native_error' }]
|
|
2111
2197
|
});
|
|
2112
|
-
finishRunView(response.error?.code === 'project_locked'
|
|
2198
|
+
await finishRunView(response.error?.code === 'project_locked'
|
|
2113
2199
|
? tx('Codex task already running', 'Codex 任务正在运行')
|
|
2114
2200
|
: tx('Local Codex error', '本地 Codex 错误'), 'failed');
|
|
2115
2201
|
return;
|
|
@@ -2159,7 +2245,7 @@
|
|
|
2159
2245
|
sensitiveFindings: sensitiveFindings.findings,
|
|
2160
2246
|
blockedFiles: [{ path: 'codex.run', reason: 'codex_no_usable_result' }]
|
|
2161
2247
|
});
|
|
2162
|
-
finishRunView(tx('Local Codex returned nothing usable', '本地 Codex 没有返回可用结果'), 'failed');
|
|
2248
|
+
await finishRunView(tx('Local Codex returned nothing usable', '本地 Codex 没有返回可用结果'), 'failed');
|
|
2163
2249
|
return;
|
|
2164
2250
|
}
|
|
2165
2251
|
const writebackProject = useExistingMirror
|
|
@@ -2169,8 +2255,10 @@
|
|
|
2169
2255
|
assistantMessage,
|
|
2170
2256
|
unsupportedChanges: response.result.unsupportedChanges || [],
|
|
2171
2257
|
mode: submittedMode,
|
|
2172
|
-
requireReviewing: submittedRequireReviewing
|
|
2258
|
+
requireReviewing: submittedRequireReviewing,
|
|
2259
|
+
autoRecompile: executionSnapshot.autoRecompile
|
|
2173
2260
|
});
|
|
2261
|
+
runSettlementPersistence.applyToCurrentRun(syncOutcome.settlement);
|
|
2174
2262
|
if (runCancellationRequested) {
|
|
2175
2263
|
throw createRunCancellationError();
|
|
2176
2264
|
}
|
|
@@ -2180,7 +2268,7 @@
|
|
|
2180
2268
|
resultStatus: syncOutcome.hasSkippedOperations ? 'completed_with_skips' : 'completed'
|
|
2181
2269
|
});
|
|
2182
2270
|
|
|
2183
|
-
finishRunView(
|
|
2271
|
+
await finishRunView(
|
|
2184
2272
|
syncOutcome.hasSkippedOperations ? tx('Sync completed with skipped items', '同步完成但有跳过项') : tx('Sync completed', '同步完成'),
|
|
2185
2273
|
syncOutcome.hasSkippedOperations ? 'failed' : 'completed'
|
|
2186
2274
|
);
|
|
@@ -2213,7 +2301,11 @@
|
|
|
2213
2301
|
// can catch early-return branches and unrecognized skip codes.
|
|
2214
2302
|
const postNavigationStatus = settleRunAfterNavigation(finishedRunRecord, syncOutcome);
|
|
2215
2303
|
if (postNavigationStatus) {
|
|
2216
|
-
persistPostNavigationRunStatus(
|
|
2304
|
+
await persistPostNavigationRunStatus(
|
|
2305
|
+
finishedRunRecord,
|
|
2306
|
+
postNavigationStatus,
|
|
2307
|
+
syncOutcome.settlement
|
|
2308
|
+
);
|
|
2217
2309
|
}
|
|
2218
2310
|
}
|
|
2219
2311
|
} catch (_settlementError) {
|
|
@@ -2242,7 +2334,7 @@
|
|
|
2242
2334
|
const returnedThreadId = response.result?.threadId || '';
|
|
2243
2335
|
if (returnedThreadId && returnedThreadId !== codexThreadId) {
|
|
2244
2336
|
updateSessionById(runSessionId, { codexThreadId: returnedThreadId });
|
|
2245
|
-
const StorageDb =
|
|
2337
|
+
const StorageDb = Modules.StorageDb;
|
|
2246
2338
|
if (StorageDb) {
|
|
2247
2339
|
const record = await StorageDb.getRecord('sessions', runSessionId);
|
|
2248
2340
|
if (record) {
|
|
@@ -2270,7 +2362,7 @@
|
|
|
2270
2362
|
} catch (error) {
|
|
2271
2363
|
if (runCancellationRequested || isRunCancellationError(error)) {
|
|
2272
2364
|
appendRunCancelledReport();
|
|
2273
|
-
finishRunView(tx('Cancelled', '已中断'), 'rejected');
|
|
2365
|
+
await finishRunView(tx('Cancelled', '已中断'), 'rejected');
|
|
2274
2366
|
void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
|
|
2275
2367
|
return;
|
|
2276
2368
|
}
|
|
@@ -2320,7 +2412,7 @@
|
|
|
2320
2412
|
resultStatus: 'failed',
|
|
2321
2413
|
blockedFiles: [{ path: 'task', reason: error.message }]
|
|
2322
2414
|
});
|
|
2323
|
-
finishRunView(tx('Task failed', '任务失败'), 'failed');
|
|
2415
|
+
await finishRunView(tx('Task failed', '任务失败'), 'failed');
|
|
2324
2416
|
} finally {
|
|
2325
2417
|
const settledView = currentRunView;
|
|
2326
2418
|
const settledRecord = settledView ? findRunRecord(settledView.recordId, settledView.sessionId) : null;
|
|
@@ -2334,13 +2426,13 @@
|
|
|
2334
2426
|
setRunning(false);
|
|
2335
2427
|
nativeChannel.clearActiveRequest();
|
|
2336
2428
|
stopRunElapsedTick();
|
|
2337
|
-
currentRunView = null;
|
|
2338
|
-
runCancellationRequested = false;
|
|
2339
|
-
runCancellationController = null;
|
|
2340
2429
|
if (isExperimentalOtEnabled()) {
|
|
2341
2430
|
await resumeOtWarmMirror('run-settled');
|
|
2342
2431
|
}
|
|
2343
|
-
await
|
|
2432
|
+
await flushQueuedSaveState().catch(() => {});
|
|
2433
|
+
currentRunView = null;
|
|
2434
|
+
runCancellationRequested = false;
|
|
2435
|
+
runCancellationController = null;
|
|
2344
2436
|
activeTurnControl.release(settlement.requestId);
|
|
2345
2437
|
if (settlement.requestId) {
|
|
2346
2438
|
activeTurnControl.acknowledge(settlement.requestId).catch(() => {});
|
|
@@ -2357,7 +2449,8 @@
|
|
|
2357
2449
|
submittedCustomInstructions,
|
|
2358
2450
|
submittedSkillLoadingSettings,
|
|
2359
2451
|
submittedAttachments,
|
|
2360
|
-
submittedSkillInvocation
|
|
2452
|
+
submittedSkillInvocation,
|
|
2453
|
+
executionSnapshot
|
|
2361
2454
|
}) {
|
|
2362
2455
|
appendRunEvent({
|
|
2363
2456
|
title: tx('Starting the Codex skill installer.', '正在启动 Codex skill installer。'),
|
|
@@ -2369,9 +2462,9 @@
|
|
|
2369
2462
|
projectId: getCurrentProjectId(),
|
|
2370
2463
|
mode: submittedMode,
|
|
2371
2464
|
task,
|
|
2372
|
-
model:
|
|
2373
|
-
reasoningEffort:
|
|
2374
|
-
speedTier:
|
|
2465
|
+
model: executionSnapshot.model,
|
|
2466
|
+
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
2467
|
+
speedTier: executionSnapshot.speedTier,
|
|
2375
2468
|
session: state.session,
|
|
2376
2469
|
threadId: codexThreadId || undefined,
|
|
2377
2470
|
customInstructions: submittedCustomInstructions || undefined,
|
|
@@ -2392,7 +2485,7 @@
|
|
|
2392
2485
|
if (runCancellationRequested || isRunCancellationError(response.error)) {
|
|
2393
2486
|
appendRunCancelledReport();
|
|
2394
2487
|
await finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
|
|
2395
|
-
finishRunView(tx('Cancelled', '已中断'), 'rejected');
|
|
2488
|
+
await finishRunView(tx('Cancelled', '已中断'), 'rejected');
|
|
2396
2489
|
return;
|
|
2397
2490
|
}
|
|
2398
2491
|
const translated = translateRawError(response.error.message, { mode: submittedMode, locale: getLocale() });
|
|
@@ -2425,7 +2518,7 @@
|
|
|
2425
2518
|
resultStatus: 'failed',
|
|
2426
2519
|
blockedFiles: [{ path: 'skill-installer', reason: response.error?.code || response.error?.message || 'native_error' }]
|
|
2427
2520
|
});
|
|
2428
|
-
finishRunView(tx('Skill installer failed', 'Skill installer 失败'), 'failed');
|
|
2521
|
+
await finishRunView(tx('Skill installer failed', 'Skill installer 失败'), 'failed');
|
|
2429
2522
|
return;
|
|
2430
2523
|
}
|
|
2431
2524
|
|
|
@@ -2439,7 +2532,7 @@
|
|
|
2439
2532
|
mode: submittedMode
|
|
2440
2533
|
});
|
|
2441
2534
|
await finalizeAuditRecord(runAuditDraft, { resultStatus: 'completed' });
|
|
2442
|
-
finishRunView(tx('Skill installer completed', 'Skill installer 已完成'), 'completed');
|
|
2535
|
+
await finishRunView(tx('Skill installer completed', 'Skill installer 已完成'), 'completed');
|
|
2443
2536
|
|
|
2444
2537
|
const runSessionForHistory = findSessionById(runSessionId) || getActiveSession(state);
|
|
2445
2538
|
const rawAssistantMessage = assistantMessage;
|
|
@@ -2463,7 +2556,7 @@
|
|
|
2463
2556
|
const returnedThreadId = response.result?.threadId || '';
|
|
2464
2557
|
if (returnedThreadId && returnedThreadId !== codexThreadId) {
|
|
2465
2558
|
updateSessionById(runSessionId, { codexThreadId: returnedThreadId });
|
|
2466
|
-
const StorageDb =
|
|
2559
|
+
const StorageDb = Modules.StorageDb;
|
|
2467
2560
|
if (StorageDb) {
|
|
2468
2561
|
const record = await StorageDb.getRecord('sessions', runSessionId);
|
|
2469
2562
|
if (record) {
|
|
@@ -2562,7 +2655,7 @@
|
|
|
2562
2655
|
const finishTitle = requireReviewing
|
|
2563
2656
|
? tx('Not started: could not enable Track Changes', '未开始:无法开启留痕')
|
|
2564
2657
|
: tx('Not started: could not switch to Editing', '未开始:无法切换到 Editing');
|
|
2565
|
-
finishRunView(finishTitle, 'failed');
|
|
2658
|
+
await finishRunView(finishTitle, 'failed');
|
|
2566
2659
|
return {
|
|
2567
2660
|
ok: false,
|
|
2568
2661
|
reason,
|
|
@@ -2597,7 +2690,7 @@
|
|
|
2597
2690
|
});
|
|
2598
2691
|
}
|
|
2599
2692
|
|
|
2600
|
-
const runGuidanceController =
|
|
2693
|
+
const runGuidanceController = Modules.RunGuidanceController.create({
|
|
2601
2694
|
findRunRecord, appendRunEvent, getCurrentRunView: () => currentRunView,
|
|
2602
2695
|
renderRunEvent, cssEscape, bumpUnreadIfDetached
|
|
2603
2696
|
});
|
|
@@ -2612,11 +2705,10 @@
|
|
|
2612
2705
|
readInputs: readPanelInputs,
|
|
2613
2706
|
getTask: () => panel?.querySelector('[data-task]')?.value || '',
|
|
2614
2707
|
hasAttachments: () => getComposerAttachmentsForRun().length > 0,
|
|
2615
|
-
capturePayload: () => (
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
focusFiles: getActiveFocusFiles()
|
|
2708
|
+
capturePayload: () => RunExecutionSnapshot.toQueuePayload(captureCurrentExecutionSnapshot()),
|
|
2709
|
+
prepareClaim: () => providerSettingsCoordinator.ensureLoaded(),
|
|
2710
|
+
resolveExecutionSnapshot: snapshot => RunExecutionSnapshot.resolveForExecution(snapshot, {
|
|
2711
|
+
catalog: providerSettingsCoordinator.getCatalog()
|
|
2620
2712
|
}),
|
|
2621
2713
|
clearTask: () => {
|
|
2622
2714
|
panel.querySelector('[data-task]').value = '';
|
|
@@ -2624,15 +2716,11 @@
|
|
|
2624
2716
|
autosizeTaskTextarea();
|
|
2625
2717
|
syncComposerSendAvailability();
|
|
2626
2718
|
},
|
|
2627
|
-
applyQueuedInput: item => {
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
requireReviewing: item.payload?.requireReviewing !== false,
|
|
2633
|
-
focusFiles: item.payload?.focusFiles || []
|
|
2634
|
-
});
|
|
2635
|
-
applyStateToPanel();
|
|
2719
|
+
applyQueuedInput: async item => {
|
|
2720
|
+
RunExecutionSnapshot.resolveForExecution(
|
|
2721
|
+
RunExecutionSnapshot.fromQueuePayload(item.payload),
|
|
2722
|
+
{ catalog: providerSettingsCoordinator.getCatalog() }
|
|
2723
|
+
);
|
|
2636
2724
|
},
|
|
2637
2725
|
run: item => runTask({ queuedInput: item }),
|
|
2638
2726
|
sendSteer: params => sendBackgroundNative({ method: 'codex.steer', params }),
|
|
@@ -2643,13 +2731,33 @@
|
|
|
2643
2731
|
removeGuidance: runGuidanceController.remove,
|
|
2644
2732
|
toast: (message, status) => showPluginToast(message, { status }),
|
|
2645
2733
|
tr,
|
|
2646
|
-
save:
|
|
2647
|
-
saveSoon:
|
|
2734
|
+
save: persistenceOptions => saveState(persistenceOptions),
|
|
2735
|
+
saveSoon: persistenceOptions => saveStateSoon(120, persistenceOptions),
|
|
2648
2736
|
randomUUID: () => crypto.randomUUID()
|
|
2649
2737
|
});
|
|
2650
2738
|
runQueueScheduler = runInputCoordinator.scheduler;
|
|
2651
2739
|
}
|
|
2652
2740
|
|
|
2741
|
+
function captureCurrentExecutionSnapshot(submitted = {}) {
|
|
2742
|
+
const requestedProviderId = submitted.providerId || state?.providerId || 'builtin';
|
|
2743
|
+
const selection = providerSettingsCoordinator.getRunSelection(requestedProviderId);
|
|
2744
|
+
const providerId = selection?.providerId || requestedProviderId;
|
|
2745
|
+
return RunExecutionSnapshot.capture({
|
|
2746
|
+
mode: submitted.mode ?? state?.mode,
|
|
2747
|
+
providerId,
|
|
2748
|
+
providerRevision: selection?.providerRevision,
|
|
2749
|
+
model: submitted.model ?? state?.model,
|
|
2750
|
+
reasoningEffort: submitted.reasoningEffort ?? state?.reasoningEffort,
|
|
2751
|
+
speedTier: submitted.speedTier ?? state?.speedTier,
|
|
2752
|
+
autoRecompile: submitted.autoRecompile ?? state?.autoRecompile !== false,
|
|
2753
|
+
requireReviewing: submitted.requireReviewing ?? state?.requireReviewing === true,
|
|
2754
|
+
focusFiles: submitted.focusFiles ?? getActiveFocusFiles()
|
|
2755
|
+
}, {
|
|
2756
|
+
source: 'submitted',
|
|
2757
|
+
requireProviderRevision: providerId !== 'builtin'
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2653
2761
|
function queueComposerInput() {
|
|
2654
2762
|
return runInputCoordinator?.queueComposerInput();
|
|
2655
2763
|
}
|
|
@@ -2716,11 +2824,7 @@
|
|
|
2716
2824
|
}
|
|
2717
2825
|
|
|
2718
2826
|
function cancelActivePageBridgeRequests() {
|
|
2719
|
-
|
|
2720
|
-
activePageBridgeCancellationHandlers.clear();
|
|
2721
|
-
for (const cancel of handlers) {
|
|
2722
|
-
cancel();
|
|
2723
|
-
}
|
|
2827
|
+
pageBridgeClient.cancelActiveRequests();
|
|
2724
2828
|
}
|
|
2725
2829
|
|
|
2726
2830
|
// Recovery path for the case where a previous Codex run leaked the
|
|
@@ -2884,9 +2988,11 @@
|
|
|
2884
2988
|
skillInvocation,
|
|
2885
2989
|
submittedMode
|
|
2886
2990
|
} = {}) {
|
|
2991
|
+
const executionSnapshot = currentRunView?.executionSnapshot || captureCurrentExecutionSnapshot();
|
|
2992
|
+
const submittedState = RunExecutionSnapshot.applyToState(state, executionSnapshot);
|
|
2887
2993
|
const params = runController.buildCodexRunParams({
|
|
2888
2994
|
currentProjectId: getCurrentProjectId(),
|
|
2889
|
-
state,
|
|
2995
|
+
state: submittedState,
|
|
2890
2996
|
task,
|
|
2891
2997
|
project,
|
|
2892
2998
|
useExistingMirror,
|
|
@@ -2913,17 +3019,17 @@
|
|
|
2913
3019
|
...params,
|
|
2914
3020
|
clientRunId: currentRunView?.recordId || '',
|
|
2915
3021
|
clientSessionId: currentRunView?.sessionId || '',
|
|
2916
|
-
providerSelection:
|
|
3022
|
+
providerSelection: RunExecutionSnapshot.toProviderSelection(executionSnapshot)
|
|
2917
3023
|
};
|
|
2918
3024
|
}
|
|
2919
3025
|
|
|
2920
3026
|
async function sendTrackedCodexRun(params) {
|
|
2921
|
-
const compatibilityGate = await
|
|
3027
|
+
const compatibilityGate = await nativeCompatibilityController.ensureForMethod('codex.run');
|
|
2922
3028
|
if (!compatibilityGate.ok) {
|
|
2923
3029
|
return compatibilityGate.response;
|
|
2924
3030
|
}
|
|
2925
|
-
|
|
2926
|
-
const tracked = nativeChannel.sendNativeTracked(
|
|
3031
|
+
throwIfRunCancellationRequested();
|
|
3032
|
+
const tracked = nativeChannel.sendNativeTracked(nativeCompatibilityController.attachEvidence({
|
|
2927
3033
|
method: 'codex.run',
|
|
2928
3034
|
params
|
|
2929
3035
|
}, compatibilityGate.compatibility));
|
|
@@ -3003,19 +3109,23 @@
|
|
|
3003
3109
|
}
|
|
3004
3110
|
|
|
3005
3111
|
async function createAuditDraftForRun(input = {}) {
|
|
3006
|
-
const StorageDb =
|
|
3112
|
+
const StorageDb = Modules.StorageDb;
|
|
3007
3113
|
if (!StorageDb || !AuditRecords) {
|
|
3008
3114
|
return null;
|
|
3009
3115
|
}
|
|
3116
|
+
const executionSnapshot = input.executionSnapshot
|
|
3117
|
+
|| currentRunView?.executionSnapshot
|
|
3118
|
+
|| captureCurrentExecutionSnapshot();
|
|
3010
3119
|
const draft = AuditRecords.buildAuditDraftRecord({
|
|
3011
3120
|
projectId: getCurrentProjectId(),
|
|
3012
3121
|
sessionId: input.sessionId || '',
|
|
3013
3122
|
turnId: currentRunView?.recordId || '',
|
|
3014
|
-
mode: input.mode ||
|
|
3015
|
-
...providerSettingsCoordinator.getProviderSnapshot(
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3123
|
+
mode: input.mode || executionSnapshot.mode,
|
|
3124
|
+
...providerSettingsCoordinator.getProviderSnapshot(executionSnapshot.providerId),
|
|
3125
|
+
providerRevision: executionSnapshot.providerRevision,
|
|
3126
|
+
model: executionSnapshot.model,
|
|
3127
|
+
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
3128
|
+
speedTier: executionSnapshot.speedTier,
|
|
3019
3129
|
task: input.task || '',
|
|
3020
3130
|
focusFiles: input.focusFiles || [],
|
|
3021
3131
|
selectedSkillIds: input.selectedSkillIds || []
|
|
@@ -3037,7 +3147,7 @@
|
|
|
3037
3147
|
}
|
|
3038
3148
|
|
|
3039
3149
|
async function finalizeAuditDraftPartial(draft, updates = {}) {
|
|
3040
|
-
const StorageDb =
|
|
3150
|
+
const StorageDb = Modules.StorageDb;
|
|
3041
3151
|
if (!StorageDb || !draft) {
|
|
3042
3152
|
return draft;
|
|
3043
3153
|
}
|
|
@@ -3048,7 +3158,7 @@
|
|
|
3048
3158
|
}
|
|
3049
3159
|
|
|
3050
3160
|
async function finalizeAuditRecord(draft, updates = {}) {
|
|
3051
|
-
const StorageDb =
|
|
3161
|
+
const StorageDb = Modules.StorageDb;
|
|
3052
3162
|
if (!StorageDb || !AuditRecords || !draft) {
|
|
3053
3163
|
return null;
|
|
3054
3164
|
}
|
|
@@ -3743,10 +3853,6 @@
|
|
|
3743
3853
|
return diffReviewPanel.createDiffReviewElement(syncChanges, options);
|
|
3744
3854
|
}
|
|
3745
3855
|
|
|
3746
|
-
function renderDiffReview(syncChanges) {
|
|
3747
|
-
return diffReviewPanel.renderDiffReview(syncChanges);
|
|
3748
|
-
}
|
|
3749
|
-
|
|
3750
3856
|
function renderReadOnlyDiffReview(syncChanges, title = tr('diffWrittenChangesTitle')) {
|
|
3751
3857
|
return diffReviewPanel.renderReadOnlyDiffReview(syncChanges, title);
|
|
3752
3858
|
}
|
|
@@ -3843,8 +3949,8 @@
|
|
|
3843
3949
|
}
|
|
3844
3950
|
|
|
3845
3951
|
function normalizeSafeProjectPath(value) {
|
|
3846
|
-
if (
|
|
3847
|
-
return
|
|
3952
|
+
if (Modules.ProjectFiles?.normalizeSafeProjectPath) {
|
|
3953
|
+
return Modules.ProjectFiles.normalizeSafeProjectPath(value);
|
|
3848
3954
|
}
|
|
3849
3955
|
return String(value || '')
|
|
3850
3956
|
.replace(/\s+/g, ' ')
|
|
@@ -4123,6 +4229,7 @@
|
|
|
4123
4229
|
// `runProjectId` is that it survives navigation; `activeProjectId` does not.
|
|
4124
4230
|
let activeProjectId = null;
|
|
4125
4231
|
let lastSpaPathname = '';
|
|
4232
|
+
let spaRouteGeneration = 0;
|
|
4126
4233
|
|
|
4127
4234
|
function cancelPendingWritebacks(prevProjectId) {
|
|
4128
4235
|
// The codebase does not maintain a separate pending-writeback queue;
|
|
@@ -4172,6 +4279,17 @@
|
|
|
4172
4279
|
});
|
|
4173
4280
|
}
|
|
4174
4281
|
|
|
4282
|
+
function getScopedPersistenceCoordinator() {
|
|
4283
|
+
if (scopedPersistenceCoordinator) return scopedPersistenceCoordinator;
|
|
4284
|
+
scopedPersistenceCoordinator = Modules.ScopedPersistenceCoordinator?.createController({
|
|
4285
|
+
Transaction: Modules.ScopedPersistenceTransaction,
|
|
4286
|
+
chromeApi: chrome,
|
|
4287
|
+
sessionStorage: window.sessionStorage,
|
|
4288
|
+
getAccountScopeId: () => window.codexOverleafDeriveAccountScopeId?.() || ''
|
|
4289
|
+
});
|
|
4290
|
+
return scopedPersistenceCoordinator;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4175
4293
|
async function reloadProjectRunHistory(newProjectId) {
|
|
4176
4294
|
// Rebind the storage key to the new project and reload the panel state
|
|
4177
4295
|
// from chrome.storage.local so the per-project session list reflects
|
|
@@ -4265,21 +4383,34 @@
|
|
|
4265
4383
|
} catch (_error) { /* swallow */ }
|
|
4266
4384
|
}
|
|
4267
4385
|
|
|
4268
|
-
function onSpaRouteChange() {
|
|
4269
|
-
const
|
|
4386
|
+
async function onSpaRouteChange() {
|
|
4387
|
+
const generation = ++spaRouteGeneration;
|
|
4388
|
+
const url = new URL(window.location.href);
|
|
4389
|
+
const pathname = url.pathname;
|
|
4390
|
+
const previousProjectId = activeProjectId;
|
|
4391
|
+
const previousAccountScopeId = cachedAccountScopeId;
|
|
4392
|
+
const nextProjectId = isProjectEditorRoute(url)
|
|
4393
|
+
? url.pathname.match(/^\/project\/([^/?#]+)/)[1]
|
|
4394
|
+
: null;
|
|
4395
|
+
if (nextProjectId !== activeProjectId) {
|
|
4396
|
+
leaveActiveProject(nextProjectId);
|
|
4397
|
+
}
|
|
4270
4398
|
// Recompute the account scope on every route change (spec §5.2 per-
|
|
4271
4399
|
// project derivation: account menu is global chrome and should be
|
|
4272
4400
|
// readable on per-project URLs too).
|
|
4273
|
-
refreshAccountScopeId()
|
|
4401
|
+
await refreshAccountScopeId();
|
|
4402
|
+
if (generation !== spaRouteGeneration || pathname !== window.location.pathname) {
|
|
4403
|
+
return;
|
|
4404
|
+
}
|
|
4274
4405
|
if (isProjectEditorRoute(url)) {
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4406
|
+
if (
|
|
4407
|
+
nextProjectId !== previousProjectId
|
|
4408
|
+
|| cachedAccountScopeId !== previousAccountScopeId
|
|
4409
|
+
) {
|
|
4410
|
+
enterProject(nextProjectId);
|
|
4279
4411
|
}
|
|
4280
4412
|
renderPerProjectVariant();
|
|
4281
4413
|
} else {
|
|
4282
|
-
leaveActiveProject(null);
|
|
4283
4414
|
// Variant render is async (it awaits the IndexedDB query); the hook
|
|
4284
4415
|
// does not await — subsequent renders pick up the rebound state.
|
|
4285
4416
|
renderRecentProjectsVariant().catch(() => { /* swallow */ });
|
|
@@ -4303,11 +4434,9 @@
|
|
|
4303
4434
|
return;
|
|
4304
4435
|
}
|
|
4305
4436
|
lastSpaPathname = next;
|
|
4306
|
-
|
|
4307
|
-
onSpaRouteChange();
|
|
4308
|
-
} catch (_error) {
|
|
4437
|
+
onSpaRouteChange().catch(() => {
|
|
4309
4438
|
// Route-hook failures must never crash the page.
|
|
4310
|
-
}
|
|
4439
|
+
});
|
|
4311
4440
|
};
|
|
4312
4441
|
const wrap = name => {
|
|
4313
4442
|
const original = window.history[name];
|
|
@@ -4327,103 +4456,16 @@
|
|
|
4327
4456
|
lastSpaPathname = window.location.pathname;
|
|
4328
4457
|
}
|
|
4329
4458
|
|
|
4330
|
-
// -----------------------------------------------------------------------
|
|
4331
|
-
// Post-navigation run settlement (spec §5.7.1 + plan Step 6).
|
|
4332
|
-
// -----------------------------------------------------------------------
|
|
4333
|
-
//
|
|
4334
|
-
// When a Codex run completes after the user navigated away from
|
|
4335
|
-
// run.runProjectId, classify the outcome and persist the run on the
|
|
4336
|
-
// ORIGINAL project's record (NOT activeProjectId — that's the whole
|
|
4337
|
-
// point of the T2 immutability contract).
|
|
4338
4459
|
function settleRunAfterNavigation(run, runResult) {
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
}
|
|
4344
|
-
// Welcome-panel + write-guard:
|
|
4345
|
-
// accept the full `syncOutcome` (not just a pre-flattened skipped list).
|
|
4346
|
-
// The previous shape — only `{ skipped: [...] }` from
|
|
4347
|
-
// `collectRunResultSkipped` — silently classified runs as
|
|
4348
|
-
// `background_completed` when (a) the skipped entries' codes were
|
|
4349
|
-
// outside a small allow-list (e.g. delete_confirmation_rejected,
|
|
4350
|
-
// governance_blocked, binary_confirmation_rejected) or (b) the
|
|
4351
|
-
// syncOutcome was an early-return branch with
|
|
4352
|
-
// `hasSkippedOperations === true` but no `applied` (which flattens to
|
|
4353
|
-
// an empty skipped list).
|
|
4354
|
-
//
|
|
4355
|
-
// Spec §5.7.1 classification:
|
|
4356
|
-
// 1. Any skipped entry with failure.code IN
|
|
4357
|
-
// { 'aborted_project_changed', 'editor_project_id_unavailable' }
|
|
4358
|
-
// → 'abandoned_after_navigation'.
|
|
4359
|
-
// 2. Else if syncOutcome.hasSkippedOperations === true OR any skipped
|
|
4360
|
-
// entry exists (code-bearing or not)
|
|
4361
|
-
// → 'needs_review_after_navigation'.
|
|
4362
|
-
// (Spec defense-in-depth: when in doubt, claim needs_review rather
|
|
4363
|
-
// than background_completed.)
|
|
4364
|
-
// 3. Else if at least one applied write AND no skipped/needs_review/
|
|
4365
|
-
// failed entries AND post-write verification was clean
|
|
4366
|
-
// → 'background_completed'.
|
|
4367
|
-
// 4. Else (no skipped, no applied) — true no-op run (e.g. Ask-mode
|
|
4368
|
-
// where there was nothing to write) → 'background_completed'.
|
|
4369
|
-
const skipped = collectRunResultSkipped(runResult);
|
|
4370
|
-
const wasGuardAborted = skipped.some(entry => {
|
|
4371
|
-
const code = entry && entry.result && entry.result.code;
|
|
4372
|
-
return code === 'aborted_project_changed' || code === 'editor_project_id_unavailable';
|
|
4460
|
+
return WritebackSettlement.settlePostNavigation({
|
|
4461
|
+
run,
|
|
4462
|
+
runResult,
|
|
4463
|
+
navigation: { occurred: activeProjectId !== run.runProjectId }
|
|
4373
4464
|
});
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
}
|
|
4377
|
-
const hasSkippedOperations = runResult && runResult.hasSkippedOperations === true;
|
|
4378
|
-
if (hasSkippedOperations || skipped.length > 0) {
|
|
4379
|
-
// Rule 2: ANY skipped entry — code-bearing or not — and/or the
|
|
4380
|
-
// outcome flag — yields needs_review_after_navigation. This is the
|
|
4381
|
-
// defense-in-depth net for skip codes outside the recognized list
|
|
4382
|
-
// (delete_confirmation_rejected, governance_blocked,
|
|
4383
|
-
// binary_confirmation_rejected, etc.) and for early-return branches
|
|
4384
|
-
// where `applied` is absent but `hasSkippedOperations` is true.
|
|
4385
|
-
return 'needs_review_after_navigation';
|
|
4386
|
-
}
|
|
4387
|
-
// Rule 3 / 4: no skipped, no failed. Either at least one clean applied
|
|
4388
|
-
// write (post-write verification implicitly clean because rule 2
|
|
4389
|
-
// already caught write_observed_mismatch / accept_not_verified /
|
|
4390
|
-
// undo_not_verified / tracked_changes_remain via their skipped-entry
|
|
4391
|
-
// form), OR a true no-op run with nothing to write. Both classify as
|
|
4392
|
-
// background_completed per spec §5.7.1 rule 4.
|
|
4393
|
-
return 'background_completed';
|
|
4394
|
-
}
|
|
4395
|
-
|
|
4396
|
-
// Flatten the `applied.skipped` arrays from a syncOutcome into the
|
|
4397
|
-
// shape `settleRunAfterNavigation` expects: `{ skipped: [{ result: { code } }] }`.
|
|
4398
|
-
// Accepts both shapes:
|
|
4399
|
-
// 1. The full syncOutcome from `applySyncChangesToOverleaf` — reads
|
|
4400
|
-
// `syncOutcome.applied.skipped`.
|
|
4401
|
-
// 2. A pre-flattened shape `{ skipped: [...] }` — used by tests and by
|
|
4402
|
-
// historical call sites.
|
|
4403
|
-
// Spec §5.7.1: settleRunAfterNavigation now consumes the full
|
|
4404
|
-
// syncOutcome directly, but this helper remains useful for the
|
|
4405
|
-
// applied-array path AND keeps the legacy test surface intact.
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4406
4467
|
function collectRunResultSkipped(syncOutcome) {
|
|
4407
|
-
|
|
4408
|
-
return [];
|
|
4409
|
-
}
|
|
4410
|
-
const skipped = [];
|
|
4411
|
-
const applied = syncOutcome.applied;
|
|
4412
|
-
if (applied && Array.isArray(applied.skipped)) {
|
|
4413
|
-
for (const entry of applied.skipped) {
|
|
4414
|
-
if (entry) {
|
|
4415
|
-
skipped.push(entry);
|
|
4416
|
-
}
|
|
4417
|
-
}
|
|
4418
|
-
}
|
|
4419
|
-
if (Array.isArray(syncOutcome.skipped)) {
|
|
4420
|
-
for (const entry of syncOutcome.skipped) {
|
|
4421
|
-
if (entry) {
|
|
4422
|
-
skipped.push(entry);
|
|
4423
|
-
}
|
|
4424
|
-
}
|
|
4425
|
-
}
|
|
4426
|
-
return skipped;
|
|
4468
|
+
return WritebackSettlement.collectRunResultSkipped(syncOutcome);
|
|
4427
4469
|
}
|
|
4428
4470
|
|
|
4429
4471
|
// Persist the post-navigation status to the run's original
|
|
@@ -4438,50 +4480,73 @@
|
|
|
4438
4480
|
// `reloadProjectRunHistory`), so the in-memory mutation would not
|
|
4439
4481
|
// reach the right record. Go directly through StorageDb to update
|
|
4440
4482
|
// the original project's session record by id.
|
|
4441
|
-
async function persistPostNavigationRunStatus(run, postNavigationStatus) {
|
|
4483
|
+
async function persistPostNavigationRunStatus(run, postNavigationStatus, settlementResult) {
|
|
4442
4484
|
if (!run || !postNavigationStatus) {
|
|
4443
|
-
return;
|
|
4485
|
+
return { ok: false, reason: 'invalid_post_navigation_settlement' };
|
|
4444
4486
|
}
|
|
4445
4487
|
const postNavigationStatusText = getPostNavigationRunStatusText(postNavigationStatus);
|
|
4446
|
-
run.
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4488
|
+
Object.assign(run, WritebackSettlement.applySettlementTransition(
|
|
4489
|
+
run,
|
|
4490
|
+
WritebackSettlement.transitionPostNavigationStatus({
|
|
4491
|
+
status: postNavigationStatus,
|
|
4492
|
+
statusText: postNavigationStatusText,
|
|
4493
|
+
finishedAt: run.finishedAt || new Date().toISOString()
|
|
4494
|
+
})
|
|
4495
|
+
));
|
|
4496
|
+
if (
|
|
4497
|
+
currentRunView
|
|
4498
|
+
&& currentRunView.runProjectId === activeProjectId
|
|
4499
|
+
&& currentRunView.runAccountScopeId === cachedAccountScopeId
|
|
4500
|
+
) {
|
|
4450
4501
|
// Same project still active — the run record is reachable through the
|
|
4451
4502
|
// normal state and the regular save path will persist it.
|
|
4452
4503
|
saveStateSoon();
|
|
4453
|
-
return;
|
|
4504
|
+
return { ok: true, lane: 'active_project' };
|
|
4454
4505
|
}
|
|
4455
4506
|
// Different project. Update the original session's run record directly.
|
|
4456
4507
|
try {
|
|
4457
|
-
const StorageDb = window.CodexOverleafStorageDb;
|
|
4458
|
-
if (!StorageDb) {
|
|
4459
|
-
return;
|
|
4460
|
-
}
|
|
4461
4508
|
const sessionId = currentRunView ? currentRunView.sessionId : '';
|
|
4462
4509
|
if (!sessionId) {
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
return;
|
|
4510
|
+
throw Object.assign(new Error('Post-navigation settlement has no original session identity.'), {
|
|
4511
|
+
code: 'post_navigation_settlement_persistence_failed',
|
|
4512
|
+
reason: 'session_scope_unavailable'
|
|
4513
|
+
});
|
|
4468
4514
|
}
|
|
4469
|
-
const
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4515
|
+
const persistence = Modules.PostNavigationSettlementPersistence;
|
|
4516
|
+
if (!persistence || typeof persistence.persistRequired !== 'function') {
|
|
4517
|
+
throw Object.assign(new Error('Post-navigation settlement persistence is unavailable.'), {
|
|
4518
|
+
code: 'post_navigation_settlement_persistence_failed',
|
|
4519
|
+
reason: 'persistence_module_unavailable'
|
|
4520
|
+
});
|
|
4473
4521
|
}
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4522
|
+
return await persistence.persistRequired({
|
|
4523
|
+
StorageDb: Modules.StorageDb,
|
|
4524
|
+
PersistenceCoordinator: getScopedPersistenceCoordinator(),
|
|
4525
|
+
WritebackSettlement,
|
|
4526
|
+
projectId: currentRunView?.runProjectId || '',
|
|
4527
|
+
accountScopeId: currentRunView?.runAccountScopeId || '',
|
|
4528
|
+
sessionId,
|
|
4529
|
+
runId: run.id,
|
|
4530
|
+
status: postNavigationStatus,
|
|
4531
|
+
statusText: postNavigationStatusText,
|
|
4532
|
+
finishedAt: run.finishedAt,
|
|
4533
|
+
settlementResult
|
|
4534
|
+
});
|
|
4535
|
+
} catch (error) {
|
|
4536
|
+
const reason = String(error?.reason || error?.code || 'unknown_failure');
|
|
4537
|
+
run.postNavigationPersistenceFailure = {
|
|
4538
|
+
code: 'post_navigation_settlement_persistence_failed',
|
|
4539
|
+
reason,
|
|
4540
|
+
at: new Date().toISOString()
|
|
4541
|
+
};
|
|
4542
|
+
appendPlainLog(tx(
|
|
4543
|
+
`Could not persist the completed run to its original project (${reason}). Reopen that project to recover the interrupted record.`,
|
|
4544
|
+
`无法将已完成运行保存到原项目(${reason})。请重新打开该项目以恢复中断记录。`
|
|
4545
|
+
));
|
|
4546
|
+
return {
|
|
4547
|
+
ok: false,
|
|
4548
|
+
reason
|
|
4549
|
+
};
|
|
4485
4550
|
}
|
|
4486
4551
|
}
|
|
4487
4552
|
|
|
@@ -4510,7 +4575,7 @@
|
|
|
4510
4575
|
getCachedAccountScopeId: () => cachedAccountScopeId,
|
|
4511
4576
|
// T5 surfaces: variant renderer + helpers. Tests drive
|
|
4512
4577
|
// `renderRecentProjectsVariant` against a stubbed `panel` and
|
|
4513
|
-
// `
|
|
4578
|
+
// `Modules.StorageDb` to assert layout / row behavior.
|
|
4514
4579
|
renderRecentProjectsVariant,
|
|
4515
4580
|
renderRecentProjectRow,
|
|
4516
4581
|
renderStatusBadge,
|
|
@@ -4550,80 +4615,11 @@
|
|
|
4550
4615
|
mode,
|
|
4551
4616
|
operations: [],
|
|
4552
4617
|
applyResults: [],
|
|
4553
|
-
nextStep: tx('Continue the conversation, or switch to
|
|
4618
|
+
nextStep: tx('Continue the conversation, or switch to Auto to let Codex edit files.', '可以继续追问,或切换到自动写入后让 Codex 修改文件。')
|
|
4554
4619
|
});
|
|
4555
4620
|
return { status: tr('modeAsk'), summaryLine };
|
|
4556
4621
|
}
|
|
4557
4622
|
|
|
4558
|
-
if (result.status === 'requires_task_confirmation') {
|
|
4559
|
-
appendPlannedChangeSummary(result.summary, tx('Preparing changes', '准备修改'));
|
|
4560
|
-
const approved = await showPluginConfirm({
|
|
4561
|
-
title: tx('Apply these changes?', '应用这些修改?'),
|
|
4562
|
-
message: formatSummary(tx('Change Summary', '修改摘要'), result.summary),
|
|
4563
|
-
confirmLabel: tx('Apply changes', '应用修改'),
|
|
4564
|
-
cancelLabel: tr('confirmDefaultCancel')
|
|
4565
|
-
});
|
|
4566
|
-
if (!approved) {
|
|
4567
|
-
appendLog(tx('Cancelled: Codex did not write any files.', '已取消:Codex 没有写入任何文件。'));
|
|
4568
|
-
const summaryLine = appendChangeSummary({ notes, summary: result.summary, status: 'rejected' });
|
|
4569
|
-
appendCompletionReport({
|
|
4570
|
-
conclusion: tx('You cancelled this change. Codex did not write files.', '你取消了这轮修改,Codex 没有写入文件。'),
|
|
4571
|
-
status: 'rejected',
|
|
4572
|
-
notes,
|
|
4573
|
-
summary: result.summary,
|
|
4574
|
-
userReport: result.userReport,
|
|
4575
|
-
mode,
|
|
4576
|
-
operations: [],
|
|
4577
|
-
applyResults: [],
|
|
4578
|
-
nextStep: tx('Adjust the task and run again, or switch to Ask first so Codex can explain the plan.', '可以调整任务描述后重新运行,或切到只问不改先让 Codex 解释方案。')
|
|
4579
|
-
});
|
|
4580
|
-
return { status: 'rejected', summaryLine };
|
|
4581
|
-
}
|
|
4582
|
-
const confirmed = result.planId
|
|
4583
|
-
? await sendNative({ method: 'task.confirm', params: { planId: result.planId } })
|
|
4584
|
-
: { ok: true, result: { operations: result.operations || [] } };
|
|
4585
|
-
if (!confirmed.ok) {
|
|
4586
|
-
appendLog(`Confirm failed: ${confirmed.error.message}`);
|
|
4587
|
-
const summaryLine = appendChangeSummary({ notes, operations: [], status: 'confirm failed' });
|
|
4588
|
-
appendCompletionReport({
|
|
4589
|
-
conclusion: tx('No writable changes were returned after confirmation.', '确认后没有拿到可写入的修改。'),
|
|
4590
|
-
status: 'confirm failed',
|
|
4591
|
-
notes,
|
|
4592
|
-
userReport: result.userReport,
|
|
4593
|
-
mode,
|
|
4594
|
-
operations: [],
|
|
4595
|
-
applyResults: [],
|
|
4596
|
-
nextStep: confirmed.error.message
|
|
4597
|
-
});
|
|
4598
|
-
return { status: 'confirm failed', summaryLine };
|
|
4599
|
-
}
|
|
4600
|
-
const operations = confirmed.result.operations || [];
|
|
4601
|
-
appendOperationsPreview(operations, tx('Confirmed; preparing to write', '用户已确认,准备写入'));
|
|
4602
|
-
const applied = await applyTaskOperations(project, operations, { allowHighRisk: true });
|
|
4603
|
-
appendApplyResult(applied);
|
|
4604
|
-
recordUndoFromApply(project, applied);
|
|
4605
|
-
const summaryLine = appendChangeSummary({
|
|
4606
|
-
notes,
|
|
4607
|
-
operations,
|
|
4608
|
-
applyResults: [applied],
|
|
4609
|
-
status: 'confirmed and applied'
|
|
4610
|
-
});
|
|
4611
|
-
appendCompletionReport({
|
|
4612
|
-
conclusion: notes || tx('Changes were written after your confirmation.', '已按你的确认写入修改。'),
|
|
4613
|
-
status: 'confirmed and applied',
|
|
4614
|
-
notes,
|
|
4615
|
-
userReport: result.userReport,
|
|
4616
|
-
mode,
|
|
4617
|
-
operations,
|
|
4618
|
-
applyResults: [applied]
|
|
4619
|
-
});
|
|
4620
|
-
return {
|
|
4621
|
-
status: 'confirmed and applied',
|
|
4622
|
-
summaryLine,
|
|
4623
|
-
hasSkippedOperations: hasSkippedApplyOperations([applied])
|
|
4624
|
-
};
|
|
4625
|
-
}
|
|
4626
|
-
|
|
4627
4623
|
if (result.status === 'delete_plan_required') {
|
|
4628
4624
|
const operations = result.operations || [];
|
|
4629
4625
|
const applyResults = [];
|
|
@@ -4696,7 +4692,7 @@
|
|
|
4696
4692
|
|
|
4697
4693
|
const operations = result.operations || [];
|
|
4698
4694
|
appendOperationsPreview(operations, tx('Preparing to write', '准备写入'));
|
|
4699
|
-
const applied = await applyTaskOperations(project, operations
|
|
4695
|
+
const applied = await applyTaskOperations(project, operations);
|
|
4700
4696
|
appendApplyResult(applied);
|
|
4701
4697
|
recordUndoFromApply(project, applied);
|
|
4702
4698
|
appendLog(mode === 'auto' ? tx('Auto write task completed.', '自动写入任务完成。') : tx('Task completed.', '任务完成。'));
|
|
@@ -5016,9 +5012,6 @@
|
|
|
5016
5012
|
if (mode === 'ask') {
|
|
5017
5013
|
return tr('modeAsk');
|
|
5018
5014
|
}
|
|
5019
|
-
if (mode === 'confirm') {
|
|
5020
|
-
return tr('modeConfirm');
|
|
5021
|
-
}
|
|
5022
5015
|
if (mode === 'auto') {
|
|
5023
5016
|
return tr('modeAuto');
|
|
5024
5017
|
}
|
|
@@ -5030,8 +5023,6 @@
|
|
|
5030
5023
|
const labels = {
|
|
5031
5024
|
completed: tx('Completed', '已完成'),
|
|
5032
5025
|
rejected: tx('Cancelled', '已取消'),
|
|
5033
|
-
'confirm failed': tx('Confirmation failed', '确认失败'),
|
|
5034
|
-
'confirmed and applied': tx('Suggested changes applied', '已应用建议修改'),
|
|
5035
5026
|
'applied with delete plan': tx('Applied with confirmed deletes', '已应用并删除确认项'),
|
|
5036
5027
|
'applied without deletes': tx('Applied without deletes', '已应用非删除修改'),
|
|
5037
5028
|
'只问不改': tr('modeAsk')
|
|
@@ -5225,7 +5216,7 @@
|
|
|
5225
5216
|
}
|
|
5226
5217
|
|
|
5227
5218
|
const mirrorStatus = await getMirrorFreshness();
|
|
5228
|
-
const mirrorHealth =
|
|
5219
|
+
const mirrorHealth = Modules.MirrorHealth.classifyMirrorHealth(mirrorStatus);
|
|
5229
5220
|
if (!mirrorStatus?.exists || !mirrorHealth.reusable) {
|
|
5230
5221
|
return { useExistingMirror: false, reason: 'mirror_not_fresh', mirrorStatus };
|
|
5231
5222
|
}
|
|
@@ -5254,7 +5245,7 @@
|
|
|
5254
5245
|
return candidates
|
|
5255
5246
|
.filter(file =>
|
|
5256
5247
|
typeof file.content === 'string' &&
|
|
5257
|
-
|
|
5248
|
+
Modules.ProjectFiles.isUsableProjectFileContent(file.content) &&
|
|
5258
5249
|
file.path &&
|
|
5259
5250
|
!seen.has(file.path) &&
|
|
5260
5251
|
seen.add(file.path)
|
|
@@ -5363,7 +5354,7 @@
|
|
|
5363
5354
|
blocking.push('Full project snapshot was not captured.');
|
|
5364
5355
|
}
|
|
5365
5356
|
|
|
5366
|
-
const unusable = textFiles.filter(file => !
|
|
5357
|
+
const unusable = textFiles.filter(file => !Modules.ProjectFiles.isUsableProjectFileContent(file.content));
|
|
5367
5358
|
if (unusable.length === textFiles.length) {
|
|
5368
5359
|
blocking.push('Captured file contents are empty or still loading.');
|
|
5369
5360
|
} else if (unusable.length) {
|
|
@@ -5401,545 +5392,84 @@
|
|
|
5401
5392
|
}
|
|
5402
5393
|
|
|
5403
5394
|
function sendNative(payload) {
|
|
5404
|
-
return (
|
|
5405
|
-
const compatibilityGate = await ensureNativeCompatibilityForMethod(payload?.method);
|
|
5406
|
-
if (!compatibilityGate.ok) {
|
|
5407
|
-
return compatibilityGate.response;
|
|
5408
|
-
}
|
|
5409
|
-
throwIfCancelledBeforeNativeDispatch(payload?.method);
|
|
5410
|
-
return nativeChannel.sendNative(attachNativeCompatibilityEvidence(payload, compatibilityGate.compatibility));
|
|
5411
|
-
})();
|
|
5395
|
+
return nativeCompatibilityController.sendNative(payload);
|
|
5412
5396
|
}
|
|
5413
5397
|
|
|
5414
5398
|
function sendBackgroundNative(payload) {
|
|
5415
|
-
return (
|
|
5416
|
-
const compatibilityGate = await ensureNativeCompatibilityForMethod(payload?.method);
|
|
5417
|
-
if (!compatibilityGate.ok) {
|
|
5418
|
-
return compatibilityGate.response;
|
|
5419
|
-
}
|
|
5420
|
-
throwIfCancelledBeforeNativeDispatch(payload?.method);
|
|
5421
|
-
return nativeChannel.sendBackgroundNative(attachNativeCompatibilityEvidence(payload, compatibilityGate.compatibility));
|
|
5422
|
-
})();
|
|
5423
|
-
}
|
|
5424
|
-
|
|
5425
|
-
function throwIfCancelledBeforeNativeDispatch(method) {
|
|
5426
|
-
if (NATIVE_COMPATIBILITY_GATED_METHODS.has(method)) {
|
|
5427
|
-
throwIfRunCancellationRequested();
|
|
5428
|
-
}
|
|
5429
|
-
}
|
|
5430
|
-
|
|
5431
|
-
async function ensureNativeCompatibilityForMethod(method) {
|
|
5432
|
-
const params = CodexOverleafCompatibility?.buildBridgePingParams
|
|
5433
|
-
? CodexOverleafCompatibility.buildBridgePingParams(getExtensionCompatibilityMetadata())
|
|
5434
|
-
: {};
|
|
5435
|
-
const response = await nativeChannel.sendBackgroundNative({
|
|
5436
|
-
method: 'bridge.ping',
|
|
5437
|
-
params
|
|
5438
|
-
});
|
|
5439
|
-
const compatibility = CodexOverleafCompatibility?.evaluateNativeCompatibility
|
|
5440
|
-
? CodexOverleafCompatibility.evaluateNativeCompatibility(response, getExtensionCompatibilityMetadata())
|
|
5441
|
-
: fallbackNativeCompatibility(response);
|
|
5442
|
-
const allowed = !NATIVE_COMPATIBILITY_GATED_METHODS.has(method) ||
|
|
5443
|
-
(CodexOverleafCompatibility?.isNativeMethodAllowed
|
|
5444
|
-
? CodexOverleafCompatibility.isNativeMethodAllowed(method, compatibility)
|
|
5445
|
-
: compatibility.status === 'ok');
|
|
5446
|
-
if (allowed) {
|
|
5447
|
-
return { ok: true, compatibility };
|
|
5448
|
-
}
|
|
5449
|
-
|
|
5450
|
-
const message = formatNativeCompatibilityBlockedMessage(method, compatibility);
|
|
5451
|
-
const error = {
|
|
5452
|
-
code: 'native_update_required',
|
|
5453
|
-
message,
|
|
5454
|
-
status: compatibility.status || 'unknown_native',
|
|
5455
|
-
classification: getNativeCompatibilityClassification(compatibility),
|
|
5456
|
-
installCommand: compatibility.installCommand || INSTALL_COMMAND,
|
|
5457
|
-
updateCommand: compatibility.updateCommand || compatibility.installCommand || INSTALL_COMMAND,
|
|
5458
|
-
currentNativeVersion: compatibility.currentNativeVersion || compatibility.nativeVersion || compatibility.native?.version || '',
|
|
5459
|
-
requiredVersion: compatibility.requiredVersion || CodexOverleafCompatibility?.BUILD_TARGET_VERSION || '',
|
|
5460
|
-
releaseUrl: compatibility.releaseUrl || ''
|
|
5461
|
-
};
|
|
5462
|
-
notifyNativeCompatibilityBlocked(error, compatibility);
|
|
5463
|
-
return {
|
|
5464
|
-
ok: false,
|
|
5465
|
-
compatibility,
|
|
5466
|
-
response: {
|
|
5467
|
-
ok: false,
|
|
5468
|
-
error
|
|
5469
|
-
}
|
|
5470
|
-
};
|
|
5471
|
-
}
|
|
5472
|
-
|
|
5473
|
-
function attachNativeCompatibilityEvidence(payload = {}, compatibility) {
|
|
5474
|
-
if (!compatibility || !NATIVE_COMPATIBILITY_GATED_METHODS.has(payload?.method)) {
|
|
5475
|
-
return payload;
|
|
5476
|
-
}
|
|
5477
|
-
return {
|
|
5478
|
-
...payload,
|
|
5479
|
-
params: {
|
|
5480
|
-
...(payload.params || {}),
|
|
5481
|
-
nativeCompatibility: compatibility
|
|
5482
|
-
}
|
|
5483
|
-
};
|
|
5484
|
-
}
|
|
5485
|
-
|
|
5486
|
-
function notifyNativeCompatibilityBlocked(error, compatibility = {}) {
|
|
5487
|
-
const message = error?.message || String(error || '');
|
|
5488
|
-
if (currentRunView) {
|
|
5489
|
-
appendRunEvent({
|
|
5490
|
-
title: message,
|
|
5491
|
-
status: 'failed'
|
|
5492
|
-
});
|
|
5493
|
-
showNativeUpdateGuidanceModal(compatibility);
|
|
5494
|
-
} else {
|
|
5495
|
-
showNativeUpdateGuidanceModal(compatibility);
|
|
5496
|
-
showPluginToast(message, { status: 'failed', sticky: true });
|
|
5497
|
-
}
|
|
5399
|
+
return nativeCompatibilityController.sendBackgroundNative(payload);
|
|
5498
5400
|
}
|
|
5499
5401
|
|
|
5500
5402
|
async function refreshNativeCompatibilityBadge() {
|
|
5501
|
-
|
|
5502
|
-
return;
|
|
5503
|
-
}
|
|
5504
|
-
try {
|
|
5505
|
-
const params = CodexOverleafCompatibility.buildBridgePingParams
|
|
5506
|
-
? CodexOverleafCompatibility.buildBridgePingParams(getExtensionCompatibilityMetadata())
|
|
5507
|
-
: {};
|
|
5508
|
-
const response = await sendBackgroundNative({ method: 'bridge.ping', params });
|
|
5509
|
-
const compatibility = CodexOverleafCompatibility.evaluateNativeCompatibility(response, getExtensionCompatibilityMetadata());
|
|
5510
|
-
const classification = getNativeCompatibilityClassification(compatibility);
|
|
5511
|
-
if (classification === 'compatible') {
|
|
5512
|
-
PanelRenderer.setBadge(panelRendererInstance.headerEl, { type: 'none' });
|
|
5513
|
-
setDiagnosticsHealth('ok');
|
|
5514
|
-
try {
|
|
5515
|
-
window.localStorage.setItem('codexOverleafNativeEverOk', 'true');
|
|
5516
|
-
} catch (_storageError) { /* private mode etc.; the wizard just stays eligible */ }
|
|
5517
|
-
return;
|
|
5518
|
-
}
|
|
5519
|
-
// update-available reads as attention; anything else (incompatible /
|
|
5520
|
-
// unsupported) reads as a hard problem on the diagnostics dot.
|
|
5521
|
-
setDiagnosticsHealth(classification === 'update-available' ? 'warn' : 'fail');
|
|
5522
|
-
PanelRenderer.setBadge(panelRendererInstance.headerEl, {
|
|
5523
|
-
type: 'update',
|
|
5524
|
-
tooltip: tx('Native host update available', 'Native host 可更新'),
|
|
5525
|
-
onClick: () => showNativeUpdateGuidanceModal(compatibility)
|
|
5526
|
-
});
|
|
5527
|
-
// A never-installed host does NOT throw: background resolves
|
|
5528
|
-
// {ok:false, native_connection_failed} and classification lands here.
|
|
5529
|
-
if (!compatibility?.native?.version) {
|
|
5530
|
-
maybePromptFirstRunSetup();
|
|
5531
|
-
}
|
|
5532
|
-
} catch (_error) {
|
|
5533
|
-
setDiagnosticsHealth('fail');
|
|
5534
|
-
// No ping response at all means the native host is missing or not
|
|
5535
|
-
// running — telling a never-installed user an "update" exists is
|
|
5536
|
-
// misleading; point them at setup instead.
|
|
5537
|
-
PanelRenderer.setBadge(panelRendererInstance.headerEl, {
|
|
5538
|
-
type: 'update',
|
|
5539
|
-
tooltip: tx('Native host is not responding — click for setup steps', 'Native host 未响应——点击查看安装步骤'),
|
|
5540
|
-
onClick: () => showNativeUpdateGuidanceModal(fallbackNativeCompatibility({ ok: false }))
|
|
5541
|
-
});
|
|
5542
|
-
maybePromptFirstRunSetup();
|
|
5543
|
-
}
|
|
5544
|
-
}
|
|
5545
|
-
|
|
5546
|
-
// First-run wizard (v1.7): the native host is the one hard prerequisite,
|
|
5547
|
-
// and pre-1.7 its absence was a 9px red dot. Auto-open the install guidance
|
|
5548
|
-
// ONCE per browser profile; the flag is set before showing so a dismissal
|
|
5549
|
-
// is respected forever (no nag loop).
|
|
5550
|
-
function maybePromptFirstRunSetup() {
|
|
5551
|
-
let storage = null;
|
|
5552
|
-
try {
|
|
5553
|
-
storage = window.localStorage;
|
|
5554
|
-
if (!storage || storage.getItem('codexOverleafSetupPromptShown') === 'true') {
|
|
5555
|
-
return;
|
|
5556
|
-
}
|
|
5557
|
-
// A profile that ever completed a successful ping is an installed user
|
|
5558
|
-
// having a transient outage, not a first run — never auto-modal them.
|
|
5559
|
-
if (storage.getItem('codexOverleafNativeEverOk') === 'true') {
|
|
5560
|
-
return;
|
|
5561
|
-
}
|
|
5562
|
-
storage.setItem('codexOverleafSetupPromptShown', 'true');
|
|
5563
|
-
} catch (_storageError) {
|
|
5564
|
-
return;
|
|
5565
|
-
}
|
|
5566
|
-
showNativeUpdateGuidanceModal(fallbackNativeCompatibility({ ok: false }));
|
|
5403
|
+
return nativeCompatibilityController.refreshBadge();
|
|
5567
5404
|
}
|
|
5568
5405
|
|
|
5569
5406
|
function showNativeUpdateGuidanceModal(compatibility = {}) {
|
|
5570
|
-
|
|
5571
|
-
ensurePanelOpen();
|
|
5572
|
-
}
|
|
5573
|
-
const existing = panel.querySelector('[data-native-update-guidance]');
|
|
5574
|
-
existing?.remove();
|
|
5575
|
-
|
|
5576
|
-
const native = compatibility.native || {};
|
|
5577
|
-
const extensionVersion = compatibility.extensionVersion || CodexOverleafCompatibility?.BUILD_TARGET_VERSION || '';
|
|
5578
|
-
const nativeVersion = compatibility.currentNativeVersion || compatibility.nativeVersion || native.version || tx('missing', '未安装');
|
|
5579
|
-
const command = compatibility.updateCommand || compatibility.installCommand || INSTALL_COMMAND;
|
|
5580
|
-
const releaseUrl = compatibility.releaseUrl || '';
|
|
5581
|
-
|
|
5582
|
-
const overlay = document.createElement('div');
|
|
5583
|
-
overlay.className = 'codex-plugin-confirm';
|
|
5584
|
-
overlay.dataset.nativeUpdateGuidance = 'true';
|
|
5585
|
-
overlay.setAttribute('role', 'dialog');
|
|
5586
|
-
overlay.setAttribute('aria-modal', 'true');
|
|
5587
|
-
const nativeMissing = !native.version;
|
|
5588
|
-
overlay.setAttribute('aria-label', nativeMissing
|
|
5589
|
-
? tx('Set up the native host', '安装 Native host')
|
|
5590
|
-
: tx('Native host update available', 'Native host 可更新'));
|
|
5591
|
-
|
|
5592
|
-
const card = document.createElement('section');
|
|
5593
|
-
card.className = 'codex-plugin-confirm-card';
|
|
5594
|
-
|
|
5595
|
-
const title = document.createElement('div');
|
|
5596
|
-
title.className = 'codex-plugin-confirm-title';
|
|
5597
|
-
title.textContent = nativeMissing
|
|
5598
|
-
? tx('One step left: install the native host', '还差一步:安装 Native host')
|
|
5599
|
-
: tx('Native host update available', 'Native host 可更新');
|
|
5600
|
-
|
|
5601
|
-
const body = document.createElement('div');
|
|
5602
|
-
body.className = 'codex-plugin-confirm-body';
|
|
5603
|
-
body.textContent = [
|
|
5604
|
-
nativeMissing
|
|
5605
|
-
? tx(`Extension v${extensionVersion} is ready; Codex still needs its local bridge to read and edit this project.`, `扩展 v${extensionVersion} 已就绪;Codex 还需要本地桥接程序才能读写这个项目。`)
|
|
5606
|
-
: tx(`Extension v${extensionVersion} / Native v${nativeVersion}`, `扩展 v${extensionVersion} / Native v${nativeVersion}`),
|
|
5607
|
-
tx('Run the platform-specific command below, reload the extension, then refresh Overleaf.', '运行下面的平台命令,重新加载扩展,然后刷新 Overleaf。')
|
|
5608
|
-
].join('\n\n');
|
|
5609
|
-
|
|
5610
|
-
const commandCode = document.createElement('code');
|
|
5611
|
-
commandCode.textContent = command;
|
|
5612
|
-
commandCode.style.display = 'block';
|
|
5613
|
-
commandCode.style.whiteSpace = 'pre-wrap';
|
|
5614
|
-
commandCode.style.marginTop = '0.75rem';
|
|
5615
|
-
body.append(commandCode);
|
|
5616
|
-
|
|
5617
|
-
if (releaseUrl) {
|
|
5618
|
-
const link = document.createElement('a');
|
|
5619
|
-
link.href = releaseUrl;
|
|
5620
|
-
link.target = '_blank';
|
|
5621
|
-
link.rel = 'noopener noreferrer';
|
|
5622
|
-
link.textContent = tx('Open GitHub Release', '打开 GitHub Release');
|
|
5623
|
-
link.style.display = 'block';
|
|
5624
|
-
link.style.marginTop = '0.75rem';
|
|
5625
|
-
body.append(link);
|
|
5626
|
-
}
|
|
5627
|
-
|
|
5628
|
-
const actions = document.createElement('div');
|
|
5629
|
-
actions.className = 'codex-plugin-confirm-actions';
|
|
5630
|
-
const copy = document.createElement('button');
|
|
5631
|
-
copy.type = 'button';
|
|
5632
|
-
copy.className = 'codex-plugin-confirm-confirm';
|
|
5633
|
-
copy.textContent = tx('Copy command', '复制命令');
|
|
5634
|
-
copy.addEventListener('click', async () => {
|
|
5635
|
-
await navigator.clipboard.writeText(command);
|
|
5636
|
-
copy.textContent = tx('Copied', '已复制');
|
|
5637
|
-
});
|
|
5638
|
-
const close = document.createElement('button');
|
|
5639
|
-
close.type = 'button';
|
|
5640
|
-
close.className = 'codex-plugin-confirm-cancel';
|
|
5641
|
-
close.textContent = tx('Close', '关闭');
|
|
5642
|
-
close.addEventListener('click', () => overlay.remove());
|
|
5643
|
-
actions.append(close, copy);
|
|
5644
|
-
|
|
5645
|
-
card.append(title, body, actions);
|
|
5646
|
-
overlay.append(card);
|
|
5647
|
-
overlay.addEventListener('click', event => {
|
|
5648
|
-
if (event.target === overlay) {
|
|
5649
|
-
overlay.remove();
|
|
5650
|
-
}
|
|
5651
|
-
});
|
|
5652
|
-
panel.append(overlay);
|
|
5653
|
-
copy.focus();
|
|
5407
|
+
return nativeCompatibilityController.showUpdateGuidance(compatibility);
|
|
5654
5408
|
}
|
|
5655
5409
|
|
|
5656
5410
|
function scheduleFirstUseOnboardingTip() {
|
|
5657
|
-
|
|
5658
|
-
maybeShowFirstUseOnboardingTip().catch(() => {});
|
|
5659
|
-
}, 250);
|
|
5660
|
-
}
|
|
5661
|
-
|
|
5662
|
-
async function maybeShowFirstUseOnboardingTip() {
|
|
5663
|
-
if (!panel || (state?.sessions || []).length > 0) {
|
|
5664
|
-
return;
|
|
5665
|
-
}
|
|
5666
|
-
const stored = await chrome.storage.local.get([ONBOARDING_TIP_STORAGE_KEY]);
|
|
5667
|
-
if (stored?.[ONBOARDING_TIP_STORAGE_KEY]) {
|
|
5668
|
-
return;
|
|
5669
|
-
}
|
|
5670
|
-
const modeSelect = panel.querySelector('[data-mode]');
|
|
5671
|
-
if (!modeSelect) {
|
|
5672
|
-
return;
|
|
5673
|
-
}
|
|
5674
|
-
const tip = document.createElement('div');
|
|
5675
|
-
tip.className = 'codex-onboarding-tip';
|
|
5676
|
-
tip.dataset.onboardingTip = 'true';
|
|
5677
|
-
tip.setAttribute('role', 'status');
|
|
5678
|
-
tip.textContent = tx(
|
|
5679
|
-
'Tip: Start with Ask mode to explore safely. Switch to Suggest to review edits before applying.',
|
|
5680
|
-
'提示:先用 Ask 模式安全探索;需要修改时切到 Suggest,应用前可先审阅。'
|
|
5681
|
-
);
|
|
5682
|
-
Object.assign(tip.style, {
|
|
5683
|
-
position: 'absolute',
|
|
5684
|
-
zIndex: '2147483647',
|
|
5685
|
-
maxWidth: '280px'
|
|
5686
|
-
});
|
|
5687
|
-
panel.append(tip);
|
|
5688
|
-
const panelRect = panel.getBoundingClientRect();
|
|
5689
|
-
const modeRect = modeSelect.getBoundingClientRect();
|
|
5690
|
-
tip.style.left = `${Math.max(12, modeRect.left - panelRect.left)}px`;
|
|
5691
|
-
tip.style.bottom = `${Math.max(72, panelRect.bottom - modeRect.top + 8)}px`;
|
|
5692
|
-
|
|
5693
|
-
const dismiss = () => {
|
|
5694
|
-
tip.remove();
|
|
5695
|
-
chrome.storage.local.set({ [ONBOARDING_TIP_STORAGE_KEY]: true }).catch?.(() => {});
|
|
5696
|
-
panel.removeEventListener('click', dismiss, true);
|
|
5697
|
-
panel.removeEventListener('keydown', dismiss, true);
|
|
5698
|
-
};
|
|
5699
|
-
panel.addEventListener('click', dismiss, true);
|
|
5700
|
-
panel.addEventListener('keydown', dismiss, true);
|
|
5701
|
-
setTimeout(dismiss, 8000);
|
|
5702
|
-
}
|
|
5703
|
-
|
|
5704
|
-
function formatNativeCompatibilityBlockedMessage(method, compatibility = {}) {
|
|
5705
|
-
const statusValue = compatibility.status || 'unknown_native';
|
|
5706
|
-
const params = { method: method || 'native request' };
|
|
5707
|
-
switch (statusValue) {
|
|
5708
|
-
case 'native_too_old':
|
|
5709
|
-
return tr('nativeCompatibilityBlockedNativeTooOld', params);
|
|
5710
|
-
case 'extension_too_old':
|
|
5711
|
-
return tr('nativeCompatibilityBlockedExtensionTooOld', params);
|
|
5712
|
-
case 'protocol_unsupported':
|
|
5713
|
-
return tr('nativeCompatibilityBlockedProtocol', params);
|
|
5714
|
-
case 'native_unhealthy':
|
|
5715
|
-
return tr('nativeCompatibilityBlockedUnhealthy', params);
|
|
5716
|
-
case 'native_missing':
|
|
5717
|
-
return tr('nativeCompatibilityBlockedMissing', params);
|
|
5718
|
-
default:
|
|
5719
|
-
return tr('nativeCompatibilityBlockedGeneric', params);
|
|
5720
|
-
}
|
|
5411
|
+
nativeCompatibilityController.scheduleFirstUseOnboardingTip();
|
|
5721
5412
|
}
|
|
5722
5413
|
|
|
5723
5414
|
async function callPageBridge(method, params) {
|
|
5724
|
-
|
|
5725
|
-
await pageBridgeReady;
|
|
5726
|
-
} catch (error) {
|
|
5727
|
-
return {
|
|
5728
|
-
ok: false,
|
|
5729
|
-
error: `Page bridge unavailable: ${error.message}`
|
|
5730
|
-
};
|
|
5731
|
-
}
|
|
5732
|
-
const timeoutMs = getPageBridgeTimeoutMs(method);
|
|
5733
|
-
return sendPageBridgeRequest(method, params, {
|
|
5734
|
-
timeoutMs
|
|
5735
|
-
});
|
|
5736
|
-
}
|
|
5737
|
-
|
|
5738
|
-
function sendPageBridgeRequest(method, params, options = {}) {
|
|
5739
|
-
const id = crypto.randomUUID();
|
|
5740
|
-
return new Promise((resolve, reject) => {
|
|
5741
|
-
const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 8000;
|
|
5742
|
-
const cancellable = CANCELLABLE_PAGE_BRIDGE_METHODS.has(method);
|
|
5743
|
-
let settled = false;
|
|
5744
|
-
let timeout = null;
|
|
5745
|
-
|
|
5746
|
-
function cleanup() {
|
|
5747
|
-
if (timeout !== null) {
|
|
5748
|
-
window.clearTimeout(timeout);
|
|
5749
|
-
}
|
|
5750
|
-
window.removeEventListener('message', onMessage);
|
|
5751
|
-
if (cancellable) {
|
|
5752
|
-
activePageBridgeCancellationHandlers.delete(id);
|
|
5753
|
-
}
|
|
5754
|
-
}
|
|
5755
|
-
|
|
5756
|
-
function resolveOnce(value) {
|
|
5757
|
-
if (settled) {
|
|
5758
|
-
return;
|
|
5759
|
-
}
|
|
5760
|
-
settled = true;
|
|
5761
|
-
cleanup();
|
|
5762
|
-
resolve(value);
|
|
5763
|
-
}
|
|
5764
|
-
|
|
5765
|
-
function rejectOnce(error) {
|
|
5766
|
-
if (settled) {
|
|
5767
|
-
return;
|
|
5768
|
-
}
|
|
5769
|
-
settled = true;
|
|
5770
|
-
cleanup();
|
|
5771
|
-
reject(error);
|
|
5772
|
-
}
|
|
5773
|
-
|
|
5774
|
-
function cancelRequest() {
|
|
5775
|
-
const error = new Error('Codex run was cancelled by the user');
|
|
5776
|
-
error.code = 'codex_cancelled';
|
|
5777
|
-
error.cancelled = true;
|
|
5778
|
-
rejectOnce(error);
|
|
5779
|
-
}
|
|
5780
|
-
|
|
5781
|
-
timeout = window.setTimeout(() => {
|
|
5782
|
-
resolveOnce({ ok: false, error: 'Page bridge timed out' });
|
|
5783
|
-
}, timeoutMs);
|
|
5784
|
-
|
|
5785
|
-
function onMessage(event) {
|
|
5786
|
-
if (event.source !== window
|
|
5787
|
-
|| event.origin !== window.location.origin
|
|
5788
|
-
|| event.data?.source !== 'codex-overleaf/page'
|
|
5789
|
-
|| event.data?.pageBridgeVersion !== CodexOverleafCompatibility?.BUILD_TARGET_VERSION
|
|
5790
|
-
|| event.data?.pageBridgeRevision !== PAGE_BRIDGE_SCRIPT_REVISION
|
|
5791
|
-
|| event.data.id !== id) {
|
|
5792
|
-
return;
|
|
5793
|
-
}
|
|
5794
|
-
resolveOnce(event.data.result);
|
|
5795
|
-
}
|
|
5796
|
-
|
|
5797
|
-
if (cancellable) {
|
|
5798
|
-
activePageBridgeCancellationHandlers.set(id, cancelRequest);
|
|
5799
|
-
if (runCancellationRequested) {
|
|
5800
|
-
window.queueMicrotask(cancelRequest);
|
|
5801
|
-
}
|
|
5802
|
-
}
|
|
5803
|
-
|
|
5804
|
-
window.addEventListener('message', onMessage);
|
|
5805
|
-
// This capability narrows accidental/spoofed page-bridge calls. It is not
|
|
5806
|
-
// a secret from malicious same-page Overleaf scripts because postMessage
|
|
5807
|
-
// traffic is page-visible.
|
|
5808
|
-
window.postMessage({
|
|
5809
|
-
source: 'codex-overleaf/content',
|
|
5810
|
-
id,
|
|
5811
|
-
method,
|
|
5812
|
-
params,
|
|
5813
|
-
capability: PAGE_BRIDGE_CAPABILITY
|
|
5814
|
-
}, window.location.origin);
|
|
5815
|
-
});
|
|
5415
|
+
return pageBridgeClient.call(method, params);
|
|
5816
5416
|
}
|
|
5817
5417
|
|
|
5818
5418
|
function getPageBridgeTimeoutMs(method) {
|
|
5819
|
-
|
|
5820
|
-
return SNAPSHOT_PAGE_BRIDGE_TIMEOUT_MS;
|
|
5821
|
-
}
|
|
5822
|
-
if (method === 'getProjectFileList') {
|
|
5823
|
-
return CONTEXT_FILE_LIST_PAGE_BRIDGE_TIMEOUT_MS;
|
|
5824
|
-
}
|
|
5825
|
-
if (method === 'triggerCompile' || method === 'getCompileLog') {
|
|
5826
|
-
return COMPILE_PAGE_BRIDGE_TIMEOUT_MS;
|
|
5827
|
-
}
|
|
5828
|
-
if (method === 'rejectTrackedChanges' || method === 'acceptTrackedChanges') {
|
|
5829
|
-
return 120000;
|
|
5830
|
-
}
|
|
5831
|
-
if (method === 'applyOperations') {
|
|
5832
|
-
// Each per-op step inside writebackRouter.applyOperationsCore can wait
|
|
5833
|
-
// up to 5s for openFileByPath (treeOperations.js:230, :259) plus up to
|
|
5834
|
-
// 5s for waitForActiveEditorText plus reviewing/save-state polling.
|
|
5835
|
-
// A multi-op write on a freshly-loaded Overleaf editor can run 15-30s
|
|
5836
|
-
// in the wild. The pre-fix default of 8000ms timed out mid-write and
|
|
5837
|
-
// left the page-side promise running uncontrolled — a 'zombie' write
|
|
5838
|
-
// could still land after the content-side reported failure. 30s is a
|
|
5839
|
-
// sane upper bound that covers the realistic slow path without
|
|
5840
|
-
// letting a genuinely hung dispatch tie up the UI indefinitely.
|
|
5841
|
-
return 30000;
|
|
5842
|
-
}
|
|
5843
|
-
return 8000;
|
|
5844
|
-
}
|
|
5845
|
-
|
|
5846
|
-
async function injectPageBridge() {
|
|
5847
|
-
await injectScriptOnce('src/shared/reviewing.js', 'codex-overleaf-reviewing-script');
|
|
5848
|
-
await injectScriptOnce('src/shared/projectFiles.js', 'codex-overleaf-project-files-script');
|
|
5849
|
-
await injectScriptOnce('src/shared/compatibility.js', 'codex-overleaf-compatibility-page-script');
|
|
5850
|
-
await injectScriptOnce('src/shared/staleGuard.js', 'codex-overleaf-stale-guard-script');
|
|
5851
|
-
await injectScriptOnce('src/shared/compileAdapter.js', 'codex-overleaf-compile-adapter-script');
|
|
5852
|
-
await injectScriptOnce('src/shared/governanceRules.js', 'codex-overleaf-governance-rules-script');
|
|
5853
|
-
await injectScriptOnce('src/shared/sensitiveScan.js', 'codex-overleaf-sensitive-scan-script');
|
|
5854
|
-
await injectScriptOnce('src/shared/auditRecords.js', 'codex-overleaf-audit-records-script');
|
|
5855
|
-
await injectScriptOnce('src/page/saveState.js', 'codex-overleaf-save-state-script', { force: true });
|
|
5856
|
-
await injectScriptOnce('src/page/overleafCapabilities.js', 'codex-overleaf-capabilities-script');
|
|
5857
|
-
await injectScriptOnce('src/page/compileBridge.js', 'codex-overleaf-compile-bridge-script');
|
|
5858
|
-
await injectScriptOnce('src/page/overleafEditor.js', 'codex-overleaf-editor-script');
|
|
5859
|
-
await injectScriptOnce('src/page/overleafProjectSnapshot.js', 'codex-overleaf-project-snapshot-script');
|
|
5860
|
-
await injectOptionalOtDependencies();
|
|
5861
|
-
await injectScriptOnce('src/page/pageBridgeCapability.js', 'codex-overleaf-page-bridge-capability-script');
|
|
5862
|
-
await injectScriptOnce('src/page/treeOperations.js', 'codex-overleaf-tree-operations-script', { force: true });
|
|
5863
|
-
await injectScriptOnce('src/page/snapshotRouter.js', 'codex-overleaf-snapshot-router-script');
|
|
5864
|
-
await injectScriptOnce('src/page/writeGuard.js', 'codex-overleaf-write-guard-script', { force: true });
|
|
5865
|
-
// Phase 7 (v1.8.0): the tracked-changes lifecycle must be defined in the
|
|
5866
|
-
// page world BEFORE writebackRouter.create() dereferences it — the vm
|
|
5867
|
-
// test harnesses inject it manually, so only this list proves it loads.
|
|
5868
|
-
await injectScriptOnce('src/page/trackedChangesLifecycle.js', 'codex-overleaf-tracked-changes-lifecycle-script', { force: true });
|
|
5869
|
-
await injectScriptOnce('src/page/writebackRouter.js', 'codex-overleaf-writeback-router-script', { force: true });
|
|
5870
|
-
await injectScriptOnce('src/pageBridge.js', 'codex-overleaf-page-bridge-script', { force: true });
|
|
5871
|
-
await initializePageBridgeCapability();
|
|
5872
|
-
}
|
|
5873
|
-
|
|
5874
|
-
async function injectOptionalOtDependencies() {
|
|
5875
|
-
try {
|
|
5876
|
-
await injectScriptOnce('src/shared/otText.js', 'codex-overleaf-ot-text-script');
|
|
5877
|
-
await injectScriptOnce('src/page/overleafRealtimeObserver.js', 'codex-overleaf-realtime-observer-script');
|
|
5878
|
-
} catch (_error) {
|
|
5879
|
-
// The page bridge has a read-only unavailable fallback for these optional OT helpers.
|
|
5880
|
-
}
|
|
5419
|
+
return pageBridgeClient.resolveTimeoutMs(method);
|
|
5881
5420
|
}
|
|
5882
5421
|
|
|
5883
|
-
function
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
}
|
|
5902
|
-
script.id = id;
|
|
5903
|
-
const runtimePrefix = String(root.__CODEX_OVERLEAF_RUNTIME_PREFIX__ || '');
|
|
5904
|
-
script.src = chrome.runtime.getURL(runtimePrefix + src);
|
|
5905
|
-
script.onload = () => {
|
|
5906
|
-
cleanup();
|
|
5907
|
-
resolve();
|
|
5908
|
-
};
|
|
5909
|
-
script.onerror = () => {
|
|
5910
|
-
cleanup();
|
|
5911
|
-
reject(new Error(`Failed to load ${src}`));
|
|
5912
|
-
};
|
|
5913
|
-
(document.head || document.documentElement).append(script);
|
|
5914
|
-
});
|
|
5422
|
+
async function loadStoredState() {
|
|
5423
|
+
const projectId = getCurrentProjectId();
|
|
5424
|
+
const persistenceCoordinator = getScopedPersistenceCoordinator();
|
|
5425
|
+
const hydrationView = projectId && persistenceCoordinator
|
|
5426
|
+
? await persistenceCoordinator.beginHydration(projectId)
|
|
5427
|
+
: null;
|
|
5428
|
+
const hydrationAccountScopeId = hydrationView?.accountScopeId || cachedAccountScopeId || '';
|
|
5429
|
+
const loaded = await loadStoredStateForProject(hydrationAccountScopeId);
|
|
5430
|
+
if (
|
|
5431
|
+
hydrationView
|
|
5432
|
+
&& persistenceCoordinator
|
|
5433
|
+
&& !persistenceCoordinator.isCurrent(hydrationView)
|
|
5434
|
+
) {
|
|
5435
|
+
throw Object.assign(new Error('Scoped hydration was superseded.'), {
|
|
5436
|
+
code: 'stale_hydration'
|
|
5437
|
+
});
|
|
5438
|
+
}
|
|
5439
|
+
return loaded;
|
|
5915
5440
|
}
|
|
5916
5441
|
|
|
5917
|
-
|
|
5918
|
-
const
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5442
|
+
function buildScopedFallbackStorageKey(baseKey, accountScopeId) {
|
|
5443
|
+
const account = typeof accountScopeId === 'string' ? accountScopeId.trim() : '';
|
|
5444
|
+
if (!account) {
|
|
5445
|
+
throw Object.assign(new Error('Scoped fallback storage requires an account identity.'), {
|
|
5446
|
+
code: 'account_scope_unavailable'
|
|
5447
|
+
});
|
|
5923
5448
|
}
|
|
5449
|
+
return `${baseKey}:account:${encodeURIComponent(account)}`;
|
|
5924
5450
|
}
|
|
5925
5451
|
|
|
5926
|
-
function
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5452
|
+
function isScopedPersistenceIdentityFailure(error) {
|
|
5453
|
+
return [
|
|
5454
|
+
'account_scope_unavailable',
|
|
5455
|
+
'scoped_claim_unavailable',
|
|
5456
|
+
'stale_hydration',
|
|
5457
|
+
'stale_view'
|
|
5458
|
+
].includes(error?.code);
|
|
5933
5459
|
}
|
|
5934
5460
|
|
|
5935
|
-
async function
|
|
5461
|
+
async function loadStoredStateForProject(accountScopeId = cachedAccountScopeId || '') {
|
|
5936
5462
|
try {
|
|
5937
|
-
const StorageDb =
|
|
5938
|
-
const Migration =
|
|
5463
|
+
const StorageDb = Modules.StorageDb;
|
|
5464
|
+
const Migration = Modules.StorageMigration;
|
|
5939
5465
|
const projectId = getCurrentProjectId();
|
|
5940
5466
|
const legacyKey = storageKey;
|
|
5941
5467
|
|
|
5942
|
-
const { prefs, sessions, activeSessionId } = await Migration.runMigrationIfNeeded(
|
|
5468
|
+
const { prefs, sessions, activeSessionId } = await Migration.runMigrationIfNeeded(
|
|
5469
|
+
projectId,
|
|
5470
|
+
legacyKey,
|
|
5471
|
+
accountScopeId
|
|
5472
|
+
);
|
|
5943
5473
|
|
|
5944
5474
|
return {
|
|
5945
5475
|
model: prefs.model || '', providerId: prefs.providerId || 'builtin',
|
|
@@ -5958,6 +5488,7 @@
|
|
|
5958
5488
|
panelWidth: prefs.panelWidth || PANEL_DEFAULT_WIDTH,
|
|
5959
5489
|
sessions: sessions.map(session => ({
|
|
5960
5490
|
id: session.id,
|
|
5491
|
+
accountScopeId: session.accountScopeId || accountScopeId,
|
|
5961
5492
|
title: session.title || '',
|
|
5962
5493
|
titleSource: session.titleSource || 'auto',
|
|
5963
5494
|
focusFiles: session.focusFiles || [],
|
|
@@ -5968,7 +5499,7 @@
|
|
|
5968
5499
|
history: Array.isArray(session.history) ? session.history : [],
|
|
5969
5500
|
task: typeof session.task === 'string' ? session.task : '',
|
|
5970
5501
|
pendingInputs: Array.isArray(session.pendingInputs) ? session.pendingInputs : [],
|
|
5971
|
-
mode: session.mode || prefs.mode || '
|
|
5502
|
+
mode: session.mode || prefs.mode || 'ask', providerId: session.providerId || 'builtin',
|
|
5972
5503
|
model: session.model || prefs.model || 'gpt-5.4',
|
|
5973
5504
|
reasoningEffort: session.reasoningEffort || prefs.reasoningEffort || 'high',
|
|
5974
5505
|
speedTier: session.speedTier || prefs.speedTier || 'standard',
|
|
@@ -5980,13 +5511,16 @@
|
|
|
5980
5511
|
runs: []
|
|
5981
5512
|
};
|
|
5982
5513
|
} catch (error) {
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
const stored = await chrome.storage.local.get(
|
|
5988
|
-
const fallback = stored[
|
|
5989
|
-
if (
|
|
5514
|
+
if (!accountScopeId || isScopedPersistenceIdentityFailure(error)) {
|
|
5515
|
+
throw error;
|
|
5516
|
+
}
|
|
5517
|
+
const fallbackKey = buildScopedFallbackStorageKey(storageKey, accountScopeId);
|
|
5518
|
+
const stored = await chrome.storage.local.get([fallbackKey]);
|
|
5519
|
+
const fallback = stored[fallbackKey] || {};
|
|
5520
|
+
if (
|
|
5521
|
+
fallback?.__codexOverleafCompactFallback === true
|
|
5522
|
+
&& fallback.__codexOverleafFallbackAccountScopeId === accountScopeId
|
|
5523
|
+
) {
|
|
5990
5524
|
return {
|
|
5991
5525
|
mode: fallback.mode || '', providerId: fallback.providerId || 'builtin',
|
|
5992
5526
|
model: fallback.model || '',
|
|
@@ -6007,7 +5541,7 @@
|
|
|
6007
5541
|
runs: []
|
|
6008
5542
|
};
|
|
6009
5543
|
}
|
|
6010
|
-
|
|
5544
|
+
throw error;
|
|
6011
5545
|
}
|
|
6012
5546
|
}
|
|
6013
5547
|
|
|
@@ -6027,8 +5561,8 @@
|
|
|
6027
5561
|
|
|
6028
5562
|
async function saveState(options) {
|
|
6029
5563
|
try {
|
|
6030
|
-
const StorageDb =
|
|
6031
|
-
const Migration =
|
|
5564
|
+
const StorageDb = Modules.StorageDb;
|
|
5565
|
+
const Migration = Modules.StorageMigration;
|
|
6032
5566
|
// Welcome-panel + write-guard:
|
|
6033
5567
|
// post-navigation persistence gate. The normal URL-derived projectId
|
|
6034
5568
|
// belongs to the route the user is *currently looking at*. When a run
|
|
@@ -6074,86 +5608,85 @@
|
|
|
6074
5608
|
compactState.experimentalOtByProject = state.experimentalOtByProject;
|
|
6075
5609
|
compactState.customInstructionsByProject = state.customInstructionsByProject;
|
|
6076
5610
|
compactState.governanceRulesByProject = state.governanceRulesByProject;
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
prefs.activeSessionByProject = StorageDb.buildActiveSessionByProject(
|
|
6088
|
-
latestPrefs?.activeSessionByProject || {},
|
|
5611
|
+
const stateSnapshot = typeof structuredClone === 'function'
|
|
5612
|
+
? structuredClone(state)
|
|
5613
|
+
: JSON.parse(JSON.stringify(state));
|
|
5614
|
+
const compactStateSnapshot = typeof structuredClone === 'function'
|
|
5615
|
+
? structuredClone(compactState)
|
|
5616
|
+
: JSON.parse(JSON.stringify(compactState));
|
|
5617
|
+
|
|
5618
|
+
const persistScopedState = persistenceContext => Modules.ScopedPersistenceCoordinator.persistPanelState({
|
|
5619
|
+
state: stateSnapshot,
|
|
5620
|
+
compactState: compactStateSnapshot,
|
|
6089
5621
|
projectId,
|
|
6090
|
-
compactState.activeSessionId || state.activeSessionId || ''
|
|
6091
|
-
);
|
|
6092
|
-
prefs.experimentalOtByProject = {
|
|
6093
|
-
...(latestPrefs?.experimentalOtByProject || {}),
|
|
6094
|
-
[projectId]: normalizeExperimentalOtByProject(state?.experimentalOtByProject)[projectId] === true
|
|
6095
|
-
};
|
|
6096
|
-
const normalizedGovernanceByProject = typeof normalizeGovernanceRulesByProject === 'function'
|
|
6097
|
-
? normalizeGovernanceRulesByProject(state?.governanceRulesByProject)
|
|
6098
|
-
: (state?.governanceRulesByProject || {});
|
|
6099
|
-
prefs.governanceRulesByProject = {
|
|
6100
|
-
...(latestPrefs?.governanceRulesByProject || {}),
|
|
6101
|
-
...normalizedGovernanceByProject
|
|
6102
|
-
};
|
|
6103
|
-
const normalizedCustomProject = normalizeCustomInstructionsByProject({ [projectId]: '' });
|
|
6104
|
-
const normalizedCustomProjectId = Object.keys(normalizedCustomProject)[0] || '';
|
|
6105
|
-
const currentCustomInstructions = normalizeCustomInstructionsByProject(state?.customInstructionsByProject);
|
|
6106
|
-
prefs.customInstructionsByProject = {
|
|
6107
|
-
...(latestPrefs?.customInstructionsByProject || {})
|
|
6108
|
-
};
|
|
6109
|
-
if (normalizedCustomProjectId) {
|
|
6110
|
-
prefs.customInstructionsByProject[normalizedCustomProjectId] =
|
|
6111
|
-
Object.prototype.hasOwnProperty.call(currentCustomInstructions, normalizedCustomProjectId)
|
|
6112
|
-
? currentCustomInstructions[normalizedCustomProjectId]
|
|
6113
|
-
: '';
|
|
6114
|
-
}
|
|
6115
|
-
await Migration.savePrefs(prefs);
|
|
6116
|
-
|
|
6117
|
-
// Save all displayable session state to IndexedDB. The panel history lives here;
|
|
6118
|
-
// chrome.storage.local only keeps small pointers/preferences.
|
|
6119
|
-
const sessionRecords = (state.sessions || []).map(session => (
|
|
6120
|
-
StorageDb.buildSessionRecord({
|
|
6121
|
-
...session,
|
|
6122
|
-
projectId,
|
|
6123
|
-
title: session.title || '',
|
|
6124
|
-
titleSource: session.titleSource || 'auto',
|
|
6125
|
-
codexThreadId: session.codexThreadId || '',
|
|
6126
|
-
status: 'active',
|
|
6127
|
-
focusFiles: Array.isArray(session.focusFiles) ? session.focusFiles : [],
|
|
6128
|
-
createdAt: session.createdAt,
|
|
6129
|
-
updatedAt: session.updatedAt
|
|
6130
|
-
}, { preserveRunActionPayload: true })
|
|
6131
|
-
));
|
|
6132
|
-
await window.CodexOverleafSessionPersistence.writeSessions({
|
|
6133
|
-
Migration, StorageDb, projectId,
|
|
6134
5622
|
deletedSessionIds: options?.deletedSessionIds,
|
|
6135
|
-
|
|
5623
|
+
Migration,
|
|
5624
|
+
StorageDb,
|
|
5625
|
+
SessionPersistence: Modules.SessionPersistence,
|
|
5626
|
+
normalizeExperimentalOtByProject,
|
|
5627
|
+
normalizeGovernanceRulesByProject,
|
|
5628
|
+
normalizeCustomInstructionsByProject,
|
|
5629
|
+
persistenceContext
|
|
6136
5630
|
});
|
|
5631
|
+
|
|
5632
|
+
const persistenceCoordinator = getScopedPersistenceCoordinator();
|
|
5633
|
+
if (!persistenceCoordinator) {
|
|
5634
|
+
throw Object.assign(new Error('Scoped persistence coordinator is unavailable.'), {
|
|
5635
|
+
code: 'scoped_persistence_unavailable'
|
|
5636
|
+
});
|
|
5637
|
+
} else {
|
|
5638
|
+
const committed = await persistenceCoordinator.commit(
|
|
5639
|
+
projectId,
|
|
5640
|
+
{
|
|
5641
|
+
detached: Boolean(projectIdOverride),
|
|
5642
|
+
rebase: true,
|
|
5643
|
+
queueMutation: options?.queueMutation,
|
|
5644
|
+
queueMutations: options?.queueMutations,
|
|
5645
|
+
sessionTombstones: options?.deletedSessionIds
|
|
5646
|
+
},
|
|
5647
|
+
persistScopedState
|
|
5648
|
+
);
|
|
5649
|
+
if (!committed.ok && (
|
|
5650
|
+
options?.queueMutation
|
|
5651
|
+
|| options?.queueMutations?.length
|
|
5652
|
+
|| committed.reason !== 'stale_view'
|
|
5653
|
+
)) {
|
|
5654
|
+
throw Object.assign(new Error(`Scoped persistence failed: ${committed.reason}`), {
|
|
5655
|
+
code: committed.reason
|
|
5656
|
+
});
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
6137
5659
|
} catch (error) {
|
|
6138
|
-
|
|
5660
|
+
if (
|
|
5661
|
+
error?.code === 'account_scope_unavailable' &&
|
|
5662
|
+
!options?.queueMutation &&
|
|
5663
|
+
!(Array.isArray(options?.queueMutations) && options.queueMutations.length)
|
|
5664
|
+
) {
|
|
5665
|
+
return;
|
|
5666
|
+
}
|
|
5667
|
+
if (options?.queueMutation || options?.queueMutations?.length) {
|
|
5668
|
+
throw error;
|
|
5669
|
+
}
|
|
5670
|
+
let fallbackError = null;
|
|
6139
5671
|
try {
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
5672
|
+
const accountScopeId = cachedAccountScopeId || '';
|
|
5673
|
+
const fallbackKey = buildScopedFallbackStorageKey(storageKey, accountScopeId);
|
|
5674
|
+
await chrome.storage.local.set({
|
|
5675
|
+
[fallbackKey]: {
|
|
5676
|
+
...prepareCompactFallbackState(state),
|
|
5677
|
+
__codexOverleafFallbackAccountScopeId: accountScopeId
|
|
5678
|
+
}
|
|
5679
|
+
});
|
|
5680
|
+
} catch (caughtFallbackError) {
|
|
5681
|
+
fallbackError = caughtFallbackError;
|
|
5682
|
+
}
|
|
5683
|
+
if (isStorageQuotaError(error)) {
|
|
5684
|
+
emitStorageQuotaFailure(error, fallbackError);
|
|
5685
|
+
}
|
|
5686
|
+
if (typeof appendStorageNoticeOnce === 'function') {
|
|
5687
|
+
appendStorageNoticeOnce('save-failed', tx(`Failed to save session state: ${error.message}`, `保存会话状态失败:${error.message}`));
|
|
5688
|
+
} else {
|
|
5689
|
+
throw error;
|
|
6157
5690
|
}
|
|
6158
5691
|
}
|
|
6159
5692
|
}
|
|
@@ -6216,7 +5749,8 @@
|
|
|
6216
5749
|
showPluginToast(text, { status: 'warning', sticky: true });
|
|
6217
5750
|
}
|
|
6218
5751
|
|
|
6219
|
-
function saveStateSoon(delayMs = 120) {
|
|
5752
|
+
function saveStateSoon(delayMs = 120, persistenceOptions = null) {
|
|
5753
|
+
pendingSaveStateOptions = mergeSaveStateOptions(pendingSaveStateOptions, persistenceOptions);
|
|
6220
5754
|
// If a saveState() is already in flight, do NOT start a parallel one.
|
|
6221
5755
|
// Mark the trailing flag so the in-flight save's completion callback
|
|
6222
5756
|
// schedules another one — that way the final disk snapshot reflects
|
|
@@ -6238,9 +5772,15 @@
|
|
|
6238
5772
|
}
|
|
6239
5773
|
|
|
6240
5774
|
function runQueuedSaveState() {
|
|
5775
|
+
if (saveStateInFlightPromise) {
|
|
5776
|
+
saveStateRunAfterFlight = true;
|
|
5777
|
+
return saveStateInFlightPromise;
|
|
5778
|
+
}
|
|
6241
5779
|
saveStateInFlight = true;
|
|
6242
5780
|
saveStateRunAfterFlight = false;
|
|
6243
|
-
|
|
5781
|
+
const persistenceOptions = pendingSaveStateOptions;
|
|
5782
|
+
pendingSaveStateOptions = null;
|
|
5783
|
+
const runPromise = saveState(persistenceOptions)
|
|
6244
5784
|
.catch(error => {
|
|
6245
5785
|
if (isStorageQuotaError(error)) {
|
|
6246
5786
|
// Structured FailureReason §9.8: timer-driven saveState raised a
|
|
@@ -6252,6 +5792,9 @@
|
|
|
6252
5792
|
appendPlainLog(tx(`Failed to save session state: ${formatStateSaveError(error)}`, `保存会话状态失败:${formatStateSaveError(error)}`));
|
|
6253
5793
|
})
|
|
6254
5794
|
.finally(() => {
|
|
5795
|
+
if (saveStateInFlightPromise === runPromise) {
|
|
5796
|
+
saveStateInFlightPromise = null;
|
|
5797
|
+
}
|
|
6255
5798
|
saveStateInFlight = false;
|
|
6256
5799
|
if (saveStateRunAfterFlight) {
|
|
6257
5800
|
// A saveStateSoon() landed during the in-flight phase. Flush it
|
|
@@ -6260,6 +5803,52 @@
|
|
|
6260
5803
|
saveStateSoon(0);
|
|
6261
5804
|
}
|
|
6262
5805
|
});
|
|
5806
|
+
saveStateInFlightPromise = runPromise;
|
|
5807
|
+
return runPromise;
|
|
5808
|
+
}
|
|
5809
|
+
|
|
5810
|
+
async function flushQueuedSaveState(persistenceOptions = null) {
|
|
5811
|
+
pendingSaveStateOptions = mergeSaveStateOptions(pendingSaveStateOptions, persistenceOptions);
|
|
5812
|
+
let persistLatestSnapshot = true;
|
|
5813
|
+
while (persistLatestSnapshot) {
|
|
5814
|
+
if (saveStateTimer) {
|
|
5815
|
+
clearTimeout(saveStateTimer);
|
|
5816
|
+
saveStateTimer = null;
|
|
5817
|
+
}
|
|
5818
|
+
if (saveStateInFlightPromise) {
|
|
5819
|
+
await saveStateInFlightPromise;
|
|
5820
|
+
if (saveStateTimer) {
|
|
5821
|
+
clearTimeout(saveStateTimer);
|
|
5822
|
+
saveStateTimer = null;
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5826
|
+
saveStateRunAfterFlight = false;
|
|
5827
|
+
await runQueuedSaveState();
|
|
5828
|
+
persistLatestSnapshot = Boolean(
|
|
5829
|
+
saveStateTimer
|
|
5830
|
+
|| saveStateRunAfterFlight
|
|
5831
|
+
|| pendingSaveStateOptions
|
|
5832
|
+
);
|
|
5833
|
+
}
|
|
5834
|
+
}
|
|
5835
|
+
|
|
5836
|
+
function mergeSaveStateOptions(current, incoming) {
|
|
5837
|
+
if (!incoming) return current;
|
|
5838
|
+
const queueMutations = [
|
|
5839
|
+
...(current?.queueMutations || (current?.queueMutation ? [current.queueMutation] : [])),
|
|
5840
|
+
...(incoming?.queueMutations || (incoming?.queueMutation ? [incoming.queueMutation] : []))
|
|
5841
|
+
];
|
|
5842
|
+
return {
|
|
5843
|
+
...(current || {}),
|
|
5844
|
+
...incoming,
|
|
5845
|
+
queueMutation: undefined,
|
|
5846
|
+
queueMutations,
|
|
5847
|
+
deletedSessionIds: Array.from(new Set([
|
|
5848
|
+
...(current?.deletedSessionIds || []),
|
|
5849
|
+
...(incoming?.deletedSessionIds || [])
|
|
5850
|
+
]))
|
|
5851
|
+
};
|
|
6263
5852
|
}
|
|
6264
5853
|
|
|
6265
5854
|
function scheduleRunStateSave(kind) {
|
|
@@ -6313,7 +5902,7 @@
|
|
|
6313
5902
|
}
|
|
6314
5903
|
|
|
6315
5904
|
async function selectMode(mode) {
|
|
6316
|
-
if (!['ask', '
|
|
5905
|
+
if (!['ask', 'auto'].includes(mode)) {
|
|
6317
5906
|
return;
|
|
6318
5907
|
}
|
|
6319
5908
|
const modeSelect = panel?.querySelector('[data-mode]');
|
|
@@ -6511,7 +6100,15 @@
|
|
|
6511
6100
|
renderPendingInputs();
|
|
6512
6101
|
}
|
|
6513
6102
|
|
|
6514
|
-
function startRunView({
|
|
6103
|
+
function startRunView({
|
|
6104
|
+
task,
|
|
6105
|
+
mode,
|
|
6106
|
+
model,
|
|
6107
|
+
reasoningEffort,
|
|
6108
|
+
speedTier,
|
|
6109
|
+
queueItemId = '',
|
|
6110
|
+
executionSnapshot = null
|
|
6111
|
+
}) {
|
|
6515
6112
|
let attachments = [];
|
|
6516
6113
|
if (Array.isArray(arguments[0]?.attachments)) {
|
|
6517
6114
|
attachments = arguments[0].attachments;
|
|
@@ -6521,7 +6118,8 @@
|
|
|
6521
6118
|
id: createRunId(),
|
|
6522
6119
|
task,
|
|
6523
6120
|
mode,
|
|
6524
|
-
...providerSettingsCoordinator.getProviderSnapshot(state?.providerId),
|
|
6121
|
+
...providerSettingsCoordinator.getProviderSnapshot(executionSnapshot?.providerId || state?.providerId),
|
|
6122
|
+
providerRevision: executionSnapshot?.providerRevision || '',
|
|
6525
6123
|
model,
|
|
6526
6124
|
reasoningEffort,
|
|
6527
6125
|
speedTier,
|
|
@@ -6539,6 +6137,9 @@
|
|
|
6539
6137
|
nativeRequestId: '',
|
|
6540
6138
|
codexTurnId: '',
|
|
6541
6139
|
nativeEventSeq: 0,
|
|
6140
|
+
executionSnapshot: executionSnapshot
|
|
6141
|
+
? RunExecutionSnapshot.cloneSnapshot(executionSnapshot)
|
|
6142
|
+
: undefined,
|
|
6542
6143
|
// Welcome-panel + write-guard:
|
|
6543
6144
|
// Immutable per-run capture of the project this run was submitted
|
|
6544
6145
|
// against. Every writeback / accept / undo dispatch attaches this id
|
|
@@ -6546,7 +6147,8 @@
|
|
|
6546
6147
|
// is still acting on the same project before mutating the editor. No
|
|
6547
6148
|
// language-level enforcement — the field is treated as immutable by
|
|
6548
6149
|
// convention and never reassigned anywhere downstream.
|
|
6549
|
-
runProjectId: getCurrentProjectId() || ''
|
|
6150
|
+
runProjectId: getCurrentProjectId() || '',
|
|
6151
|
+
runAccountScopeId: cachedAccountScopeId || ''
|
|
6550
6152
|
};
|
|
6551
6153
|
const active = getActiveSession(state);
|
|
6552
6154
|
const titlePatch = active?.titleSource !== 'manual'
|
|
@@ -6576,13 +6178,19 @@
|
|
|
6576
6178
|
// Mirror the immutable runProjectId on the view so writeback /
|
|
6577
6179
|
// accept / undo dispatches don't need to re-look up the record.
|
|
6578
6180
|
runProjectId: record.runProjectId,
|
|
6181
|
+
runAccountScopeId: record.runAccountScopeId,
|
|
6182
|
+
executionSnapshot: record.executionSnapshot,
|
|
6579
6183
|
root,
|
|
6580
6184
|
runProcess: root.querySelector('[data-run-process]'),
|
|
6581
6185
|
processLabel: root.querySelector('[data-run-process-summary]'),
|
|
6582
6186
|
events: root.querySelector('[data-run-events]'),
|
|
6583
6187
|
report: root.querySelector('[data-run-report]'),
|
|
6584
6188
|
status: root.querySelector('[data-run-status]'),
|
|
6585
|
-
|
|
6189
|
+
// A run starts before warm-mirror resolution has produced its lightweight
|
|
6190
|
+
// project snapshot. Seed references from the already-prefetched project
|
|
6191
|
+
// inventory so early stream events and fast Ask responses can still
|
|
6192
|
+
// resolve project-relative locations.
|
|
6193
|
+
projectFiles: getCurrentProjectReferenceFiles(),
|
|
6586
6194
|
startedAt: Date.now(),
|
|
6587
6195
|
queueItemId,
|
|
6588
6196
|
nativeRequestId: '',
|
|
@@ -6590,7 +6198,20 @@
|
|
|
6590
6198
|
};
|
|
6591
6199
|
}
|
|
6592
6200
|
|
|
6593
|
-
function
|
|
6201
|
+
function touchSessionForTerminalRun(sessionId, finishedAt) {
|
|
6202
|
+
const session = Array.isArray(state.sessions)
|
|
6203
|
+
? state.sessions.find(item => item?.id === sessionId)
|
|
6204
|
+
: null;
|
|
6205
|
+
if (!session) return '';
|
|
6206
|
+
const previousUpdatedAt = Date.parse(session.updatedAt || '') || 0;
|
|
6207
|
+
const requestedUpdatedAt = Date.parse(finishedAt || '') || Date.now();
|
|
6208
|
+
const updatedAt = new Date(Math.max(requestedUpdatedAt, previousUpdatedAt + 1)).toISOString();
|
|
6209
|
+
session.updatedAt = updatedAt;
|
|
6210
|
+
session.lastActivityAt = updatedAt;
|
|
6211
|
+
return updatedAt;
|
|
6212
|
+
}
|
|
6213
|
+
|
|
6214
|
+
async function finishRunView(text, status) {
|
|
6594
6215
|
if (!currentRunView) {
|
|
6595
6216
|
return;
|
|
6596
6217
|
}
|
|
@@ -6603,6 +6224,7 @@
|
|
|
6603
6224
|
record.status = status;
|
|
6604
6225
|
record.statusText = statusText;
|
|
6605
6226
|
record.finishedAt = new Date().toISOString();
|
|
6227
|
+
touchSessionForTerminalRun(currentRunView.sessionId, record.finishedAt);
|
|
6606
6228
|
currentRunView.terminalStatus = status;
|
|
6607
6229
|
runGuidanceController.settleView(currentRunView);
|
|
6608
6230
|
// Welcome-panel + write-guard:
|
|
@@ -6616,7 +6238,11 @@
|
|
|
6616
6238
|
const navigationDivergent = currentRunView.runProjectId
|
|
6617
6239
|
&& activeProjectId !== currentRunView.runProjectId;
|
|
6618
6240
|
if (!navigationDivergent) {
|
|
6619
|
-
|
|
6241
|
+
// A terminal badge is a durability promise: once the user can see
|
|
6242
|
+
// Done/Failed/Cancelled, an immediate reload must hydrate that same
|
|
6243
|
+
// terminal state. Drain any older running snapshot and persist the
|
|
6244
|
+
// latest record before exposing the terminal UI.
|
|
6245
|
+
await flushQueuedSaveState().catch(() => {});
|
|
6620
6246
|
}
|
|
6621
6247
|
renderSessionList();
|
|
6622
6248
|
}
|
|
@@ -6936,6 +6562,39 @@
|
|
|
6936
6562
|
|
|
6937
6563
|
|
|
6938
6564
|
|
|
6565
|
+
function getCurrentProjectMathSources() {
|
|
6566
|
+
const sources = [];
|
|
6567
|
+
const seen = new Set();
|
|
6568
|
+
let remainingChars = 256 * 1024;
|
|
6569
|
+
const add = item => {
|
|
6570
|
+
if (!item || typeof item !== 'object' || typeof item.content !== 'string') return;
|
|
6571
|
+
const path = normalizeReferencePathForRuntime(item.path || item.filePath || '');
|
|
6572
|
+
if (!path || seen.has(path) || hasUnsafeRuntimePathSegments(path)
|
|
6573
|
+
|| !/\.(?:tex|sty|cls|ltx)$/i.test(path) || remainingChars <= 0) return;
|
|
6574
|
+
const content = item.content.slice(0, Math.min(64 * 1024, remainingChars));
|
|
6575
|
+
remainingChars -= content.length;
|
|
6576
|
+
seen.add(path);
|
|
6577
|
+
sources.push({ path, content });
|
|
6578
|
+
};
|
|
6579
|
+
for (const collection of [
|
|
6580
|
+
currentRunView?.projectFiles,
|
|
6581
|
+
currentRunView?.files,
|
|
6582
|
+
currentRunView?.project?.files,
|
|
6583
|
+
currentRunView?.snapshot?.files,
|
|
6584
|
+
currentRunView?.projectSnapshot?.files,
|
|
6585
|
+
currentRunView?.contextFiles,
|
|
6586
|
+
state?.projectFiles,
|
|
6587
|
+
state?.files,
|
|
6588
|
+
state?.project?.files,
|
|
6589
|
+
state?.snapshot?.files,
|
|
6590
|
+
state?.projectSnapshot?.files,
|
|
6591
|
+
state?.contextFiles
|
|
6592
|
+
]) {
|
|
6593
|
+
if (Array.isArray(collection)) collection.forEach(add);
|
|
6594
|
+
}
|
|
6595
|
+
return sources;
|
|
6596
|
+
}
|
|
6597
|
+
|
|
6939
6598
|
function getCurrentProjectReferenceFiles() {
|
|
6940
6599
|
const files = [];
|
|
6941
6600
|
const seen = new Set();
|
|
@@ -6986,7 +6645,14 @@
|
|
|
6986
6645
|
}
|
|
6987
6646
|
};
|
|
6988
6647
|
|
|
6648
|
+
const activeSession = typeof getActiveSession === 'function' ? getActiveSession(state) : null;
|
|
6989
6649
|
addFiles(currentRunView?.projectFiles);
|
|
6650
|
+
addFiles(activeSession?.projectReferenceFiles);
|
|
6651
|
+
// ContextTray owns the exact, background-prefetched project file list.
|
|
6652
|
+
// Warm-mirror runs intentionally carry only the active/focused overlay, so
|
|
6653
|
+
// omitting this inventory makes valid references to every other project
|
|
6654
|
+
// file render as plain text.
|
|
6655
|
+
addFiles(contextTrayController?.getContextProject?.()?.files);
|
|
6990
6656
|
addFiles(currentRunView?.files);
|
|
6991
6657
|
addFiles(currentRunView?.project?.files);
|
|
6992
6658
|
addFiles(currentRunView?.snapshot?.files);
|
|
@@ -7000,14 +6666,26 @@
|
|
|
7000
6666
|
addFiles(state?.contextFiles);
|
|
7001
6667
|
addTextPaths(state?.focusFiles);
|
|
7002
6668
|
addTextPaths(state?.session?.focusFiles);
|
|
7003
|
-
const activeSession = typeof getActiveSession === 'function' ? getActiveSession(state) : null;
|
|
7004
6669
|
addTextPaths(activeSession?.focusFiles);
|
|
7005
6670
|
for (const session of Array.isArray(state?.sessions) ? state.sessions : []) {
|
|
6671
|
+
addFiles(session?.projectReferenceFiles);
|
|
7006
6672
|
addTextPaths(session?.focusFiles);
|
|
7007
6673
|
}
|
|
7008
6674
|
return files;
|
|
7009
6675
|
}
|
|
7010
6676
|
|
|
6677
|
+
function persistCurrentProjectReferenceFiles(project) {
|
|
6678
|
+
const projectFiles = captureProjectReferenceFiles(project);
|
|
6679
|
+
if (currentRunView) {
|
|
6680
|
+
currentRunView.projectFiles = projectFiles;
|
|
6681
|
+
}
|
|
6682
|
+
if (getActiveSession(state)) {
|
|
6683
|
+
state = updateActiveSession(state, { projectReferenceFiles: projectFiles });
|
|
6684
|
+
saveStateSoon();
|
|
6685
|
+
}
|
|
6686
|
+
return projectFiles;
|
|
6687
|
+
}
|
|
6688
|
+
|
|
7011
6689
|
function captureProjectReferenceFiles(project) {
|
|
7012
6690
|
const result = [];
|
|
7013
6691
|
const seen = new Set();
|
|
@@ -7032,31 +6710,11 @@
|
|
|
7032
6710
|
|
|
7033
6711
|
|
|
7034
6712
|
function isUndoResultEffectivelyApplied(run, result) {
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
.map(file => [file.path, file.content]));
|
|
7041
|
-
if (!expectedByPath.size) {
|
|
7042
|
-
return false;
|
|
7043
|
-
}
|
|
7044
|
-
const undoRestore = buildNoTraceUndoRestore(run);
|
|
7045
|
-
const editPaths = Array.from(new Set((undoRestore.operations || [])
|
|
7046
|
-
.filter(operation => (
|
|
7047
|
-
operation?.type === 'edit'
|
|
7048
|
-
&& operation.path
|
|
7049
|
-
&& expectedByPath.has(operation.path)
|
|
7050
|
-
))
|
|
7051
|
-
.map(operation => operation.path)));
|
|
7052
|
-
if (!editPaths.length) {
|
|
7053
|
-
return false;
|
|
7054
|
-
}
|
|
7055
|
-
return editPaths.every(path => (result.applied || []).some(item => (
|
|
7056
|
-
item?.operation?.type === 'edit'
|
|
7057
|
-
&& item.operation.path === path
|
|
7058
|
-
&& item?.result?.verifiedContent === expectedByPath.get(path)
|
|
7059
|
-
)));
|
|
6713
|
+
return WritebackSettlement.isUndoResultEffectivelyApplied(
|
|
6714
|
+
run,
|
|
6715
|
+
result,
|
|
6716
|
+
buildNoTraceUndoRestore(run).operations
|
|
6717
|
+
);
|
|
7060
6718
|
}
|
|
7061
6719
|
|
|
7062
6720
|
async function undoRun(runId) {
|
|
@@ -7166,7 +6824,11 @@
|
|
|
7166
6824
|
// A subset undo never closes the ledger: unselected files can still
|
|
7167
6825
|
// be undone later, so the run stays 'partial' and the button usable.
|
|
7168
6826
|
const fullSelection = selectedOperations.length === undoOperations.length;
|
|
7169
|
-
|
|
6827
|
+
applyLegacyUndoSettlement(
|
|
6828
|
+
runId,
|
|
6829
|
+
undoApplied && fullSelection ? 'applied' : 'partial',
|
|
6830
|
+
result
|
|
6831
|
+
);
|
|
7170
6832
|
}
|
|
7171
6833
|
|
|
7172
6834
|
async function undoRunTrackedChanges(runId, run) {
|
|
@@ -7239,91 +6901,13 @@
|
|
|
7239
6901
|
}
|
|
7240
6902
|
});
|
|
7241
6903
|
if (lifecycleReject) {
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
// does not match this run's expected pre-write content, synthesize an
|
|
7245
|
-
// `undo_not_verified` failure (warning, retryable, needs_review,
|
|
7246
|
-
// changedDocument:true) and attach it to the result so the §7
|
|
7247
|
-
// settlement matrix routes the run to `needs_review`.
|
|
7248
|
-
attachUndoNotVerifiedFailure(run, result);
|
|
7249
|
-
// §7 settlement matrix: only land in terminal `rejected` when post-action
|
|
7250
|
-
// proof is sufficient. If any per-op failure marks `needs_review` (or
|
|
7251
|
-
// matches a known unverified code), settle as `needs_review` so both
|
|
7252
|
-
// Accept and Undo stay actionable and the user can reconcile.
|
|
7253
|
-
applyRejectSettlement(runId, result);
|
|
7254
|
-
return;
|
|
7255
|
-
}
|
|
7256
|
-
setRunUndoStatus(runId, result.skipped?.length ? 'partial' : 'applied');
|
|
7257
|
-
}
|
|
7258
|
-
|
|
7259
|
-
// Post-undo proof step (§9.5). Called only on lifecycle reject paths after
|
|
7260
|
-
// the page bridge has returned. We only synthesize an `undo_not_verified`
|
|
7261
|
-
// failure when:
|
|
7262
|
-
// - The page op did not already emit a needs_review-class failure (the
|
|
7263
|
-
// settlement matrix would have caught it).
|
|
7264
|
-
// - The reject result has no skipped items (otherwise the existing
|
|
7265
|
-
// skipped failures drive settlement).
|
|
7266
|
-
// - At least one expected pre-run path has no matching `applied` entry
|
|
7267
|
-
// whose `verifiedContent` equals the expected content. (Note: we do
|
|
7268
|
-
// NOT delegate to `isUndoResultEffectivelyApplied` because that helper
|
|
7269
|
-
// short-circuits to true on `skipped.length === 0` — the §9.5 contract
|
|
7270
|
-
// requires real per-path verifiedContent proof.)
|
|
7271
|
-
// The synthesized failure is appended to `result.skipped` as a synthetic
|
|
7272
|
-
// proof entry so `collectFailuresFromResult` picks it up.
|
|
7273
|
-
function attachUndoNotVerifiedFailure(run, result) {
|
|
7274
|
-
if (!result || typeof result !== 'object') return;
|
|
7275
|
-
if (result.ok === false) return;
|
|
7276
|
-
if (Array.isArray(result.skipped) && result.skipped.length > 0) return;
|
|
7277
|
-
const expectedFiles = Array.isArray(run?.undoExpectedFiles) ? run.undoExpectedFiles : [];
|
|
7278
|
-
if (!expectedFiles.length) return;
|
|
7279
|
-
if (isUndoVerifiedContentMatching(run, result)) return;
|
|
7280
|
-
const firstPath = expectedFiles.find(entry => entry && typeof entry.path === 'string')?.path || '';
|
|
7281
|
-
const failure = buildContentFailure('undo_not_verified', { path: firstPath, type: 'undo' }, {
|
|
7282
|
-
changedDocument: true,
|
|
7283
|
-
terminalState: 'needs_review',
|
|
7284
|
-
evidence: {
|
|
7285
|
-
undoApplied: true,
|
|
7286
|
-
verified: false,
|
|
7287
|
-
expectedFileCount: expectedFiles.length
|
|
7288
|
-
}
|
|
7289
|
-
});
|
|
7290
|
-
const synthetic = {
|
|
7291
|
-
operation: { path: firstPath, type: 'undo' },
|
|
7292
|
-
result: {
|
|
7293
|
-
ok: false,
|
|
7294
|
-
code: 'undo_not_verified',
|
|
7295
|
-
reason: failure.userMessage,
|
|
7296
|
-
failure
|
|
7297
|
-
}
|
|
7298
|
-
};
|
|
7299
|
-
if (!Array.isArray(result.skipped)) {
|
|
7300
|
-
result.skipped = [];
|
|
7301
|
-
}
|
|
7302
|
-
result.skipped.push(synthetic);
|
|
7303
|
-
result.ok = false;
|
|
7304
|
-
}
|
|
7305
|
-
|
|
7306
|
-
// Per-path verifiedContent match for the §9.5 proof step. Returns true
|
|
7307
|
-
// only when every expected pre-run path has a matching `applied` entry
|
|
7308
|
-
// whose `result.verifiedContent` equals the expected content. Empty
|
|
7309
|
-
// expected files map → true (nothing to prove).
|
|
7310
|
-
function isUndoVerifiedContentMatching(run, result) {
|
|
7311
|
-
const expectedByPath = new Map((run?.undoExpectedFiles || [])
|
|
7312
|
-
.filter(file => file && typeof file.path === 'string' && typeof file.content === 'string')
|
|
7313
|
-
.map(file => [file.path, file.content]));
|
|
7314
|
-
if (!expectedByPath.size) return true;
|
|
7315
|
-
const applied = Array.isArray(result.applied) ? result.applied : [];
|
|
7316
|
-
for (const [path, expected] of expectedByPath.entries()) {
|
|
7317
|
-
const matched = applied.some(entry => {
|
|
7318
|
-
if (!entry || !entry.operation) return false;
|
|
7319
|
-
if (entry.operation.path !== path) return false;
|
|
7320
|
-
if (entry.operation.type !== 'edit') return false;
|
|
7321
|
-
if (entry.result && entry.result.ok === false) return false;
|
|
7322
|
-
return entry.result && entry.result.verifiedContent === expected;
|
|
6904
|
+
WritebackSettlement.attachUndoNotVerifiedFailure(run, result, {
|
|
6905
|
+
buildFailure: buildContentFailure
|
|
7323
6906
|
});
|
|
7324
|
-
|
|
6907
|
+
applyTrackedChangeSettlement(runId, 'reject', result);
|
|
6908
|
+
return;
|
|
7325
6909
|
}
|
|
7326
|
-
|
|
6910
|
+
applyLegacyUndoSettlement(runId, result.skipped?.length ? 'partial' : 'applied', result);
|
|
7327
6911
|
}
|
|
7328
6912
|
|
|
7329
6913
|
async function acceptRun(runId) {
|
|
@@ -7345,8 +6929,6 @@
|
|
|
7345
6929
|
return;
|
|
7346
6930
|
}
|
|
7347
6931
|
|
|
7348
|
-
// UI-local in-flight lock: disables the acting button with progress. Never
|
|
7349
|
-
// written to trackedChangeStatus, never persisted.
|
|
7350
6932
|
trackedChangeInFlight.set(runId, 'accept');
|
|
7351
6933
|
refreshRunCardControls(runId);
|
|
7352
6934
|
appendRunRecordEvent(runId, {
|
|
@@ -7361,25 +6943,12 @@
|
|
|
7361
6943
|
trackedChanges: run.undoTrackedChanges || [],
|
|
7362
6944
|
expectedFiles: run.undoExpectedFiles || [],
|
|
7363
6945
|
postFiles: buildTrackedUndoPostFiles(run),
|
|
7364
|
-
// The run's own forward writeback operations. The Accept replay
|
|
7365
|
-
// re-applies these exact patches so it writes only the changed
|
|
7366
|
-
// fragments, never a whole-file overwrite.
|
|
7367
6946
|
appliedOperations: Array.isArray(run.appliedOperations) ? run.appliedOperations : [],
|
|
7368
|
-
// Welcome-panel + write-guard:
|
|
7369
|
-
// bind the accept replay to the run's original project. If the user
|
|
7370
|
-
// has navigated to a different project the page-side guard refuses
|
|
7371
|
-
// with `aborted_project_changed` before any mutation.
|
|
7372
6947
|
runProjectId: getRunProjectIdForWriteback(run)
|
|
7373
6948
|
});
|
|
7374
6949
|
} finally {
|
|
7375
6950
|
trackedChangeInFlight.delete(runId);
|
|
7376
6951
|
}
|
|
7377
|
-
// Surface every page-layer step as its own run-card event row so the user
|
|
7378
|
-
// can copy-paste the full Accept trace if the sticky-Editing reconfirm
|
|
7379
|
-
// ever still cannot keep the replay untracked. Each entry is
|
|
7380
|
-
// self-contained — the diagnostic info inline carries the reviewing
|
|
7381
|
-
// detector verdict, the strict gate booleans, and per-op
|
|
7382
|
-
// re-toggle/verify outcomes so no extra page-state query is needed.
|
|
7383
6952
|
appendAcceptDiagnosticEvents(runId, Array.isArray(result.diagnostics) ? result.diagnostics : []);
|
|
7384
6953
|
appendRunRecordEvent(runId, {
|
|
7385
6954
|
title: tr('runAcceptTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
|
|
@@ -7396,10 +6965,6 @@
|
|
|
7396
6965
|
}))
|
|
7397
6966
|
}
|
|
7398
6967
|
});
|
|
7399
|
-
// Accept All is editor-undo + untracked replay. If the editor-undo could not
|
|
7400
|
-
// reach the pre-write state (content drifted), the page layer bails without
|
|
7401
|
-
// re-writing and returns not-ok. The run then stays `pending` so the user
|
|
7402
|
-
// can retry — it does NOT go to a decisive terminal status.
|
|
7403
6968
|
if (result.ok === false) {
|
|
7404
6969
|
appendRunRecordEvent(runId, {
|
|
7405
6970
|
title: tr('runAcceptTrackedFailed'),
|
|
@@ -7408,121 +6973,22 @@
|
|
|
7408
6973
|
[tr('detailReason')]: tr('runAcceptTrackedFailedReason')
|
|
7409
6974
|
}
|
|
7410
6975
|
});
|
|
7411
|
-
|
|
7412
|
-
|
|
6976
|
+
if (!WritebackSettlement.isSuccessfulTrackedChangeSettlement(result)) {
|
|
6977
|
+
refreshRunCardControls(runId);
|
|
6978
|
+
}
|
|
7413
6979
|
}
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
// the run to `needs_review` rather than terminal `accepted`.
|
|
7421
|
-
attachAcceptNotVerifiedFailure(run, result);
|
|
7422
|
-
// §7 settlement matrix: only land in terminal `accepted` when post-action
|
|
7423
|
-
// proof is sufficient. If any per-op failure marks `needs_review` (or
|
|
7424
|
-
// matches a known unverified code), settle as `needs_review` so both
|
|
7425
|
-
// Accept and Undo stay actionable and the user can reconcile.
|
|
7426
|
-
applyAcceptSettlement(runId, result);
|
|
7427
|
-
}
|
|
7428
|
-
|
|
7429
|
-
// Fail-closed when the run record carries no runProjectId of its own.
|
|
7430
|
-
//
|
|
7431
|
-
// Older persisted runs (pre-v1.3.8) and restored-from-history runs may have
|
|
7432
|
-
// an empty runProjectId. Returning the editor's current projectId as a
|
|
7433
|
-
// fallback would let the page-side write-guard pass when the user happens
|
|
7434
|
-
// to be in the project they originally ran the task on — but the run
|
|
7435
|
-
// itself has NO authoritative binding, so we can't actually prove it.
|
|
7436
|
-
// Returning '' triggers the runProjectId-missing branch of writeGuard /
|
|
7437
|
-
// checkWritebackRunProjectId, which surfaces editor_project_id_unavailable
|
|
7438
|
-
// with the canonical 'Refresh Overleaf and retry' next-step text.
|
|
6980
|
+
WritebackSettlement.attachAcceptNotVerifiedFailure(run, result, {
|
|
6981
|
+
buildFailure: buildContentFailure
|
|
6982
|
+
});
|
|
6983
|
+
applyTrackedChangeSettlement(runId, 'accept', result);
|
|
6984
|
+
}
|
|
6985
|
+
|
|
7439
6986
|
function getRunProjectIdForWriteback(run) {
|
|
7440
6987
|
return typeof run?.runProjectId === 'string' && run.runProjectId
|
|
7441
6988
|
? run.runProjectId
|
|
7442
6989
|
: '';
|
|
7443
6990
|
}
|
|
7444
6991
|
|
|
7445
|
-
// Post-accept proof step (§9.6). Called only on lifecycle accept paths
|
|
7446
|
-
// after the page bridge has returned. We only synthesize an
|
|
7447
|
-
// `accept_not_verified` failure when:
|
|
7448
|
-
// - The page op did not already emit a needs_review-class failure (the
|
|
7449
|
-
// settlement matrix would have caught it).
|
|
7450
|
-
// - The accept result has no skipped items (otherwise the existing
|
|
7451
|
-
// skipped failures drive settlement).
|
|
7452
|
-
// - The page bridge did not confirm clean post-accept state — either no
|
|
7453
|
-
// `verified` flag, or the per-path applied items do not show the
|
|
7454
|
-
// trackedChange was confirmed cleared.
|
|
7455
|
-
// The synthesized failure is appended to `result.skipped` as a synthetic
|
|
7456
|
-
// proof entry so `collectFailuresFromResult` picks it up.
|
|
7457
|
-
function attachAcceptNotVerifiedFailure(run, result) {
|
|
7458
|
-
if (!result || typeof result !== 'object') return;
|
|
7459
|
-
if (result.ok === false) return;
|
|
7460
|
-
if (Array.isArray(result.skipped) && result.skipped.length > 0) return;
|
|
7461
|
-
if (isAcceptResultEffectivelyVerified(run, result)) return;
|
|
7462
|
-
const expectedFiles = Array.isArray(run?.undoExpectedFiles) ? run.undoExpectedFiles : [];
|
|
7463
|
-
const firstPath = expectedFiles.find(entry => entry && typeof entry.path === 'string')?.path
|
|
7464
|
-
|| (run?.undoTrackedChanges || []).find(change => change && typeof change.path === 'string')?.path
|
|
7465
|
-
|| '';
|
|
7466
|
-
const failure = buildContentFailure('accept_not_verified', { path: firstPath, type: 'accept' }, {
|
|
7467
|
-
changedDocument: true,
|
|
7468
|
-
terminalState: 'needs_review',
|
|
7469
|
-
evidence: {
|
|
7470
|
-
acceptApplied: true,
|
|
7471
|
-
verified: false,
|
|
7472
|
-
expectedFileCount: expectedFiles.length,
|
|
7473
|
-
trackedChangeCount: Array.isArray(run?.undoTrackedChanges) ? run.undoTrackedChanges.length : 0
|
|
7474
|
-
}
|
|
7475
|
-
});
|
|
7476
|
-
const synthetic = {
|
|
7477
|
-
operation: { path: firstPath, type: 'accept' },
|
|
7478
|
-
result: {
|
|
7479
|
-
ok: false,
|
|
7480
|
-
code: 'accept_not_verified',
|
|
7481
|
-
reason: failure.userMessage,
|
|
7482
|
-
failure
|
|
7483
|
-
}
|
|
7484
|
-
};
|
|
7485
|
-
if (!Array.isArray(result.skipped)) {
|
|
7486
|
-
result.skipped = [];
|
|
7487
|
-
}
|
|
7488
|
-
result.skipped.push(synthetic);
|
|
7489
|
-
result.ok = false;
|
|
7490
|
-
}
|
|
7491
|
-
|
|
7492
|
-
// Mirrors `isUndoResultEffectivelyApplied` for the accept side. Returns
|
|
7493
|
-
// true when the page-bridge result carries proof that the run's tracked
|
|
7494
|
-
// changes are gone: each tracked-change ref has a matching `applied` entry
|
|
7495
|
-
// whose `result.verifiedContent` equals the run's known post-write content
|
|
7496
|
-
// for that path. Returns false when proof is missing or contradicted —
|
|
7497
|
-
// §9.6 `accept_not_verified`. Falls back to true when there are no
|
|
7498
|
-
// expected files (defensive: the page bridge already returned ok, and
|
|
7499
|
-
// there is nothing to verify against).
|
|
7500
|
-
function isAcceptResultEffectivelyVerified(run, result) {
|
|
7501
|
-
if (!result || typeof result !== 'object') return true;
|
|
7502
|
-
const expectedFiles = Array.isArray(run?.undoExpectedFiles) ? run.undoExpectedFiles : [];
|
|
7503
|
-
if (!expectedFiles.length) return true;
|
|
7504
|
-
if (result.verified === true) return true;
|
|
7505
|
-
const trackedChanges = Array.isArray(run?.undoTrackedChanges) ? run.undoTrackedChanges : [];
|
|
7506
|
-
if (!trackedChanges.length) return true;
|
|
7507
|
-
const applied = Array.isArray(result.applied) ? result.applied : [];
|
|
7508
|
-
if (!applied.length) return false;
|
|
7509
|
-
// Every tracked-change ref must show up in `applied` with a result
|
|
7510
|
-
// carrying ok:true. If any tracked change does not appear as applied,
|
|
7511
|
-
// proof is missing.
|
|
7512
|
-
return trackedChanges.every(change => {
|
|
7513
|
-
const key = change && (change.key || change.id || change.label);
|
|
7514
|
-
if (!key) return false;
|
|
7515
|
-
return applied.some(entry => {
|
|
7516
|
-
const ref = entry && entry.trackedChange;
|
|
7517
|
-
if (!ref) return false;
|
|
7518
|
-
const refKey = ref.key || ref.id || ref.label;
|
|
7519
|
-
if (refKey !== key) return false;
|
|
7520
|
-
if (entry.result && entry.result.ok === false) return false;
|
|
7521
|
-
return true;
|
|
7522
|
-
});
|
|
7523
|
-
});
|
|
7524
|
-
}
|
|
7525
|
-
|
|
7526
6992
|
// Translates the page bridge's Accept All `diagnostics` array into one
|
|
7527
6993
|
// run-card event row per step. Each entry is `{ step, info }`; the step
|
|
7528
6994
|
// names map 1:1 to the runAcceptTrackedStep* i18n keys. We render `info`
|
|
@@ -7532,14 +6998,14 @@
|
|
|
7532
6998
|
for (const entry of diagnostics || []) {
|
|
7533
6999
|
const step = entry && entry.step;
|
|
7534
7000
|
const info = (entry && entry.info) || {};
|
|
7535
|
-
const titleKey = ACCEPT_DIAGNOSTIC_TITLE_KEYS[step];
|
|
7001
|
+
const titleKey = WritebackSettlement.ACCEPT_DIAGNOSTIC_TITLE_KEYS[step];
|
|
7536
7002
|
if (!titleKey) {
|
|
7537
7003
|
continue;
|
|
7538
7004
|
}
|
|
7539
7005
|
const title = titleKey === 'runAcceptTrackedStepReplayStart' || titleKey === 'runAcceptTrackedStepReplayDone'
|
|
7540
7006
|
? tr(titleKey, { path: info.path || tr('unknownFile') })
|
|
7541
7007
|
: tr(titleKey);
|
|
7542
|
-
const status = acceptDiagnosticStatus(step, info);
|
|
7008
|
+
const status = WritebackSettlement.acceptDiagnosticStatus(step, info);
|
|
7543
7009
|
appendRunRecordEvent(runId, {
|
|
7544
7010
|
title,
|
|
7545
7011
|
kind: 'activity',
|
|
@@ -7549,289 +7015,28 @@
|
|
|
7549
7015
|
}
|
|
7550
7016
|
}
|
|
7551
7017
|
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
const ACCEPT_DIAGNOSTIC_TITLE_KEYS = {
|
|
7555
|
-
editorUndo: 'runAcceptTrackedStepEditorUndo',
|
|
7556
|
-
modeBefore: 'runAcceptTrackedStepModeBefore',
|
|
7557
|
-
forceEditing: 'runAcceptTrackedStepForceEditing',
|
|
7558
|
-
replayStart: 'runAcceptTrackedStepReplayStart',
|
|
7559
|
-
replayDone: 'runAcceptTrackedStepReplayDone',
|
|
7560
|
-
restoreReviewing: 'runAcceptTrackedStepRestoreReviewing'
|
|
7561
|
-
};
|
|
7562
|
-
|
|
7563
|
-
function acceptDiagnosticStatus(step, info) {
|
|
7564
|
-
if (step === 'modeBefore' || step === 'replayStart') {
|
|
7565
|
-
// Pre-state reads: never a failure, only context.
|
|
7566
|
-
return 'info';
|
|
7567
|
-
}
|
|
7568
|
-
if (step === 'editorUndo' || step === 'forceEditing' || step === 'restoreReviewing') {
|
|
7569
|
-
return info && info.ok === true ? 'info' : 'failed';
|
|
7570
|
-
}
|
|
7571
|
-
if (step === 'replayDone') {
|
|
7572
|
-
return info && info.ok === true ? 'info' : 'failed';
|
|
7573
|
-
}
|
|
7574
|
-
return 'info';
|
|
7575
|
-
}
|
|
7576
|
-
|
|
7577
|
-
// Drives a tracked-change-lifecycle run to a decisive terminal status when
|
|
7578
|
-
// post-action proof is sufficient. Called by `applyAcceptSettlement` /
|
|
7579
|
-
// `applyRejectSettlement` only after the §7 settlement matrix has cleared
|
|
7580
|
-
// the run: if the page-layer returned per-op failures that imply unverified
|
|
7581
|
-
// post-action state, the settlement helper routes to `needs_review` instead,
|
|
7582
|
-
// and this helper is never called. The heavy payload is emptied here so
|
|
7583
|
-
// stale refs never re-enter retention.
|
|
7584
|
-
function applyTerminalTrackedChangeStatus(runId, status) {
|
|
7585
|
-
const run = findRunRecord(runId);
|
|
7586
|
-
if (!run || !TERMINAL_TRACKED_CHANGE_STATUS.has(status)) {
|
|
7587
|
-
return;
|
|
7588
|
-
}
|
|
7589
|
-
run.trackedChangeStatus = status;
|
|
7590
|
-
run.undoTrackedChanges = [];
|
|
7591
|
-
run.undoExpectedFiles = [];
|
|
7592
|
-
saveStateSoon();
|
|
7593
|
-
refreshRunCardControls(runId);
|
|
7594
|
-
}
|
|
7595
|
-
|
|
7596
|
-
// Inline mirror of the content-side subset of the FailureReason §9 catalog
|
|
7597
|
-
// used by T5 emit sites. Mirrors the page-side `PAGE_FAILURE_CATALOG` in
|
|
7598
|
-
// `extension/src/page/writebackRouter.js` so neither runtime has to depend on
|
|
7599
|
-
// load order with `shared/failureReasons.js`. Each entry mirrors stage /
|
|
7600
|
-
// severity / defaultRetryable / fallbackUserMessage / fallbackNextAction
|
|
7601
|
-
// from `FAILURE_CODE_CATALOG`; keep in sync when adding codes.
|
|
7602
|
-
const CONTENT_FAILURE_CATALOG = {
|
|
7603
|
-
project_snapshot_unavailable: {
|
|
7604
|
-
stage: 'context', severity: 'error', defaultRetryable: true,
|
|
7605
|
-
fallbackUserMessage: 'Codex could not read the Overleaf project snapshot.',
|
|
7606
|
-
fallbackNextAction: 'Refresh Overleaf, then rerun the task.'
|
|
7607
|
-
},
|
|
7608
|
-
selected_context_unresolved: {
|
|
7609
|
-
stage: 'context', severity: 'warning', defaultRetryable: true,
|
|
7610
|
-
fallbackUserMessage: 'Codex could not resolve the requested selection or context.',
|
|
7611
|
-
fallbackNextAction: 'Select the target again or specify the file/section explicitly.'
|
|
7612
|
-
},
|
|
7613
|
-
codex_no_usable_result: {
|
|
7614
|
-
stage: 'codex', severity: 'error', defaultRetryable: true,
|
|
7615
|
-
fallbackUserMessage: 'Local Codex returned no usable final report or operations.',
|
|
7616
|
-
fallbackNextAction: 'Open Technical Details and resolve the local Codex error.'
|
|
7617
|
-
},
|
|
7618
|
-
codex_project_locked: {
|
|
7619
|
-
stage: 'codex', severity: 'blocked', defaultRetryable: true,
|
|
7620
|
-
fallbackUserMessage: 'Another Codex task is already running for this Overleaf project.',
|
|
7621
|
-
fallbackNextAction: 'Wait for the active task to finish, or cancel it before retrying.'
|
|
7622
|
-
},
|
|
7623
|
-
storage_quota_exceeded: {
|
|
7624
|
-
stage: 'storage', severity: 'warning', defaultRetryable: true,
|
|
7625
|
-
fallbackUserMessage: 'Browser storage quota was exceeded.',
|
|
7626
|
-
fallbackNextAction: 'Clear old run history or reduce attachments.'
|
|
7627
|
-
},
|
|
7628
|
-
native_bridge_unavailable: {
|
|
7629
|
-
stage: 'native', severity: 'blocked', defaultRetryable: true,
|
|
7630
|
-
fallbackUserMessage: 'Extension cannot connect to the Codex native host.',
|
|
7631
|
-
fallbackNextAction: 'Run install-native or reload the extension.'
|
|
7632
|
-
},
|
|
7633
|
-
undo_not_verified: {
|
|
7634
|
-
stage: 'undo', severity: 'warning', defaultRetryable: true,
|
|
7635
|
-
fallbackUserMessage: 'Undo ran, but Codex could not prove the file returned to pre-run content.',
|
|
7636
|
-
fallbackNextAction: 'Inspect the file manually before continuing.'
|
|
7637
|
-
},
|
|
7638
|
-
accept_not_verified: {
|
|
7639
|
-
stage: 'accept', severity: 'warning', defaultRetryable: true,
|
|
7640
|
-
fallbackUserMessage: 'Accept appeared to run but Codex could not prove final content/state.',
|
|
7641
|
-
fallbackNextAction: 'Inspect Overleaf Reviewing before continuing.'
|
|
7642
|
-
}
|
|
7643
|
-
};
|
|
7644
|
-
|
|
7645
|
-
// Build a structured FailureReason record (§7) for emit at the content
|
|
7646
|
-
// runtime layer. `overrides` merges into the entry: callers supply `file` /
|
|
7647
|
-
// `activeFile` / `userMessage` / `evidence` / `changedDocument` /
|
|
7648
|
-
// `terminalState` etc. The `op` argument is the operation context that
|
|
7649
|
-
// augments `file` / `operationType` when not explicitly overridden.
|
|
7650
|
-
function buildContentFailure(code, op, overrides) {
|
|
7651
|
-
// The local catalog holds content-runtime-specific codes; every other
|
|
7652
|
-
// code (codex_not_found, codex_timeout, codex_output_limit,
|
|
7653
|
-
// stale_source_changed, native_protocol_incompatible, ...) resolves
|
|
7654
|
-
// through the shared §9 catalog. Without this fallback the function
|
|
7655
|
-
// returned null for every shared-only code, so completion reports
|
|
7656
|
-
// carried no structured failure and the recovery-action registry never
|
|
7657
|
-
// fired for them (v1.7.6 fleet P1).
|
|
7658
|
-
const entry = CONTENT_FAILURE_CATALOG[code] || FailureReasons?.FAILURE_CODE_CATALOG?.[code];
|
|
7659
|
-
if (!entry) {
|
|
7660
|
-
return null;
|
|
7661
|
-
}
|
|
7662
|
-
const merged = overrides || {};
|
|
7663
|
-
const opCtx = op || {};
|
|
7664
|
-
const failure = {
|
|
7018
|
+
function buildContentFailure(code, operation, overrides) {
|
|
7019
|
+
return WritebackSettlement.buildContentFailure(
|
|
7665
7020
|
code,
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
retryable: merged.retryable === undefined ? entry.defaultRetryable : merged.retryable === true,
|
|
7670
|
-
nextAction: merged.nextAction || entry.fallbackNextAction
|
|
7671
|
-
};
|
|
7672
|
-
const file = merged.file !== undefined ? merged.file : opCtx.path;
|
|
7673
|
-
if (file) failure.file = file;
|
|
7674
|
-
const operationType = merged.operationType !== undefined ? merged.operationType : opCtx.type;
|
|
7675
|
-
if (operationType) failure.operationType = operationType;
|
|
7676
|
-
if (merged.activeFile !== undefined) failure.activeFile = merged.activeFile;
|
|
7677
|
-
if (merged.changedDocument !== undefined) failure.changedDocument = merged.changedDocument === true;
|
|
7678
|
-
if (merged.terminalState !== undefined) failure.terminalState = merged.terminalState;
|
|
7679
|
-
if (merged.technicalMessage !== undefined) failure.technicalMessage = merged.technicalMessage;
|
|
7680
|
-
if (merged.evidence !== undefined) failure.evidence = merged.evidence;
|
|
7681
|
-
return failure;
|
|
7682
|
-
}
|
|
7683
|
-
|
|
7684
|
-
// Accept / Undo has a two-state user lifecycle: actionable before the page
|
|
7685
|
-
// action, terminal after a successful page action. Warning-class verification
|
|
7686
|
-
// evidence can still be recorded in technical details, but it must not keep
|
|
7687
|
-
// the primary buttons actionable after Overleaf accepted the operation.
|
|
7688
|
-
|
|
7689
|
-
// Codes that imply post-Accept proof is missing or contradicted. A page-side
|
|
7690
|
-
// emitter (T4) is expected to fill these in; render-time normalization from
|
|
7691
|
-
// the FailureReasons module repairs legacy `{ ok: false, code, reason }`
|
|
7692
|
-
// shapes into the same structured failure record so this matrix works
|
|
7693
|
-
// uniformly across both shapes.
|
|
7694
|
-
const ACCEPT_NEEDS_REVIEW_CODES = new Set([
|
|
7695
|
-
'tracked_changes_remain',
|
|
7696
|
-
'accept_not_verified',
|
|
7697
|
-
'tracked_changes_created_unexpectedly',
|
|
7698
|
-
'accept_replay_created_tracked_changes',
|
|
7699
|
-
'write_observed_mismatch'
|
|
7700
|
-
]);
|
|
7701
|
-
|
|
7702
|
-
const REJECT_NEEDS_REVIEW_CODES = new Set([
|
|
7703
|
-
'undo_not_verified',
|
|
7704
|
-
'undo_operation_failed',
|
|
7705
|
-
'undo_reviewing_restore_unverified',
|
|
7706
|
-
'tracked_change_nodes_not_identified',
|
|
7707
|
-
'tracked_changes_remain',
|
|
7708
|
-
'write_observed_mismatch'
|
|
7709
|
-
]);
|
|
7710
|
-
|
|
7711
|
-
// Walks the page-layer result's applied/skipped lists, normalizing each
|
|
7712
|
-
// item's `result` (whether structured `failure` or legacy `code` + `reason`)
|
|
7713
|
-
// into a `FailureReason` record. Used by `applyAcceptSettlement` /
|
|
7714
|
-
// `applyRejectSettlement` to decide between `accepted`/`rejected`,
|
|
7715
|
-
// `needs_review`, and "stay pending" (blocked).
|
|
7716
|
-
function collectFailuresFromResult(result) {
|
|
7717
|
-
const failures = [];
|
|
7718
|
-
if (!result || typeof result !== 'object') {
|
|
7719
|
-
return failures;
|
|
7720
|
-
}
|
|
7721
|
-
if (FailureReasons && FailureReasons.normalizeFailureReason instanceof Function) {
|
|
7722
|
-
for (const entry of Array.isArray(result.skipped) ? result.skipped : []) {
|
|
7723
|
-
const inner = (entry && entry.result) || entry;
|
|
7724
|
-
if (!inner || inner.ok === true) continue;
|
|
7725
|
-
const operation = (entry && entry.operation) || (entry && entry.trackedChange ? { path: entry.trackedChange.path } : undefined);
|
|
7726
|
-
failures.push(FailureReasons.normalizeFailureReason(inner, operation));
|
|
7727
|
-
}
|
|
7728
|
-
for (const entry of Array.isArray(result.applied) ? result.applied : []) {
|
|
7729
|
-
const inner = entry && entry.result;
|
|
7730
|
-
if (!inner || inner.ok !== false) continue;
|
|
7731
|
-
const operation = (entry && entry.operation) || (entry && entry.trackedChange ? { path: entry.trackedChange.path } : undefined);
|
|
7732
|
-
failures.push(FailureReasons.normalizeFailureReason(inner, operation));
|
|
7733
|
-
}
|
|
7734
|
-
} else {
|
|
7735
|
-
// Defensive fallback: the FailureReasons module should be present in
|
|
7736
|
-
// production (loaded by the content script bundle) but tests that strip
|
|
7737
|
-
// it out should still see the legacy code-only path treated as a
|
|
7738
|
-
// generic non-blocking failure.
|
|
7739
|
-
for (const entry of Array.isArray(result.skipped) ? result.skipped : []) {
|
|
7740
|
-
const inner = (entry && entry.result) || entry;
|
|
7741
|
-
if (inner && inner.ok !== true) {
|
|
7742
|
-
failures.push({
|
|
7743
|
-
code: (inner && typeof inner.code === 'string' ? inner.code : '') || 'unknown_legacy_failure',
|
|
7744
|
-
severity: 'error'
|
|
7745
|
-
});
|
|
7746
|
-
}
|
|
7747
|
-
}
|
|
7748
|
-
}
|
|
7749
|
-
return failures;
|
|
7750
|
-
}
|
|
7751
|
-
|
|
7752
|
-
// Run-card settlement for Accept changes. Three branches:
|
|
7753
|
-
// 1. The primary failure is `blocked` (e.g. preflight / navigation refused
|
|
7754
|
-
// to touch Overleaf) — stay pending so the user can retry without the
|
|
7755
|
-
// card claiming terminal accepted.
|
|
7756
|
-
// 2. Any successful page-side accept work — settle as terminal `accepted`.
|
|
7757
|
-
// 3. Otherwise, unverified/no-op failures can remain retryable.
|
|
7758
|
-
function applyAcceptSettlement(runId, result) {
|
|
7759
|
-
const failures = collectFailuresFromResult(result);
|
|
7760
|
-
const primary = FailureReasons && FailureReasons.selectPrimaryFailure instanceof Function
|
|
7761
|
-
? FailureReasons.selectPrimaryFailure(failures)
|
|
7762
|
-
: (failures[0] || null);
|
|
7763
|
-
if (primary && primary.terminalState === 'blocked') {
|
|
7764
|
-
// Stay pending — page bridge declined the operation before any document
|
|
7765
|
-
// change. The preceding result event already showed the user the reason.
|
|
7766
|
-
refreshRunCardControls(runId);
|
|
7767
|
-
return;
|
|
7768
|
-
}
|
|
7769
|
-
if (isSuccessfulTrackedChangeSettlement(result)) {
|
|
7770
|
-
applyTerminalTrackedChangeStatus(runId, 'accepted');
|
|
7771
|
-
return;
|
|
7772
|
-
}
|
|
7773
|
-
const needsReview = failures.some(failure =>
|
|
7774
|
-
failure.terminalState === 'needs_review' ||
|
|
7775
|
-
ACCEPT_NEEDS_REVIEW_CODES.has(failure.code)
|
|
7021
|
+
operation,
|
|
7022
|
+
overrides,
|
|
7023
|
+
FailureReasons
|
|
7776
7024
|
);
|
|
7777
|
-
if (needsReview) {
|
|
7778
|
-
applyNeedsReviewTrackedChangeStatus(runId);
|
|
7779
|
-
return;
|
|
7780
|
-
}
|
|
7781
|
-
applyTerminalTrackedChangeStatus(runId, 'accepted');
|
|
7782
|
-
}
|
|
7783
|
-
|
|
7784
|
-
// Same three-branch shape as `applyAcceptSettlement`, using the undo-side
|
|
7785
|
-
// unverified-proof codes only when no successful reject/undo work happened.
|
|
7786
|
-
function applyRejectSettlement(runId, result) {
|
|
7787
|
-
const failures = collectFailuresFromResult(result);
|
|
7788
|
-
const primary = FailureReasons && FailureReasons.selectPrimaryFailure instanceof Function
|
|
7789
|
-
? FailureReasons.selectPrimaryFailure(failures)
|
|
7790
|
-
: (failures[0] || null);
|
|
7791
|
-
if (primary && primary.terminalState === 'blocked') {
|
|
7792
|
-
refreshRunCardControls(runId);
|
|
7793
|
-
return;
|
|
7794
|
-
}
|
|
7795
|
-
if (isSuccessfulTrackedChangeSettlement(result)) {
|
|
7796
|
-
applyTerminalTrackedChangeStatus(runId, 'rejected');
|
|
7797
|
-
return;
|
|
7798
|
-
}
|
|
7799
|
-
const needsReview = failures.some(failure =>
|
|
7800
|
-
failure.terminalState === 'needs_review' ||
|
|
7801
|
-
REJECT_NEEDS_REVIEW_CODES.has(failure.code)
|
|
7802
|
-
);
|
|
7803
|
-
if (needsReview) {
|
|
7804
|
-
applyNeedsReviewTrackedChangeStatus(runId);
|
|
7805
|
-
return;
|
|
7806
|
-
}
|
|
7807
|
-
applyTerminalTrackedChangeStatus(runId, 'rejected');
|
|
7808
|
-
}
|
|
7809
|
-
|
|
7810
|
-
function isSuccessfulTrackedChangeSettlement(result) {
|
|
7811
|
-
if (!result || typeof result !== 'object') {
|
|
7812
|
-
return false;
|
|
7813
|
-
}
|
|
7814
|
-
if (result.ok === true) {
|
|
7815
|
-
return true;
|
|
7816
|
-
}
|
|
7817
|
-
if (!Array.isArray(result.applied)) {
|
|
7818
|
-
return false;
|
|
7819
|
-
}
|
|
7820
|
-
return result.applied.some(entry => {
|
|
7821
|
-
const inner = entry && (entry.result || entry);
|
|
7822
|
-
return !inner || inner.ok !== false;
|
|
7823
|
-
});
|
|
7824
7025
|
}
|
|
7825
7026
|
|
|
7826
|
-
|
|
7827
|
-
// to retry Accept or Undo after inspecting Overleaf, so the heavy payload
|
|
7828
|
-
// (tracked-change refs + expected files) must survive.
|
|
7829
|
-
function applyNeedsReviewTrackedChangeStatus(runId) {
|
|
7027
|
+
function applyTrackedChangeSettlement(runId, kind, result) {
|
|
7830
7028
|
const run = findRunRecord(runId);
|
|
7831
|
-
|
|
7029
|
+
const settlement = WritebackSettlement.settleTrackedChangeLifecycle({
|
|
7030
|
+
kind,
|
|
7031
|
+
run,
|
|
7032
|
+
result,
|
|
7033
|
+
failureReasons: FailureReasons
|
|
7034
|
+
});
|
|
7035
|
+
if (settlement.decision === 'blocked') {
|
|
7036
|
+
refreshRunCardControls(runId);
|
|
7832
7037
|
return;
|
|
7833
7038
|
}
|
|
7834
|
-
run.
|
|
7039
|
+
Object.assign(run, WritebackSettlement.applySettlementTransition(run, settlement));
|
|
7835
7040
|
saveStateSoon();
|
|
7836
7041
|
refreshRunCardControls(runId);
|
|
7837
7042
|
}
|
|
@@ -7908,13 +7113,7 @@
|
|
|
7908
7113
|
// possibly-divergent base. This keeps wide paragraph patches safe: their
|
|
7909
7114
|
// whole-paragraph `expected` would otherwise silently fail to re-apply.
|
|
7910
7115
|
function attachVerifiedContentToOperation(operation, result) {
|
|
7911
|
-
|
|
7912
|
-
return operation;
|
|
7913
|
-
}
|
|
7914
|
-
if (operation.type === 'edit' && typeof result?.verifiedContent === 'string') {
|
|
7915
|
-
return { ...operation, verifiedContent: result.verifiedContent };
|
|
7916
|
-
}
|
|
7917
|
-
return operation;
|
|
7116
|
+
return WritebackSettlement.attachVerifiedContentToOperation(operation, result);
|
|
7918
7117
|
}
|
|
7919
7118
|
|
|
7920
7119
|
function recordUndoFromApply(project, applyResult) {
|
|
@@ -7938,15 +7137,20 @@
|
|
|
7938
7137
|
];
|
|
7939
7138
|
record.appliedOperations = combinedAppliedOperations;
|
|
7940
7139
|
|
|
7941
|
-
if (state.requireReviewing === true) {
|
|
7942
|
-
const combinedTrackedChanges =
|
|
7140
|
+
if ((currentRunView?.executionSnapshot?.requireReviewing ?? state.requireReviewing) === true) {
|
|
7141
|
+
const combinedTrackedChanges = normalizeApplyTrackedChanges([
|
|
7943
7142
|
...(Array.isArray(record.undoTrackedChanges) ? record.undoTrackedChanges : []),
|
|
7944
7143
|
...trackedChanges
|
|
7945
7144
|
]);
|
|
7946
7145
|
record.undoOperations = [];
|
|
7947
7146
|
record.undoBaseFiles = [];
|
|
7948
7147
|
record.undoTrackedChanges = combinedTrackedChanges;
|
|
7949
|
-
record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(
|
|
7148
|
+
record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(
|
|
7149
|
+
project,
|
|
7150
|
+
combinedAppliedOperations,
|
|
7151
|
+
combinedTrackedChanges,
|
|
7152
|
+
record.undoExpectedFiles
|
|
7153
|
+
);
|
|
7950
7154
|
record.undoStatus = '';
|
|
7951
7155
|
record.partialWriteback = skippedEntries.length > 0;
|
|
7952
7156
|
// Recording tracked-change refs enters the run into the tracked-change
|
|
@@ -7994,7 +7198,12 @@
|
|
|
7994
7198
|
record.undoOperations = checkpoint.undoOperations;
|
|
7995
7199
|
record.undoBaseFiles = checkpoint.undoBaseFiles;
|
|
7996
7200
|
record.undoTrackedChanges = [];
|
|
7997
|
-
record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(
|
|
7201
|
+
record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(
|
|
7202
|
+
project,
|
|
7203
|
+
combinedAppliedOperations,
|
|
7204
|
+
[],
|
|
7205
|
+
record.undoExpectedFiles
|
|
7206
|
+
);
|
|
7998
7207
|
record.undoStatus = '';
|
|
7999
7208
|
record.partialWriteback = skippedEntries.length > 0;
|
|
8000
7209
|
refreshRunCardControls(record.id);
|
|
@@ -8011,49 +7220,21 @@
|
|
|
8011
7220
|
}
|
|
8012
7221
|
|
|
8013
7222
|
function normalizeApplyTrackedChanges(changes = []) {
|
|
8014
|
-
|
|
8015
|
-
const normalized = [];
|
|
8016
|
-
for (const change of changes || []) {
|
|
8017
|
-
const key = typeof change?.key === 'string' ? change.key : '';
|
|
8018
|
-
if (!key || seen.has(key)) {
|
|
8019
|
-
continue;
|
|
8020
|
-
}
|
|
8021
|
-
seen.add(key);
|
|
8022
|
-
normalized.push({
|
|
8023
|
-
key,
|
|
8024
|
-
id: typeof change.id === 'string' ? change.id : '',
|
|
8025
|
-
path: typeof change.path === 'string' ? change.path : '',
|
|
8026
|
-
label: typeof change.label === 'string' ? change.label : ''
|
|
8027
|
-
});
|
|
8028
|
-
}
|
|
8029
|
-
return normalized;
|
|
8030
|
-
}
|
|
8031
|
-
|
|
8032
|
-
function mergeTrackedChanges(changes = []) {
|
|
8033
|
-
return normalizeApplyTrackedChanges(changes);
|
|
7223
|
+
return WritebackSettlement.normalizeApplyTrackedChanges(changes);
|
|
8034
7224
|
}
|
|
8035
7225
|
|
|
8036
|
-
function selectExpectedFilesForTrackedUndo(
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
paths.add(operation.to);
|
|
8049
|
-
}
|
|
8050
|
-
}
|
|
8051
|
-
return (project?.files || [])
|
|
8052
|
-
.filter(file => paths.has(file.path) && typeof file.content === 'string')
|
|
8053
|
-
.map(file => ({
|
|
8054
|
-
path: file.path,
|
|
8055
|
-
content: file.content
|
|
8056
|
-
}));
|
|
7226
|
+
function selectExpectedFilesForTrackedUndo(
|
|
7227
|
+
project,
|
|
7228
|
+
operations = [],
|
|
7229
|
+
trackedChanges = [],
|
|
7230
|
+
previousExpectedFiles = []
|
|
7231
|
+
) {
|
|
7232
|
+
return WritebackSettlement.selectExpectedFilesForTrackedUndo(
|
|
7233
|
+
project,
|
|
7234
|
+
operations,
|
|
7235
|
+
trackedChanges,
|
|
7236
|
+
previousExpectedFiles
|
|
7237
|
+
);
|
|
8057
7238
|
}
|
|
8058
7239
|
|
|
8059
7240
|
function buildTrackedUndoPostFiles(run) {
|
|
@@ -8136,6 +7317,21 @@
|
|
|
8136
7317
|
refreshRunCardControls(runId);
|
|
8137
7318
|
}
|
|
8138
7319
|
|
|
7320
|
+
function applyLegacyUndoSettlement(runId, undoStatus, result) {
|
|
7321
|
+
const run = findRunRecord(runId);
|
|
7322
|
+
if (!run) return;
|
|
7323
|
+
Object.assign(run, WritebackSettlement.applySettlementTransition(
|
|
7324
|
+
run,
|
|
7325
|
+
WritebackSettlement.settleLegacyUndo({
|
|
7326
|
+
run,
|
|
7327
|
+
status: undoStatus,
|
|
7328
|
+
result
|
|
7329
|
+
})
|
|
7330
|
+
));
|
|
7331
|
+
saveStateSoon();
|
|
7332
|
+
refreshRunCardControls(runId);
|
|
7333
|
+
}
|
|
7334
|
+
|
|
8139
7335
|
function refreshRunCardControls(runId) {
|
|
8140
7336
|
const run = findRunRecord(runId);
|
|
8141
7337
|
const root = panel?.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
|
|
@@ -8290,13 +7486,16 @@
|
|
|
8290
7486
|
const operations = input.operations || [];
|
|
8291
7487
|
const applyResults = input.applyResults || [];
|
|
8292
7488
|
const record = currentRunView?.recordId ? findRunRecord(currentRunView.recordId, currentRunView.sessionId) : null;
|
|
8293
|
-
const
|
|
7489
|
+
const settlementProjection = WritebackSettlement.projectRunSettlement(record || {});
|
|
7490
|
+
const undoCount = settlementProjection.canUndo ? getRunUndoCount(record) : 0;
|
|
8294
7491
|
const report = buildHumanCompletionReport({
|
|
8295
7492
|
...input,
|
|
8296
7493
|
locale: getLocale(),
|
|
8297
7494
|
operations,
|
|
8298
7495
|
applyResults,
|
|
8299
7496
|
undoCount,
|
|
7497
|
+
settlementProjection,
|
|
7498
|
+
failure: input.failure || settlementProjection.primaryFailure || undefined,
|
|
8300
7499
|
includeWriteResult: true
|
|
8301
7500
|
});
|
|
8302
7501
|
|
|
@@ -8315,7 +7514,7 @@
|
|
|
8315
7514
|
// which recovery button to render. (Fixed in v1.7.5: callers passed
|
|
8316
7515
|
// input.failure since v1.6.2 but it was never forwarded, so recovery
|
|
8317
7516
|
// buttons on completion reports could never appear.)
|
|
8318
|
-
failure: input.failure || undefined,
|
|
7517
|
+
failure: input.failure || settlementProjection.primaryFailure || undefined,
|
|
8319
7518
|
compileErrors: input.compileErrors || undefined,
|
|
8320
7519
|
rejectedHunks: input.rejectedHunks || currentRunView?.rejectedHunks || undefined
|
|
8321
7520
|
});
|