codex-overleaf-link 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -23
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/runtime-manifest.json +5 -71
- package/extension/src/background.js +53 -69
- package/extension/src/backgroundUpdateCoordinator.js +128 -66
- package/extension/src/content/applyResultFormatters.js +2 -3
- package/extension/src/content/contentRuntime.js +650 -1449
- 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 +41370 -0
- package/extension/src/content/generated/content.bundle.meta.json +102 -0
- package/extension/src/content/legacyGlobalRegistry.js +129 -0
- package/extension/src/content/markdownText.js +5 -7
- 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 +7 -5
- 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 +11 -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 +91 -23
- package/extension/src/contentScript.js +5 -1
- package/extension/src/page/trackedChangesLifecycle.js +6 -1
- package/extension/src/pageBridge.js +47 -98
- package/extension/src/shared/agentTranscript.js +8 -5
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/governanceRules.js +1 -1
- 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 +177 -0
- package/extension/src/shared/runExecutionSnapshotCodec.js +168 -0
- package/extension/src/shared/runInputQueue.js +81 -34
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +79 -0
- package/extension/src/shared/scopedPersistenceQueuePolicy.js +181 -0
- package/extension/src/shared/scopedPersistenceTransaction.js +238 -0
- package/extension/src/shared/sessionState.js +64 -48
- package/extension/src/shared/settlementFacts.js +351 -0
- package/extension/src/shared/storageDb.js +19 -5
- 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 +143 -0
- package/extension/src/shared/writebackSettlement.js +705 -0
- 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 +3 -3
- 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;
|
|
@@ -55,31 +60,28 @@
|
|
|
55
60
|
'skills.install',
|
|
56
61
|
'skills.remove'
|
|
57
62
|
]);
|
|
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();
|
|
63
|
+
const PageRpcContract = Modules.PageRpcContract;
|
|
70
64
|
const PANEL_DEFAULT_WIDTH = 380;
|
|
71
65
|
const PANEL_MIN_WIDTH = 340;
|
|
72
66
|
const PANEL_MAX_WIDTH = 760;
|
|
73
67
|
const PAGE_MIN_WIDTH = 520;
|
|
74
|
-
const CodexOverleafCompatibility =
|
|
68
|
+
const CodexOverleafCompatibility = Modules.Compatibility;
|
|
75
69
|
const INSTALL_COMMAND = CodexOverleafCompatibility?.buildInstallCommand?.(
|
|
76
70
|
undefined,
|
|
77
71
|
undefined,
|
|
78
72
|
getCurrentExtensionId()
|
|
79
73
|
) || 'curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/main/install.sh | bash -s -- --extension-id <chrome-extension-id>';
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
const pageBridgeClient = Modules.PageBridgeClient.create({
|
|
75
|
+
root,
|
|
76
|
+
window,
|
|
77
|
+
document,
|
|
78
|
+
chromeApi: chrome,
|
|
79
|
+
crypto,
|
|
80
|
+
contract: PageRpcContract,
|
|
81
|
+
compatibility: CodexOverleafCompatibility,
|
|
82
|
+
isCancellationRequested: () => runCancellationRequested
|
|
83
|
+
});
|
|
84
|
+
pageBridgeClient.start();
|
|
83
85
|
const {
|
|
84
86
|
createSession,
|
|
85
87
|
deleteSession,
|
|
@@ -92,43 +94,45 @@
|
|
|
92
94
|
selectVisibleSessionsForList,
|
|
93
95
|
setActiveSession,
|
|
94
96
|
updateActiveSession
|
|
95
|
-
} =
|
|
96
|
-
const LineReferences =
|
|
97
|
-
const { getProjectStorageKey } =
|
|
98
|
-
const { HIGH_RISK_TYPES, buildChangeSummaryLine, hasSkippedApplyOperations } =
|
|
97
|
+
} = Modules.SessionState;
|
|
98
|
+
const LineReferences = Modules.LineReferences;
|
|
99
|
+
const { getProjectStorageKey } = Modules.StorageKeys;
|
|
100
|
+
const { HIGH_RISK_TYPES, buildChangeSummaryLine, hasSkippedApplyOperations } = Modules.Summary;
|
|
99
101
|
const {
|
|
100
102
|
buildExpectedFilesAfterOperations,
|
|
101
103
|
buildSnapshotRestoreUndo,
|
|
102
104
|
buildUndoCheckpoint
|
|
103
|
-
} =
|
|
105
|
+
} = Modules.UndoOperations;
|
|
104
106
|
const { buildHumanCompletionReport, mapAgentEventToActivity, stripEmptyHtmlCommentPlaceholders,
|
|
105
|
-
translateRawError } =
|
|
106
|
-
const i18n =
|
|
107
|
-
const nativeChannel =
|
|
107
|
+
translateRawError } = Modules.AgentTranscript;
|
|
108
|
+
const i18n = Modules.I18n;
|
|
109
|
+
const nativeChannel = Modules.NativeChannel.create({
|
|
108
110
|
chrome,
|
|
109
111
|
crypto
|
|
110
112
|
});
|
|
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 =
|
|
113
|
+
const writebackController = Modules.WritebackController;
|
|
114
|
+
const ReviewHunks = Modules.ReviewHunks;
|
|
115
|
+
const DiffReviewPanel = Modules.DiffReviewPanel;
|
|
116
|
+
const ContextTray = Modules.ContextTray;
|
|
117
|
+
const LocalSkillsPanel = Modules.LocalSkillsPanel;
|
|
118
|
+
const runController = Modules.RunController;
|
|
119
|
+
const RunInputQueue = Modules.RunInputQueue;
|
|
120
|
+
const RunQueueScheduler = Modules.RunQueueScheduler;
|
|
121
|
+
const ActiveTurnControl = Modules.ActiveTurnControl;
|
|
122
|
+
const PendingInputView = Modules.PendingInputView;
|
|
123
|
+
const mirrorHealth = Modules.MirrorHealth;
|
|
124
|
+
const otWarmMirrorController = Modules.OtWarmMirrorController;
|
|
125
|
+
const PanelRenderer = Modules.PanelRenderer;
|
|
126
|
+
const SessionPanel = Modules.SessionPanel;
|
|
127
|
+
const SettingsPanel = Modules.SettingsPanel;
|
|
128
|
+
const DiagnosticsPanel = Modules.DiagnosticsPanel;
|
|
129
|
+
const ComposerPanel = Modules.ComposerPanel;
|
|
130
|
+
const GovernanceRules = Modules.GovernanceRules;
|
|
131
|
+
const SensitiveScan = Modules.SensitiveScan;
|
|
132
|
+
const AuditRecords = Modules.AuditRecords;
|
|
133
|
+
const FailureReasons = Modules.FailureReasons;
|
|
134
|
+
const RunExecutionSnapshot = Modules.RunExecutionSnapshot;
|
|
135
|
+
const WritebackSettlement = Modules.WritebackSettlement;
|
|
132
136
|
|
|
133
137
|
// Module composition (hoisted-safe zone). These wiring blocks MUST run
|
|
134
138
|
// before any controller below consumes their destructured exports by
|
|
@@ -140,7 +144,6 @@
|
|
|
140
144
|
// `rejected`; `accepted` and `rejected` are terminal. Kept in sync with
|
|
141
145
|
// sessionState.js. A run is in the tracked-change lifecycle when it carries
|
|
142
146
|
// one of these values or still holds tracked-change refs.
|
|
143
|
-
const TERMINAL_TRACKED_CHANGE_STATUS = new Set(['accepted', 'rejected']);
|
|
144
147
|
|
|
145
148
|
// The UI-local in-flight lock for tracked-change accept/reject. Never written
|
|
146
149
|
// to trackedChangeStatus and never persisted — it lives only on the controller.
|
|
@@ -173,9 +176,12 @@
|
|
|
173
176
|
// runtime collaborators they need; the destructured consts keep every
|
|
174
177
|
// existing call site unchanged. Function declarations hoist, so passing them
|
|
175
178
|
// here is safe; mutable state is handed over as lazy getters.
|
|
176
|
-
const markdownText =
|
|
179
|
+
const markdownText = Modules.MarkdownText.create({
|
|
180
|
+
LineReferences: Modules.LineReferences,
|
|
181
|
+
MathText: Modules.MathText,
|
|
177
182
|
tx,
|
|
178
183
|
callPageBridge,
|
|
184
|
+
getCurrentProjectId,
|
|
179
185
|
getCurrentProjectReferenceFiles,
|
|
180
186
|
showPluginToast,
|
|
181
187
|
recordRenderingDiagnostic: detail => appendTechnicalEvent({ type: 'rendering.diagnostic', detail })
|
|
@@ -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,
|
|
@@ -529,9 +556,12 @@
|
|
|
529
556
|
confirmBinaryOperations,
|
|
530
557
|
filterSyncChangesByOperations,
|
|
531
558
|
writebackController,
|
|
559
|
+
writebackSettlement: WritebackSettlement,
|
|
560
|
+
compileAdapter: Modules.CompileAdapter,
|
|
532
561
|
RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
|
|
533
562
|
getState: () => state,
|
|
534
|
-
getCurrentRunView: () => currentRunView
|
|
563
|
+
getCurrentRunView: () => currentRunView,
|
|
564
|
+
onMirrorRefreshSettled: runSettlementPersistence.settleMirrorRefresh
|
|
535
565
|
});
|
|
536
566
|
const {
|
|
537
567
|
applySyncChangesToOverleaf,
|
|
@@ -570,7 +600,8 @@
|
|
|
570
600
|
}
|
|
571
601
|
}
|
|
572
602
|
|
|
573
|
-
const modelPicker =
|
|
603
|
+
const modelPicker = Modules.ModelPicker.create({
|
|
604
|
+
Support: Modules.ModelPickerSupport,
|
|
574
605
|
tr,
|
|
575
606
|
tx,
|
|
576
607
|
getLocale,
|
|
@@ -603,7 +634,7 @@
|
|
|
603
634
|
updateModelDisplay
|
|
604
635
|
} = modelPicker;
|
|
605
636
|
|
|
606
|
-
const projectProviderSelection =
|
|
637
|
+
const projectProviderSelection = Modules.ProjectProviderSelection.create({
|
|
607
638
|
tx, tr, getRunningSessionIds, showToast: showPluginToast, showConfirm: showPluginConfirm,
|
|
608
639
|
getState: () => state, setState: next => { state = next; }, getPanel: () => panel,
|
|
609
640
|
readSelectedModel: readSelectedModelInput, readSelectedSpeed: readSelectedSpeedInput,
|
|
@@ -611,7 +642,9 @@
|
|
|
611
642
|
normalizeState: normalizePanelState, applyStateToPanel
|
|
612
643
|
});
|
|
613
644
|
|
|
614
|
-
const providerSettingsCoordinator =
|
|
645
|
+
const providerSettingsCoordinator = Modules.ProviderSettingsCoordinator.create({
|
|
646
|
+
ProviderProfiles: Modules.ProviderProfiles,
|
|
647
|
+
ProviderSettingsDialog: Modules.ProviderSettingsDialog,
|
|
615
648
|
tx,
|
|
616
649
|
sendBackgroundNative,
|
|
617
650
|
document,
|
|
@@ -658,7 +691,13 @@
|
|
|
658
691
|
}
|
|
659
692
|
});
|
|
660
693
|
|
|
661
|
-
const recentProjects =
|
|
694
|
+
const recentProjects = Modules.RecentProjects.create({
|
|
695
|
+
ProjectSessionCleanup: Modules.ProjectSessionCleanup,
|
|
696
|
+
SessionPersistence: Modules.SessionPersistence,
|
|
697
|
+
SessionState: Modules.SessionState,
|
|
698
|
+
StorageDb: Modules.StorageDb,
|
|
699
|
+
StorageKeys: Modules.StorageKeys,
|
|
700
|
+
StorageMigration: Modules.StorageMigration,
|
|
662
701
|
tr,
|
|
663
702
|
tx,
|
|
664
703
|
openCustomInstructionsSettings,
|
|
@@ -690,7 +729,7 @@
|
|
|
690
729
|
const MAX_COMPOSER_ATTACHMENT_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
691
730
|
const MAX_COMPOSER_ATTACHMENTS = 8;
|
|
692
731
|
const MAX_ATTACHMENT_PREVIEW_DATA_URL_CHARS = 768 * 1024;
|
|
693
|
-
const composerAttachmentController =
|
|
732
|
+
const composerAttachmentController = Modules.ComposerAttachments.createComposerAttachmentController({
|
|
694
733
|
getPanel: () => panel,
|
|
695
734
|
tr,
|
|
696
735
|
tx,
|
|
@@ -708,6 +747,7 @@
|
|
|
708
747
|
reviewHunks: ReviewHunks,
|
|
709
748
|
tr,
|
|
710
749
|
callPageBridge,
|
|
750
|
+
getCurrentProjectId: () => getCurrentProjectId(),
|
|
711
751
|
getRunEvents: () => currentRunView?.events,
|
|
712
752
|
appendRunEvent,
|
|
713
753
|
scrollLogToBottom,
|
|
@@ -736,7 +776,7 @@
|
|
|
736
776
|
closeDiagnosticsMenu,
|
|
737
777
|
closeCustomInstructionsSettings,
|
|
738
778
|
closeSlashMenu,
|
|
739
|
-
projectFiles:
|
|
779
|
+
projectFiles: Modules.ProjectFiles
|
|
740
780
|
});
|
|
741
781
|
const localSkillsPanel = LocalSkillsPanel.createLocalSkillsPanelController({
|
|
742
782
|
root: window,
|
|
@@ -795,6 +835,8 @@
|
|
|
795
835
|
// (captured at the start of the in-flight save) be the last writer.
|
|
796
836
|
let saveStateInFlight = false;
|
|
797
837
|
let saveStateRunAfterFlight = false;
|
|
838
|
+
let saveStateInFlightPromise = null;
|
|
839
|
+
let pendingSaveStateOptions = null;
|
|
798
840
|
let streamRenderTimer = null;
|
|
799
841
|
let pendingStreamRenderEvents = new Map();
|
|
800
842
|
let storageNoticeKeys = new Set();
|
|
@@ -806,7 +848,35 @@
|
|
|
806
848
|
let runCancellationRequested = false;
|
|
807
849
|
let runCancellationController = null;
|
|
808
850
|
let pluginConfirmController = null;
|
|
809
|
-
|
|
851
|
+
let scopedPersistenceCoordinator = null;
|
|
852
|
+
const nativeCompatibilityController = Modules.NativeCompatibilityController.create({
|
|
853
|
+
compatibility: CodexOverleafCompatibility,
|
|
854
|
+
nativeChannel,
|
|
855
|
+
gatedMethods: NATIVE_COMPATIBILITY_GATED_METHODS,
|
|
856
|
+
getExtensionCompatibilityMetadata,
|
|
857
|
+
fallbackNativeCompatibility,
|
|
858
|
+
getNativeCompatibilityClassification,
|
|
859
|
+
installCommand: INSTALL_COMMAND,
|
|
860
|
+
throwIfCancellationRequested: throwIfRunCancellationRequested,
|
|
861
|
+
getCurrentRunView: () => currentRunView,
|
|
862
|
+
appendRunEvent,
|
|
863
|
+
showPluginToast,
|
|
864
|
+
getPanel: () => panel,
|
|
865
|
+
ensurePanelOpen,
|
|
866
|
+
getPanelRendererInstance: () => panelRendererInstance,
|
|
867
|
+
setBadge: (...args) => PanelRenderer.setBadge(...args),
|
|
868
|
+
setDiagnosticsHealth,
|
|
869
|
+
getState: () => state,
|
|
870
|
+
localStorage: root.localStorage,
|
|
871
|
+
document,
|
|
872
|
+
navigator,
|
|
873
|
+
chromeApi: chrome,
|
|
874
|
+
onboardingTipStorageKey: ONBOARDING_TIP_STORAGE_KEY,
|
|
875
|
+
setTimeout: (...args) => root.setTimeout(...args),
|
|
876
|
+
tr,
|
|
877
|
+
tx
|
|
878
|
+
});
|
|
879
|
+
const updateIdleClient = Modules.UpdateIdle?.create({
|
|
810
880
|
document,
|
|
811
881
|
getBusyState: () => ({
|
|
812
882
|
run: Boolean(currentRunView),
|
|
@@ -878,6 +948,7 @@
|
|
|
878
948
|
});
|
|
879
949
|
|
|
880
950
|
async function init() {
|
|
951
|
+
await refreshAccountScopeId();
|
|
881
952
|
storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
|
|
882
953
|
state = normalizePanelState(await loadStoredState(), { restoreRunningRuns: true });
|
|
883
954
|
initializeRunQueueScheduler();
|
|
@@ -890,7 +961,7 @@
|
|
|
890
961
|
.catch(() => {});
|
|
891
962
|
}, 900);
|
|
892
963
|
ensurePanelOpen();
|
|
893
|
-
|
|
964
|
+
Modules.UpdateNotice?.mount?.(panel?.panelEl || panel, {
|
|
894
965
|
getLocale: () => state?.locale || 'en'
|
|
895
966
|
});
|
|
896
967
|
// v1.8.1: on a non-editor route the panel must NEVER first paint the
|
|
@@ -1554,7 +1625,7 @@
|
|
|
1554
1625
|
}
|
|
1555
1626
|
|
|
1556
1627
|
async function getRecentAuditLogsForCurrentProject(limit = 12) {
|
|
1557
|
-
const StorageDb =
|
|
1628
|
+
const StorageDb = Modules.StorageDb;
|
|
1558
1629
|
if (!StorageDb?.getAllByIndex) {
|
|
1559
1630
|
return [];
|
|
1560
1631
|
}
|
|
@@ -1673,20 +1744,36 @@
|
|
|
1673
1744
|
await pendingMirror.catch(() => {});
|
|
1674
1745
|
}
|
|
1675
1746
|
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
1747
|
const queuedInput = options.queuedInput || null;
|
|
1685
1748
|
const task = String(queuedInput?.text || state.task || '').trim();
|
|
1686
1749
|
if (!task) {
|
|
1687
1750
|
appendLog(tx('Enter a task first.', '请先输入任务。'));
|
|
1688
1751
|
return;
|
|
1689
1752
|
}
|
|
1753
|
+
const submittedPanelState = {
|
|
1754
|
+
mode: state.mode,
|
|
1755
|
+
providerId: state.providerId,
|
|
1756
|
+
model: state.model,
|
|
1757
|
+
reasoningEffort: state.reasoningEffort,
|
|
1758
|
+
speedTier: state.speedTier,
|
|
1759
|
+
autoRecompile: state.autoRecompile !== false,
|
|
1760
|
+
requireReviewing: state.requireReviewing === true,
|
|
1761
|
+
focusFiles: getActiveFocusFiles()
|
|
1762
|
+
};
|
|
1763
|
+
const submittedCustomInstructions = getCustomInstructionsForCurrentProject();
|
|
1764
|
+
const submittedSkillLoadingSettings = getSkillLoadingSettings();
|
|
1765
|
+
const submittedAttachments = getComposerAttachmentsForRun();
|
|
1766
|
+
const submittedSkillInvocation = getComposerSkillInvocationForRun();
|
|
1767
|
+
await providerSettingsCoordinator.ensureLoaded();
|
|
1768
|
+
const executionSnapshot = queuedInput
|
|
1769
|
+
? RunExecutionSnapshot.resolveForExecution(
|
|
1770
|
+
RunExecutionSnapshot.fromQueuePayload(queuedInput.payload),
|
|
1771
|
+
{ catalog: providerSettingsCoordinator.getCatalog() }
|
|
1772
|
+
)
|
|
1773
|
+
: captureCurrentExecutionSnapshot(submittedPanelState);
|
|
1774
|
+
// Freeze submitted run identity before the panel can change during the run.
|
|
1775
|
+
const submittedMode = executionSnapshot.mode;
|
|
1776
|
+
const submittedRequireReviewing = executionSnapshot.requireReviewing === true;
|
|
1690
1777
|
|
|
1691
1778
|
runCancellationRequested = false;
|
|
1692
1779
|
runCancellationController = new AbortController();
|
|
@@ -1694,24 +1781,26 @@
|
|
|
1694
1781
|
currentRunView = startRunView({
|
|
1695
1782
|
task,
|
|
1696
1783
|
mode: submittedMode,
|
|
1697
|
-
model:
|
|
1698
|
-
reasoningEffort:
|
|
1699
|
-
speedTier:
|
|
1784
|
+
model: executionSnapshot.model,
|
|
1785
|
+
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
1786
|
+
speedTier: executionSnapshot.speedTier,
|
|
1787
|
+
executionSnapshot,
|
|
1700
1788
|
attachments: submittedAttachments,
|
|
1701
1789
|
skillInvocation: submittedSkillInvocation,
|
|
1702
1790
|
queueItemId: queuedInput?.id || ''
|
|
1703
1791
|
});
|
|
1704
1792
|
const runSessionId = currentRunView.sessionId;
|
|
1705
|
-
if (queuedInput?.id) {
|
|
1706
|
-
await runQueueScheduler?.markExecuting(runSessionId, queuedInput.id, currentRunView.recordId);
|
|
1707
|
-
}
|
|
1708
1793
|
let runAuditDraft = null;
|
|
1709
1794
|
try {
|
|
1795
|
+
if (queuedInput?.id) {
|
|
1796
|
+
await runQueueScheduler?.markExecuting(runSessionId, queuedInput.id, currentRunView.recordId);
|
|
1797
|
+
}
|
|
1710
1798
|
runAuditDraft = await awaitRunStep(createAuditDraftForRun({
|
|
1711
1799
|
task,
|
|
1712
1800
|
sessionId: runSessionId,
|
|
1713
1801
|
mode: submittedMode,
|
|
1714
|
-
focusFiles:
|
|
1802
|
+
focusFiles: executionSnapshot.focusFiles,
|
|
1803
|
+
executionSnapshot
|
|
1715
1804
|
}));
|
|
1716
1805
|
announceCrossTabRunStart();
|
|
1717
1806
|
// Binary attachments belong to this submitted turn. The run record has
|
|
@@ -1726,13 +1815,13 @@
|
|
|
1726
1815
|
status: 'running',
|
|
1727
1816
|
detail: {
|
|
1728
1817
|
[tr('mode')]: formatModeLabel(submittedMode),
|
|
1729
|
-
[tx('Model', '模型')]:
|
|
1730
|
-
[tx('Reasoning effort', '推理强度')]:
|
|
1731
|
-
[tx('Speed', '速度')]:
|
|
1818
|
+
[tx('Model', '模型')]: executionSnapshot.model,
|
|
1819
|
+
[tx('Reasoning effort', '推理强度')]: executionSnapshot.reasoningEffort,
|
|
1820
|
+
[tx('Speed', '速度')]: executionSnapshot.speedTier,
|
|
1732
1821
|
[tx('Track required', '要求留痕')]: submittedRequireReviewing ? tx('yes', '是') : tx('no', '否'),
|
|
1733
1822
|
[tx('Skill', '技能')]: submittedSkillInvocation?.title || tr('noneValue'),
|
|
1734
1823
|
[tx('Attachments', '附件')]: submittedAttachments.map(attachment => attachment.name).join(listSeparator()) || tr('noneValue'),
|
|
1735
|
-
'@context': formatContextItems(
|
|
1824
|
+
'@context': formatContextItems(executionSnapshot.focusFiles)
|
|
1736
1825
|
}
|
|
1737
1826
|
});
|
|
1738
1827
|
if (submittedSkillInvocation?.id === 'skill-installer') {
|
|
@@ -1744,15 +1833,16 @@
|
|
|
1744
1833
|
submittedCustomInstructions,
|
|
1745
1834
|
submittedSkillLoadingSettings,
|
|
1746
1835
|
submittedAttachments,
|
|
1747
|
-
submittedSkillInvocation
|
|
1836
|
+
submittedSkillInvocation,
|
|
1837
|
+
executionSnapshot
|
|
1748
1838
|
}));
|
|
1749
1839
|
return;
|
|
1750
1840
|
}
|
|
1751
1841
|
appendRunEvent({
|
|
1752
1842
|
// @context prioritizes these files; complete project runs may still update related files.
|
|
1753
1843
|
title: tx(
|
|
1754
|
-
`This run will prioritize: ${formatContextItems(
|
|
1755
|
-
`本轮将优先处理:${formatContextItems(
|
|
1844
|
+
`This run will prioritize: ${formatContextItems(executionSnapshot.focusFiles)} (persists across turns; clear via +)`,
|
|
1845
|
+
`本轮将优先处理:${formatContextItems(executionSnapshot.focusFiles)}(跨轮保留,可在 + 中清除)`
|
|
1756
1846
|
),
|
|
1757
1847
|
status: 'completed'
|
|
1758
1848
|
});
|
|
@@ -1770,7 +1860,7 @@
|
|
|
1770
1860
|
return;
|
|
1771
1861
|
}
|
|
1772
1862
|
|
|
1773
|
-
let focusFiles =
|
|
1863
|
+
let focusFiles = [...executionSnapshot.focusFiles];
|
|
1774
1864
|
const warmStart = await awaitRunStep(resolveWarmRunStart({ focusFiles, mode: submittedMode }));
|
|
1775
1865
|
let project = warmStart.project || null;
|
|
1776
1866
|
let useExistingMirror = warmStart.useExistingMirror;
|
|
@@ -1802,7 +1892,7 @@
|
|
|
1802
1892
|
project,
|
|
1803
1893
|
snapshotWarnings,
|
|
1804
1894
|
focusFiles,
|
|
1805
|
-
isUsableProjectFileContent:
|
|
1895
|
+
isUsableProjectFileContent: Modules.ProjectFiles.isUsableProjectFileContent
|
|
1806
1896
|
});
|
|
1807
1897
|
let restrictToFocusFiles = runController.shouldRestrictWritebackToFocus({ focusFiles, focusedPartialSnapshot });
|
|
1808
1898
|
if (snapshotWarnings.blocking.length && !warmMirrorReuse.useExistingMirror && !focusedPartialSnapshot) {
|
|
@@ -1986,7 +2076,7 @@
|
|
|
1986
2076
|
const userChoice = await showThreadResumeFailedPrompt();
|
|
1987
2077
|
if (userChoice === 'new') {
|
|
1988
2078
|
updateSessionById(runSessionId, { codexThreadId: '' });
|
|
1989
|
-
const StorageDb =
|
|
2079
|
+
const StorageDb = Modules.StorageDb;
|
|
1990
2080
|
if (StorageDb) {
|
|
1991
2081
|
const record = await StorageDb.getRecord('sessions', runSessionId);
|
|
1992
2082
|
if (record) {
|
|
@@ -2169,8 +2259,10 @@
|
|
|
2169
2259
|
assistantMessage,
|
|
2170
2260
|
unsupportedChanges: response.result.unsupportedChanges || [],
|
|
2171
2261
|
mode: submittedMode,
|
|
2172
|
-
requireReviewing: submittedRequireReviewing
|
|
2262
|
+
requireReviewing: submittedRequireReviewing,
|
|
2263
|
+
autoRecompile: executionSnapshot.autoRecompile
|
|
2173
2264
|
});
|
|
2265
|
+
runSettlementPersistence.applyToCurrentRun(syncOutcome.settlement);
|
|
2174
2266
|
if (runCancellationRequested) {
|
|
2175
2267
|
throw createRunCancellationError();
|
|
2176
2268
|
}
|
|
@@ -2213,7 +2305,11 @@
|
|
|
2213
2305
|
// can catch early-return branches and unrecognized skip codes.
|
|
2214
2306
|
const postNavigationStatus = settleRunAfterNavigation(finishedRunRecord, syncOutcome);
|
|
2215
2307
|
if (postNavigationStatus) {
|
|
2216
|
-
persistPostNavigationRunStatus(
|
|
2308
|
+
await persistPostNavigationRunStatus(
|
|
2309
|
+
finishedRunRecord,
|
|
2310
|
+
postNavigationStatus,
|
|
2311
|
+
syncOutcome.settlement
|
|
2312
|
+
);
|
|
2217
2313
|
}
|
|
2218
2314
|
}
|
|
2219
2315
|
} catch (_settlementError) {
|
|
@@ -2242,7 +2338,7 @@
|
|
|
2242
2338
|
const returnedThreadId = response.result?.threadId || '';
|
|
2243
2339
|
if (returnedThreadId && returnedThreadId !== codexThreadId) {
|
|
2244
2340
|
updateSessionById(runSessionId, { codexThreadId: returnedThreadId });
|
|
2245
|
-
const StorageDb =
|
|
2341
|
+
const StorageDb = Modules.StorageDb;
|
|
2246
2342
|
if (StorageDb) {
|
|
2247
2343
|
const record = await StorageDb.getRecord('sessions', runSessionId);
|
|
2248
2344
|
if (record) {
|
|
@@ -2334,13 +2430,13 @@
|
|
|
2334
2430
|
setRunning(false);
|
|
2335
2431
|
nativeChannel.clearActiveRequest();
|
|
2336
2432
|
stopRunElapsedTick();
|
|
2337
|
-
currentRunView = null;
|
|
2338
|
-
runCancellationRequested = false;
|
|
2339
|
-
runCancellationController = null;
|
|
2340
2433
|
if (isExperimentalOtEnabled()) {
|
|
2341
2434
|
await resumeOtWarmMirror('run-settled');
|
|
2342
2435
|
}
|
|
2343
|
-
await
|
|
2436
|
+
await flushQueuedSaveState().catch(() => {});
|
|
2437
|
+
currentRunView = null;
|
|
2438
|
+
runCancellationRequested = false;
|
|
2439
|
+
runCancellationController = null;
|
|
2344
2440
|
activeTurnControl.release(settlement.requestId);
|
|
2345
2441
|
if (settlement.requestId) {
|
|
2346
2442
|
activeTurnControl.acknowledge(settlement.requestId).catch(() => {});
|
|
@@ -2357,7 +2453,8 @@
|
|
|
2357
2453
|
submittedCustomInstructions,
|
|
2358
2454
|
submittedSkillLoadingSettings,
|
|
2359
2455
|
submittedAttachments,
|
|
2360
|
-
submittedSkillInvocation
|
|
2456
|
+
submittedSkillInvocation,
|
|
2457
|
+
executionSnapshot
|
|
2361
2458
|
}) {
|
|
2362
2459
|
appendRunEvent({
|
|
2363
2460
|
title: tx('Starting the Codex skill installer.', '正在启动 Codex skill installer。'),
|
|
@@ -2369,9 +2466,9 @@
|
|
|
2369
2466
|
projectId: getCurrentProjectId(),
|
|
2370
2467
|
mode: submittedMode,
|
|
2371
2468
|
task,
|
|
2372
|
-
model:
|
|
2373
|
-
reasoningEffort:
|
|
2374
|
-
speedTier:
|
|
2469
|
+
model: executionSnapshot.model,
|
|
2470
|
+
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
2471
|
+
speedTier: executionSnapshot.speedTier,
|
|
2375
2472
|
session: state.session,
|
|
2376
2473
|
threadId: codexThreadId || undefined,
|
|
2377
2474
|
customInstructions: submittedCustomInstructions || undefined,
|
|
@@ -2463,7 +2560,7 @@
|
|
|
2463
2560
|
const returnedThreadId = response.result?.threadId || '';
|
|
2464
2561
|
if (returnedThreadId && returnedThreadId !== codexThreadId) {
|
|
2465
2562
|
updateSessionById(runSessionId, { codexThreadId: returnedThreadId });
|
|
2466
|
-
const StorageDb =
|
|
2563
|
+
const StorageDb = Modules.StorageDb;
|
|
2467
2564
|
if (StorageDb) {
|
|
2468
2565
|
const record = await StorageDb.getRecord('sessions', runSessionId);
|
|
2469
2566
|
if (record) {
|
|
@@ -2597,7 +2694,7 @@
|
|
|
2597
2694
|
});
|
|
2598
2695
|
}
|
|
2599
2696
|
|
|
2600
|
-
const runGuidanceController =
|
|
2697
|
+
const runGuidanceController = Modules.RunGuidanceController.create({
|
|
2601
2698
|
findRunRecord, appendRunEvent, getCurrentRunView: () => currentRunView,
|
|
2602
2699
|
renderRunEvent, cssEscape, bumpUnreadIfDetached
|
|
2603
2700
|
});
|
|
@@ -2612,11 +2709,10 @@
|
|
|
2612
2709
|
readInputs: readPanelInputs,
|
|
2613
2710
|
getTask: () => panel?.querySelector('[data-task]')?.value || '',
|
|
2614
2711
|
hasAttachments: () => getComposerAttachmentsForRun().length > 0,
|
|
2615
|
-
capturePayload: () => (
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
focusFiles: getActiveFocusFiles()
|
|
2712
|
+
capturePayload: () => RunExecutionSnapshot.toQueuePayload(captureCurrentExecutionSnapshot()),
|
|
2713
|
+
prepareClaim: () => providerSettingsCoordinator.ensureLoaded(),
|
|
2714
|
+
resolveExecutionSnapshot: snapshot => RunExecutionSnapshot.resolveForExecution(snapshot, {
|
|
2715
|
+
catalog: providerSettingsCoordinator.getCatalog()
|
|
2620
2716
|
}),
|
|
2621
2717
|
clearTask: () => {
|
|
2622
2718
|
panel.querySelector('[data-task]').value = '';
|
|
@@ -2624,15 +2720,11 @@
|
|
|
2624
2720
|
autosizeTaskTextarea();
|
|
2625
2721
|
syncComposerSendAvailability();
|
|
2626
2722
|
},
|
|
2627
|
-
applyQueuedInput: item => {
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
requireReviewing: item.payload?.requireReviewing !== false,
|
|
2633
|
-
focusFiles: item.payload?.focusFiles || []
|
|
2634
|
-
});
|
|
2635
|
-
applyStateToPanel();
|
|
2723
|
+
applyQueuedInput: async item => {
|
|
2724
|
+
RunExecutionSnapshot.resolveForExecution(
|
|
2725
|
+
RunExecutionSnapshot.fromQueuePayload(item.payload),
|
|
2726
|
+
{ catalog: providerSettingsCoordinator.getCatalog() }
|
|
2727
|
+
);
|
|
2636
2728
|
},
|
|
2637
2729
|
run: item => runTask({ queuedInput: item }),
|
|
2638
2730
|
sendSteer: params => sendBackgroundNative({ method: 'codex.steer', params }),
|
|
@@ -2643,13 +2735,33 @@
|
|
|
2643
2735
|
removeGuidance: runGuidanceController.remove,
|
|
2644
2736
|
toast: (message, status) => showPluginToast(message, { status }),
|
|
2645
2737
|
tr,
|
|
2646
|
-
save:
|
|
2647
|
-
saveSoon:
|
|
2738
|
+
save: persistenceOptions => saveState(persistenceOptions),
|
|
2739
|
+
saveSoon: persistenceOptions => saveStateSoon(120, persistenceOptions),
|
|
2648
2740
|
randomUUID: () => crypto.randomUUID()
|
|
2649
2741
|
});
|
|
2650
2742
|
runQueueScheduler = runInputCoordinator.scheduler;
|
|
2651
2743
|
}
|
|
2652
2744
|
|
|
2745
|
+
function captureCurrentExecutionSnapshot(submitted = {}) {
|
|
2746
|
+
const requestedProviderId = submitted.providerId || state?.providerId || 'builtin';
|
|
2747
|
+
const selection = providerSettingsCoordinator.getRunSelection(requestedProviderId);
|
|
2748
|
+
const providerId = selection?.providerId || requestedProviderId;
|
|
2749
|
+
return RunExecutionSnapshot.capture({
|
|
2750
|
+
mode: submitted.mode ?? state?.mode,
|
|
2751
|
+
providerId,
|
|
2752
|
+
providerRevision: selection?.providerRevision,
|
|
2753
|
+
model: submitted.model ?? state?.model,
|
|
2754
|
+
reasoningEffort: submitted.reasoningEffort ?? state?.reasoningEffort,
|
|
2755
|
+
speedTier: submitted.speedTier ?? state?.speedTier,
|
|
2756
|
+
autoRecompile: submitted.autoRecompile ?? state?.autoRecompile !== false,
|
|
2757
|
+
requireReviewing: submitted.requireReviewing ?? state?.requireReviewing === true,
|
|
2758
|
+
focusFiles: submitted.focusFiles ?? getActiveFocusFiles()
|
|
2759
|
+
}, {
|
|
2760
|
+
source: 'submitted',
|
|
2761
|
+
requireProviderRevision: providerId !== 'builtin'
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2653
2765
|
function queueComposerInput() {
|
|
2654
2766
|
return runInputCoordinator?.queueComposerInput();
|
|
2655
2767
|
}
|
|
@@ -2716,11 +2828,7 @@
|
|
|
2716
2828
|
}
|
|
2717
2829
|
|
|
2718
2830
|
function cancelActivePageBridgeRequests() {
|
|
2719
|
-
|
|
2720
|
-
activePageBridgeCancellationHandlers.clear();
|
|
2721
|
-
for (const cancel of handlers) {
|
|
2722
|
-
cancel();
|
|
2723
|
-
}
|
|
2831
|
+
pageBridgeClient.cancelActiveRequests();
|
|
2724
2832
|
}
|
|
2725
2833
|
|
|
2726
2834
|
// Recovery path for the case where a previous Codex run leaked the
|
|
@@ -2884,9 +2992,11 @@
|
|
|
2884
2992
|
skillInvocation,
|
|
2885
2993
|
submittedMode
|
|
2886
2994
|
} = {}) {
|
|
2995
|
+
const executionSnapshot = currentRunView?.executionSnapshot || captureCurrentExecutionSnapshot();
|
|
2996
|
+
const submittedState = RunExecutionSnapshot.applyToState(state, executionSnapshot);
|
|
2887
2997
|
const params = runController.buildCodexRunParams({
|
|
2888
2998
|
currentProjectId: getCurrentProjectId(),
|
|
2889
|
-
state,
|
|
2999
|
+
state: submittedState,
|
|
2890
3000
|
task,
|
|
2891
3001
|
project,
|
|
2892
3002
|
useExistingMirror,
|
|
@@ -2913,17 +3023,17 @@
|
|
|
2913
3023
|
...params,
|
|
2914
3024
|
clientRunId: currentRunView?.recordId || '',
|
|
2915
3025
|
clientSessionId: currentRunView?.sessionId || '',
|
|
2916
|
-
providerSelection:
|
|
3026
|
+
providerSelection: RunExecutionSnapshot.toProviderSelection(executionSnapshot)
|
|
2917
3027
|
};
|
|
2918
3028
|
}
|
|
2919
3029
|
|
|
2920
3030
|
async function sendTrackedCodexRun(params) {
|
|
2921
|
-
const compatibilityGate = await
|
|
3031
|
+
const compatibilityGate = await nativeCompatibilityController.ensureForMethod('codex.run');
|
|
2922
3032
|
if (!compatibilityGate.ok) {
|
|
2923
3033
|
return compatibilityGate.response;
|
|
2924
3034
|
}
|
|
2925
|
-
|
|
2926
|
-
const tracked = nativeChannel.sendNativeTracked(
|
|
3035
|
+
throwIfRunCancellationRequested();
|
|
3036
|
+
const tracked = nativeChannel.sendNativeTracked(nativeCompatibilityController.attachEvidence({
|
|
2927
3037
|
method: 'codex.run',
|
|
2928
3038
|
params
|
|
2929
3039
|
}, compatibilityGate.compatibility));
|
|
@@ -3003,19 +3113,23 @@
|
|
|
3003
3113
|
}
|
|
3004
3114
|
|
|
3005
3115
|
async function createAuditDraftForRun(input = {}) {
|
|
3006
|
-
const StorageDb =
|
|
3116
|
+
const StorageDb = Modules.StorageDb;
|
|
3007
3117
|
if (!StorageDb || !AuditRecords) {
|
|
3008
3118
|
return null;
|
|
3009
3119
|
}
|
|
3120
|
+
const executionSnapshot = input.executionSnapshot
|
|
3121
|
+
|| currentRunView?.executionSnapshot
|
|
3122
|
+
|| captureCurrentExecutionSnapshot();
|
|
3010
3123
|
const draft = AuditRecords.buildAuditDraftRecord({
|
|
3011
3124
|
projectId: getCurrentProjectId(),
|
|
3012
3125
|
sessionId: input.sessionId || '',
|
|
3013
3126
|
turnId: currentRunView?.recordId || '',
|
|
3014
|
-
mode: input.mode ||
|
|
3015
|
-
...providerSettingsCoordinator.getProviderSnapshot(
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3127
|
+
mode: input.mode || executionSnapshot.mode,
|
|
3128
|
+
...providerSettingsCoordinator.getProviderSnapshot(executionSnapshot.providerId),
|
|
3129
|
+
providerRevision: executionSnapshot.providerRevision,
|
|
3130
|
+
model: executionSnapshot.model,
|
|
3131
|
+
reasoningEffort: executionSnapshot.reasoningEffort,
|
|
3132
|
+
speedTier: executionSnapshot.speedTier,
|
|
3019
3133
|
task: input.task || '',
|
|
3020
3134
|
focusFiles: input.focusFiles || [],
|
|
3021
3135
|
selectedSkillIds: input.selectedSkillIds || []
|
|
@@ -3037,7 +3151,7 @@
|
|
|
3037
3151
|
}
|
|
3038
3152
|
|
|
3039
3153
|
async function finalizeAuditDraftPartial(draft, updates = {}) {
|
|
3040
|
-
const StorageDb =
|
|
3154
|
+
const StorageDb = Modules.StorageDb;
|
|
3041
3155
|
if (!StorageDb || !draft) {
|
|
3042
3156
|
return draft;
|
|
3043
3157
|
}
|
|
@@ -3048,7 +3162,7 @@
|
|
|
3048
3162
|
}
|
|
3049
3163
|
|
|
3050
3164
|
async function finalizeAuditRecord(draft, updates = {}) {
|
|
3051
|
-
const StorageDb =
|
|
3165
|
+
const StorageDb = Modules.StorageDb;
|
|
3052
3166
|
if (!StorageDb || !AuditRecords || !draft) {
|
|
3053
3167
|
return null;
|
|
3054
3168
|
}
|
|
@@ -3843,8 +3957,8 @@
|
|
|
3843
3957
|
}
|
|
3844
3958
|
|
|
3845
3959
|
function normalizeSafeProjectPath(value) {
|
|
3846
|
-
if (
|
|
3847
|
-
return
|
|
3960
|
+
if (Modules.ProjectFiles?.normalizeSafeProjectPath) {
|
|
3961
|
+
return Modules.ProjectFiles.normalizeSafeProjectPath(value);
|
|
3848
3962
|
}
|
|
3849
3963
|
return String(value || '')
|
|
3850
3964
|
.replace(/\s+/g, ' ')
|
|
@@ -4123,6 +4237,7 @@
|
|
|
4123
4237
|
// `runProjectId` is that it survives navigation; `activeProjectId` does not.
|
|
4124
4238
|
let activeProjectId = null;
|
|
4125
4239
|
let lastSpaPathname = '';
|
|
4240
|
+
let spaRouteGeneration = 0;
|
|
4126
4241
|
|
|
4127
4242
|
function cancelPendingWritebacks(prevProjectId) {
|
|
4128
4243
|
// The codebase does not maintain a separate pending-writeback queue;
|
|
@@ -4172,6 +4287,17 @@
|
|
|
4172
4287
|
});
|
|
4173
4288
|
}
|
|
4174
4289
|
|
|
4290
|
+
function getScopedPersistenceCoordinator() {
|
|
4291
|
+
if (scopedPersistenceCoordinator) return scopedPersistenceCoordinator;
|
|
4292
|
+
scopedPersistenceCoordinator = Modules.ScopedPersistenceCoordinator?.createController({
|
|
4293
|
+
Transaction: Modules.ScopedPersistenceTransaction,
|
|
4294
|
+
chromeApi: chrome,
|
|
4295
|
+
sessionStorage: window.sessionStorage,
|
|
4296
|
+
getAccountScopeId: () => window.codexOverleafDeriveAccountScopeId?.() || ''
|
|
4297
|
+
});
|
|
4298
|
+
return scopedPersistenceCoordinator;
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4175
4301
|
async function reloadProjectRunHistory(newProjectId) {
|
|
4176
4302
|
// Rebind the storage key to the new project and reload the panel state
|
|
4177
4303
|
// from chrome.storage.local so the per-project session list reflects
|
|
@@ -4265,21 +4391,34 @@
|
|
|
4265
4391
|
} catch (_error) { /* swallow */ }
|
|
4266
4392
|
}
|
|
4267
4393
|
|
|
4268
|
-
function onSpaRouteChange() {
|
|
4269
|
-
const
|
|
4394
|
+
async function onSpaRouteChange() {
|
|
4395
|
+
const generation = ++spaRouteGeneration;
|
|
4396
|
+
const url = new URL(window.location.href);
|
|
4397
|
+
const pathname = url.pathname;
|
|
4398
|
+
const previousProjectId = activeProjectId;
|
|
4399
|
+
const previousAccountScopeId = cachedAccountScopeId;
|
|
4400
|
+
const nextProjectId = isProjectEditorRoute(url)
|
|
4401
|
+
? url.pathname.match(/^\/project\/([^/?#]+)/)[1]
|
|
4402
|
+
: null;
|
|
4403
|
+
if (nextProjectId !== activeProjectId) {
|
|
4404
|
+
leaveActiveProject(nextProjectId);
|
|
4405
|
+
}
|
|
4270
4406
|
// Recompute the account scope on every route change (spec §5.2 per-
|
|
4271
4407
|
// project derivation: account menu is global chrome and should be
|
|
4272
4408
|
// readable on per-project URLs too).
|
|
4273
|
-
refreshAccountScopeId()
|
|
4409
|
+
await refreshAccountScopeId();
|
|
4410
|
+
if (generation !== spaRouteGeneration || pathname !== window.location.pathname) {
|
|
4411
|
+
return;
|
|
4412
|
+
}
|
|
4274
4413
|
if (isProjectEditorRoute(url)) {
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4414
|
+
if (
|
|
4415
|
+
nextProjectId !== previousProjectId
|
|
4416
|
+
|| cachedAccountScopeId !== previousAccountScopeId
|
|
4417
|
+
) {
|
|
4418
|
+
enterProject(nextProjectId);
|
|
4279
4419
|
}
|
|
4280
4420
|
renderPerProjectVariant();
|
|
4281
4421
|
} else {
|
|
4282
|
-
leaveActiveProject(null);
|
|
4283
4422
|
// Variant render is async (it awaits the IndexedDB query); the hook
|
|
4284
4423
|
// does not await — subsequent renders pick up the rebound state.
|
|
4285
4424
|
renderRecentProjectsVariant().catch(() => { /* swallow */ });
|
|
@@ -4303,11 +4442,9 @@
|
|
|
4303
4442
|
return;
|
|
4304
4443
|
}
|
|
4305
4444
|
lastSpaPathname = next;
|
|
4306
|
-
|
|
4307
|
-
onSpaRouteChange();
|
|
4308
|
-
} catch (_error) {
|
|
4445
|
+
onSpaRouteChange().catch(() => {
|
|
4309
4446
|
// Route-hook failures must never crash the page.
|
|
4310
|
-
}
|
|
4447
|
+
});
|
|
4311
4448
|
};
|
|
4312
4449
|
const wrap = name => {
|
|
4313
4450
|
const original = window.history[name];
|
|
@@ -4327,103 +4464,16 @@
|
|
|
4327
4464
|
lastSpaPathname = window.location.pathname;
|
|
4328
4465
|
}
|
|
4329
4466
|
|
|
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
4467
|
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';
|
|
4468
|
+
return WritebackSettlement.settlePostNavigation({
|
|
4469
|
+
run,
|
|
4470
|
+
runResult,
|
|
4471
|
+
navigation: { occurred: activeProjectId !== run.runProjectId }
|
|
4373
4472
|
});
|
|
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.
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4406
4475
|
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;
|
|
4476
|
+
return WritebackSettlement.collectRunResultSkipped(syncOutcome);
|
|
4427
4477
|
}
|
|
4428
4478
|
|
|
4429
4479
|
// Persist the post-navigation status to the run's original
|
|
@@ -4438,50 +4488,73 @@
|
|
|
4438
4488
|
// `reloadProjectRunHistory`), so the in-memory mutation would not
|
|
4439
4489
|
// reach the right record. Go directly through StorageDb to update
|
|
4440
4490
|
// the original project's session record by id.
|
|
4441
|
-
async function persistPostNavigationRunStatus(run, postNavigationStatus) {
|
|
4491
|
+
async function persistPostNavigationRunStatus(run, postNavigationStatus, settlementResult) {
|
|
4442
4492
|
if (!run || !postNavigationStatus) {
|
|
4443
|
-
return;
|
|
4493
|
+
return { ok: false, reason: 'invalid_post_navigation_settlement' };
|
|
4444
4494
|
}
|
|
4445
4495
|
const postNavigationStatusText = getPostNavigationRunStatusText(postNavigationStatus);
|
|
4446
|
-
run.
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4496
|
+
Object.assign(run, WritebackSettlement.applySettlementTransition(
|
|
4497
|
+
run,
|
|
4498
|
+
WritebackSettlement.transitionPostNavigationStatus({
|
|
4499
|
+
status: postNavigationStatus,
|
|
4500
|
+
statusText: postNavigationStatusText,
|
|
4501
|
+
finishedAt: run.finishedAt || new Date().toISOString()
|
|
4502
|
+
})
|
|
4503
|
+
));
|
|
4504
|
+
if (
|
|
4505
|
+
currentRunView
|
|
4506
|
+
&& currentRunView.runProjectId === activeProjectId
|
|
4507
|
+
&& currentRunView.runAccountScopeId === cachedAccountScopeId
|
|
4508
|
+
) {
|
|
4450
4509
|
// Same project still active — the run record is reachable through the
|
|
4451
4510
|
// normal state and the regular save path will persist it.
|
|
4452
4511
|
saveStateSoon();
|
|
4453
|
-
return;
|
|
4512
|
+
return { ok: true, lane: 'active_project' };
|
|
4454
4513
|
}
|
|
4455
4514
|
// Different project. Update the original session's run record directly.
|
|
4456
4515
|
try {
|
|
4457
|
-
const StorageDb = window.CodexOverleafStorageDb;
|
|
4458
|
-
if (!StorageDb) {
|
|
4459
|
-
return;
|
|
4460
|
-
}
|
|
4461
4516
|
const sessionId = currentRunView ? currentRunView.sessionId : '';
|
|
4462
4517
|
if (!sessionId) {
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
return;
|
|
4518
|
+
throw Object.assign(new Error('Post-navigation settlement has no original session identity.'), {
|
|
4519
|
+
code: 'post_navigation_settlement_persistence_failed',
|
|
4520
|
+
reason: 'session_scope_unavailable'
|
|
4521
|
+
});
|
|
4468
4522
|
}
|
|
4469
|
-
const
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4523
|
+
const persistence = Modules.PostNavigationSettlementPersistence;
|
|
4524
|
+
if (!persistence || typeof persistence.persistRequired !== 'function') {
|
|
4525
|
+
throw Object.assign(new Error('Post-navigation settlement persistence is unavailable.'), {
|
|
4526
|
+
code: 'post_navigation_settlement_persistence_failed',
|
|
4527
|
+
reason: 'persistence_module_unavailable'
|
|
4528
|
+
});
|
|
4473
4529
|
}
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4530
|
+
return await persistence.persistRequired({
|
|
4531
|
+
StorageDb: Modules.StorageDb,
|
|
4532
|
+
PersistenceCoordinator: getScopedPersistenceCoordinator(),
|
|
4533
|
+
WritebackSettlement,
|
|
4534
|
+
projectId: currentRunView?.runProjectId || '',
|
|
4535
|
+
accountScopeId: currentRunView?.runAccountScopeId || '',
|
|
4536
|
+
sessionId,
|
|
4537
|
+
runId: run.id,
|
|
4538
|
+
status: postNavigationStatus,
|
|
4539
|
+
statusText: postNavigationStatusText,
|
|
4540
|
+
finishedAt: run.finishedAt,
|
|
4541
|
+
settlementResult
|
|
4542
|
+
});
|
|
4543
|
+
} catch (error) {
|
|
4544
|
+
const reason = String(error?.reason || error?.code || 'unknown_failure');
|
|
4545
|
+
run.postNavigationPersistenceFailure = {
|
|
4546
|
+
code: 'post_navigation_settlement_persistence_failed',
|
|
4547
|
+
reason,
|
|
4548
|
+
at: new Date().toISOString()
|
|
4549
|
+
};
|
|
4550
|
+
appendPlainLog(tx(
|
|
4551
|
+
`Could not persist the completed run to its original project (${reason}). Reopen that project to recover the interrupted record.`,
|
|
4552
|
+
`无法将已完成运行保存到原项目(${reason})。请重新打开该项目以恢复中断记录。`
|
|
4553
|
+
));
|
|
4554
|
+
return {
|
|
4555
|
+
ok: false,
|
|
4556
|
+
reason
|
|
4557
|
+
};
|
|
4485
4558
|
}
|
|
4486
4559
|
}
|
|
4487
4560
|
|
|
@@ -4510,7 +4583,7 @@
|
|
|
4510
4583
|
getCachedAccountScopeId: () => cachedAccountScopeId,
|
|
4511
4584
|
// T5 surfaces: variant renderer + helpers. Tests drive
|
|
4512
4585
|
// `renderRecentProjectsVariant` against a stubbed `panel` and
|
|
4513
|
-
// `
|
|
4586
|
+
// `Modules.StorageDb` to assert layout / row behavior.
|
|
4514
4587
|
renderRecentProjectsVariant,
|
|
4515
4588
|
renderRecentProjectRow,
|
|
4516
4589
|
renderStatusBadge,
|
|
@@ -5225,7 +5298,7 @@
|
|
|
5225
5298
|
}
|
|
5226
5299
|
|
|
5227
5300
|
const mirrorStatus = await getMirrorFreshness();
|
|
5228
|
-
const mirrorHealth =
|
|
5301
|
+
const mirrorHealth = Modules.MirrorHealth.classifyMirrorHealth(mirrorStatus);
|
|
5229
5302
|
if (!mirrorStatus?.exists || !mirrorHealth.reusable) {
|
|
5230
5303
|
return { useExistingMirror: false, reason: 'mirror_not_fresh', mirrorStatus };
|
|
5231
5304
|
}
|
|
@@ -5254,7 +5327,7 @@
|
|
|
5254
5327
|
return candidates
|
|
5255
5328
|
.filter(file =>
|
|
5256
5329
|
typeof file.content === 'string' &&
|
|
5257
|
-
|
|
5330
|
+
Modules.ProjectFiles.isUsableProjectFileContent(file.content) &&
|
|
5258
5331
|
file.path &&
|
|
5259
5332
|
!seen.has(file.path) &&
|
|
5260
5333
|
seen.add(file.path)
|
|
@@ -5363,7 +5436,7 @@
|
|
|
5363
5436
|
blocking.push('Full project snapshot was not captured.');
|
|
5364
5437
|
}
|
|
5365
5438
|
|
|
5366
|
-
const unusable = textFiles.filter(file => !
|
|
5439
|
+
const unusable = textFiles.filter(file => !Modules.ProjectFiles.isUsableProjectFileContent(file.content));
|
|
5367
5440
|
if (unusable.length === textFiles.length) {
|
|
5368
5441
|
blocking.push('Captured file contents are empty or still loading.');
|
|
5369
5442
|
} else if (unusable.length) {
|
|
@@ -5401,545 +5474,84 @@
|
|
|
5401
5474
|
}
|
|
5402
5475
|
|
|
5403
5476
|
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
|
-
})();
|
|
5477
|
+
return nativeCompatibilityController.sendNative(payload);
|
|
5412
5478
|
}
|
|
5413
5479
|
|
|
5414
5480
|
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
|
-
}
|
|
5481
|
+
return nativeCompatibilityController.sendBackgroundNative(payload);
|
|
5498
5482
|
}
|
|
5499
5483
|
|
|
5500
5484
|
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 }));
|
|
5485
|
+
return nativeCompatibilityController.refreshBadge();
|
|
5567
5486
|
}
|
|
5568
5487
|
|
|
5569
5488
|
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();
|
|
5489
|
+
return nativeCompatibilityController.showUpdateGuidance(compatibility);
|
|
5654
5490
|
}
|
|
5655
5491
|
|
|
5656
5492
|
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
|
-
}
|
|
5493
|
+
nativeCompatibilityController.scheduleFirstUseOnboardingTip();
|
|
5721
5494
|
}
|
|
5722
5495
|
|
|
5723
5496
|
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
|
-
});
|
|
5497
|
+
return pageBridgeClient.call(method, params);
|
|
5816
5498
|
}
|
|
5817
5499
|
|
|
5818
5500
|
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
|
-
}
|
|
5501
|
+
return pageBridgeClient.resolveTimeoutMs(method);
|
|
5881
5502
|
}
|
|
5882
5503
|
|
|
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
|
-
});
|
|
5504
|
+
async function loadStoredState() {
|
|
5505
|
+
const projectId = getCurrentProjectId();
|
|
5506
|
+
const persistenceCoordinator = getScopedPersistenceCoordinator();
|
|
5507
|
+
const hydrationView = projectId && persistenceCoordinator
|
|
5508
|
+
? await persistenceCoordinator.beginHydration(projectId)
|
|
5509
|
+
: null;
|
|
5510
|
+
const hydrationAccountScopeId = hydrationView?.accountScopeId || cachedAccountScopeId || '';
|
|
5511
|
+
const loaded = await loadStoredStateForProject(hydrationAccountScopeId);
|
|
5512
|
+
if (
|
|
5513
|
+
hydrationView
|
|
5514
|
+
&& persistenceCoordinator
|
|
5515
|
+
&& !persistenceCoordinator.isCurrent(hydrationView)
|
|
5516
|
+
) {
|
|
5517
|
+
throw Object.assign(new Error('Scoped hydration was superseded.'), {
|
|
5518
|
+
code: 'stale_hydration'
|
|
5519
|
+
});
|
|
5520
|
+
}
|
|
5521
|
+
return loaded;
|
|
5915
5522
|
}
|
|
5916
5523
|
|
|
5917
|
-
|
|
5918
|
-
const
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5524
|
+
function buildScopedFallbackStorageKey(baseKey, accountScopeId) {
|
|
5525
|
+
const account = typeof accountScopeId === 'string' ? accountScopeId.trim() : '';
|
|
5526
|
+
if (!account) {
|
|
5527
|
+
throw Object.assign(new Error('Scoped fallback storage requires an account identity.'), {
|
|
5528
|
+
code: 'account_scope_unavailable'
|
|
5529
|
+
});
|
|
5923
5530
|
}
|
|
5531
|
+
return `${baseKey}:account:${encodeURIComponent(account)}`;
|
|
5924
5532
|
}
|
|
5925
5533
|
|
|
5926
|
-
function
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5534
|
+
function isScopedPersistenceIdentityFailure(error) {
|
|
5535
|
+
return [
|
|
5536
|
+
'account_scope_unavailable',
|
|
5537
|
+
'scoped_claim_unavailable',
|
|
5538
|
+
'stale_hydration',
|
|
5539
|
+
'stale_view'
|
|
5540
|
+
].includes(error?.code);
|
|
5933
5541
|
}
|
|
5934
5542
|
|
|
5935
|
-
async function
|
|
5543
|
+
async function loadStoredStateForProject(accountScopeId = cachedAccountScopeId || '') {
|
|
5936
5544
|
try {
|
|
5937
|
-
const StorageDb =
|
|
5938
|
-
const Migration =
|
|
5545
|
+
const StorageDb = Modules.StorageDb;
|
|
5546
|
+
const Migration = Modules.StorageMigration;
|
|
5939
5547
|
const projectId = getCurrentProjectId();
|
|
5940
5548
|
const legacyKey = storageKey;
|
|
5941
5549
|
|
|
5942
|
-
const { prefs, sessions, activeSessionId } = await Migration.runMigrationIfNeeded(
|
|
5550
|
+
const { prefs, sessions, activeSessionId } = await Migration.runMigrationIfNeeded(
|
|
5551
|
+
projectId,
|
|
5552
|
+
legacyKey,
|
|
5553
|
+
accountScopeId
|
|
5554
|
+
);
|
|
5943
5555
|
|
|
5944
5556
|
return {
|
|
5945
5557
|
model: prefs.model || '', providerId: prefs.providerId || 'builtin',
|
|
@@ -5958,6 +5570,7 @@
|
|
|
5958
5570
|
panelWidth: prefs.panelWidth || PANEL_DEFAULT_WIDTH,
|
|
5959
5571
|
sessions: sessions.map(session => ({
|
|
5960
5572
|
id: session.id,
|
|
5573
|
+
accountScopeId: session.accountScopeId || accountScopeId,
|
|
5961
5574
|
title: session.title || '',
|
|
5962
5575
|
titleSource: session.titleSource || 'auto',
|
|
5963
5576
|
focusFiles: session.focusFiles || [],
|
|
@@ -5980,13 +5593,16 @@
|
|
|
5980
5593
|
runs: []
|
|
5981
5594
|
};
|
|
5982
5595
|
} catch (error) {
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
const stored = await chrome.storage.local.get(
|
|
5988
|
-
const fallback = stored[
|
|
5989
|
-
if (
|
|
5596
|
+
if (!accountScopeId || isScopedPersistenceIdentityFailure(error)) {
|
|
5597
|
+
throw error;
|
|
5598
|
+
}
|
|
5599
|
+
const fallbackKey = buildScopedFallbackStorageKey(storageKey, accountScopeId);
|
|
5600
|
+
const stored = await chrome.storage.local.get([fallbackKey]);
|
|
5601
|
+
const fallback = stored[fallbackKey] || {};
|
|
5602
|
+
if (
|
|
5603
|
+
fallback?.__codexOverleafCompactFallback === true
|
|
5604
|
+
&& fallback.__codexOverleafFallbackAccountScopeId === accountScopeId
|
|
5605
|
+
) {
|
|
5990
5606
|
return {
|
|
5991
5607
|
mode: fallback.mode || '', providerId: fallback.providerId || 'builtin',
|
|
5992
5608
|
model: fallback.model || '',
|
|
@@ -6007,7 +5623,7 @@
|
|
|
6007
5623
|
runs: []
|
|
6008
5624
|
};
|
|
6009
5625
|
}
|
|
6010
|
-
|
|
5626
|
+
throw error;
|
|
6011
5627
|
}
|
|
6012
5628
|
}
|
|
6013
5629
|
|
|
@@ -6027,8 +5643,8 @@
|
|
|
6027
5643
|
|
|
6028
5644
|
async function saveState(options) {
|
|
6029
5645
|
try {
|
|
6030
|
-
const StorageDb =
|
|
6031
|
-
const Migration =
|
|
5646
|
+
const StorageDb = Modules.StorageDb;
|
|
5647
|
+
const Migration = Modules.StorageMigration;
|
|
6032
5648
|
// Welcome-panel + write-guard:
|
|
6033
5649
|
// post-navigation persistence gate. The normal URL-derived projectId
|
|
6034
5650
|
// belongs to the route the user is *currently looking at*. When a run
|
|
@@ -6074,86 +5690,85 @@
|
|
|
6074
5690
|
compactState.experimentalOtByProject = state.experimentalOtByProject;
|
|
6075
5691
|
compactState.customInstructionsByProject = state.customInstructionsByProject;
|
|
6076
5692
|
compactState.governanceRulesByProject = state.governanceRulesByProject;
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
prefs.activeSessionByProject = StorageDb.buildActiveSessionByProject(
|
|
6088
|
-
latestPrefs?.activeSessionByProject || {},
|
|
5693
|
+
const stateSnapshot = typeof structuredClone === 'function'
|
|
5694
|
+
? structuredClone(state)
|
|
5695
|
+
: JSON.parse(JSON.stringify(state));
|
|
5696
|
+
const compactStateSnapshot = typeof structuredClone === 'function'
|
|
5697
|
+
? structuredClone(compactState)
|
|
5698
|
+
: JSON.parse(JSON.stringify(compactState));
|
|
5699
|
+
|
|
5700
|
+
const persistScopedState = persistenceContext => Modules.ScopedPersistenceCoordinator.persistPanelState({
|
|
5701
|
+
state: stateSnapshot,
|
|
5702
|
+
compactState: compactStateSnapshot,
|
|
6089
5703
|
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
5704
|
deletedSessionIds: options?.deletedSessionIds,
|
|
6135
|
-
|
|
5705
|
+
Migration,
|
|
5706
|
+
StorageDb,
|
|
5707
|
+
SessionPersistence: Modules.SessionPersistence,
|
|
5708
|
+
normalizeExperimentalOtByProject,
|
|
5709
|
+
normalizeGovernanceRulesByProject,
|
|
5710
|
+
normalizeCustomInstructionsByProject,
|
|
5711
|
+
persistenceContext
|
|
6136
5712
|
});
|
|
5713
|
+
|
|
5714
|
+
const persistenceCoordinator = getScopedPersistenceCoordinator();
|
|
5715
|
+
if (!persistenceCoordinator) {
|
|
5716
|
+
throw Object.assign(new Error('Scoped persistence coordinator is unavailable.'), {
|
|
5717
|
+
code: 'scoped_persistence_unavailable'
|
|
5718
|
+
});
|
|
5719
|
+
} else {
|
|
5720
|
+
const committed = await persistenceCoordinator.commit(
|
|
5721
|
+
projectId,
|
|
5722
|
+
{
|
|
5723
|
+
detached: Boolean(projectIdOverride),
|
|
5724
|
+
rebase: true,
|
|
5725
|
+
queueMutation: options?.queueMutation,
|
|
5726
|
+
queueMutations: options?.queueMutations,
|
|
5727
|
+
sessionTombstones: options?.deletedSessionIds
|
|
5728
|
+
},
|
|
5729
|
+
persistScopedState
|
|
5730
|
+
);
|
|
5731
|
+
if (!committed.ok && (
|
|
5732
|
+
options?.queueMutation
|
|
5733
|
+
|| options?.queueMutations?.length
|
|
5734
|
+
|| committed.reason !== 'stale_view'
|
|
5735
|
+
)) {
|
|
5736
|
+
throw Object.assign(new Error(`Scoped persistence failed: ${committed.reason}`), {
|
|
5737
|
+
code: committed.reason
|
|
5738
|
+
});
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
6137
5741
|
} catch (error) {
|
|
6138
|
-
|
|
5742
|
+
if (
|
|
5743
|
+
error?.code === 'account_scope_unavailable' &&
|
|
5744
|
+
!options?.queueMutation &&
|
|
5745
|
+
!(Array.isArray(options?.queueMutations) && options.queueMutations.length)
|
|
5746
|
+
) {
|
|
5747
|
+
return;
|
|
5748
|
+
}
|
|
5749
|
+
if (options?.queueMutation || options?.queueMutations?.length) {
|
|
5750
|
+
throw error;
|
|
5751
|
+
}
|
|
5752
|
+
let fallbackError = null;
|
|
6139
5753
|
try {
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
5754
|
+
const accountScopeId = cachedAccountScopeId || '';
|
|
5755
|
+
const fallbackKey = buildScopedFallbackStorageKey(storageKey, accountScopeId);
|
|
5756
|
+
await chrome.storage.local.set({
|
|
5757
|
+
[fallbackKey]: {
|
|
5758
|
+
...prepareCompactFallbackState(state),
|
|
5759
|
+
__codexOverleafFallbackAccountScopeId: accountScopeId
|
|
5760
|
+
}
|
|
5761
|
+
});
|
|
5762
|
+
} catch (caughtFallbackError) {
|
|
5763
|
+
fallbackError = caughtFallbackError;
|
|
5764
|
+
}
|
|
5765
|
+
if (isStorageQuotaError(error)) {
|
|
5766
|
+
emitStorageQuotaFailure(error, fallbackError);
|
|
5767
|
+
}
|
|
5768
|
+
if (typeof appendStorageNoticeOnce === 'function') {
|
|
5769
|
+
appendStorageNoticeOnce('save-failed', tx(`Failed to save session state: ${error.message}`, `保存会话状态失败:${error.message}`));
|
|
5770
|
+
} else {
|
|
5771
|
+
throw error;
|
|
6157
5772
|
}
|
|
6158
5773
|
}
|
|
6159
5774
|
}
|
|
@@ -6216,7 +5831,8 @@
|
|
|
6216
5831
|
showPluginToast(text, { status: 'warning', sticky: true });
|
|
6217
5832
|
}
|
|
6218
5833
|
|
|
6219
|
-
function saveStateSoon(delayMs = 120) {
|
|
5834
|
+
function saveStateSoon(delayMs = 120, persistenceOptions = null) {
|
|
5835
|
+
pendingSaveStateOptions = mergeSaveStateOptions(pendingSaveStateOptions, persistenceOptions);
|
|
6220
5836
|
// If a saveState() is already in flight, do NOT start a parallel one.
|
|
6221
5837
|
// Mark the trailing flag so the in-flight save's completion callback
|
|
6222
5838
|
// schedules another one — that way the final disk snapshot reflects
|
|
@@ -6238,9 +5854,15 @@
|
|
|
6238
5854
|
}
|
|
6239
5855
|
|
|
6240
5856
|
function runQueuedSaveState() {
|
|
5857
|
+
if (saveStateInFlightPromise) {
|
|
5858
|
+
saveStateRunAfterFlight = true;
|
|
5859
|
+
return saveStateInFlightPromise;
|
|
5860
|
+
}
|
|
6241
5861
|
saveStateInFlight = true;
|
|
6242
5862
|
saveStateRunAfterFlight = false;
|
|
6243
|
-
|
|
5863
|
+
const persistenceOptions = pendingSaveStateOptions;
|
|
5864
|
+
pendingSaveStateOptions = null;
|
|
5865
|
+
const runPromise = saveState(persistenceOptions)
|
|
6244
5866
|
.catch(error => {
|
|
6245
5867
|
if (isStorageQuotaError(error)) {
|
|
6246
5868
|
// Structured FailureReason §9.8: timer-driven saveState raised a
|
|
@@ -6252,6 +5874,9 @@
|
|
|
6252
5874
|
appendPlainLog(tx(`Failed to save session state: ${formatStateSaveError(error)}`, `保存会话状态失败:${formatStateSaveError(error)}`));
|
|
6253
5875
|
})
|
|
6254
5876
|
.finally(() => {
|
|
5877
|
+
if (saveStateInFlightPromise === runPromise) {
|
|
5878
|
+
saveStateInFlightPromise = null;
|
|
5879
|
+
}
|
|
6255
5880
|
saveStateInFlight = false;
|
|
6256
5881
|
if (saveStateRunAfterFlight) {
|
|
6257
5882
|
// A saveStateSoon() landed during the in-flight phase. Flush it
|
|
@@ -6260,6 +5885,52 @@
|
|
|
6260
5885
|
saveStateSoon(0);
|
|
6261
5886
|
}
|
|
6262
5887
|
});
|
|
5888
|
+
saveStateInFlightPromise = runPromise;
|
|
5889
|
+
return runPromise;
|
|
5890
|
+
}
|
|
5891
|
+
|
|
5892
|
+
async function flushQueuedSaveState(persistenceOptions = null) {
|
|
5893
|
+
pendingSaveStateOptions = mergeSaveStateOptions(pendingSaveStateOptions, persistenceOptions);
|
|
5894
|
+
let persistLatestSnapshot = true;
|
|
5895
|
+
while (persistLatestSnapshot) {
|
|
5896
|
+
if (saveStateTimer) {
|
|
5897
|
+
clearTimeout(saveStateTimer);
|
|
5898
|
+
saveStateTimer = null;
|
|
5899
|
+
}
|
|
5900
|
+
if (saveStateInFlightPromise) {
|
|
5901
|
+
await saveStateInFlightPromise;
|
|
5902
|
+
if (saveStateTimer) {
|
|
5903
|
+
clearTimeout(saveStateTimer);
|
|
5904
|
+
saveStateTimer = null;
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
|
|
5908
|
+
saveStateRunAfterFlight = false;
|
|
5909
|
+
await runQueuedSaveState();
|
|
5910
|
+
persistLatestSnapshot = Boolean(
|
|
5911
|
+
saveStateTimer
|
|
5912
|
+
|| saveStateRunAfterFlight
|
|
5913
|
+
|| pendingSaveStateOptions
|
|
5914
|
+
);
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
|
|
5918
|
+
function mergeSaveStateOptions(current, incoming) {
|
|
5919
|
+
if (!incoming) return current;
|
|
5920
|
+
const queueMutations = [
|
|
5921
|
+
...(current?.queueMutations || (current?.queueMutation ? [current.queueMutation] : [])),
|
|
5922
|
+
...(incoming?.queueMutations || (incoming?.queueMutation ? [incoming.queueMutation] : []))
|
|
5923
|
+
];
|
|
5924
|
+
return {
|
|
5925
|
+
...(current || {}),
|
|
5926
|
+
...incoming,
|
|
5927
|
+
queueMutation: undefined,
|
|
5928
|
+
queueMutations,
|
|
5929
|
+
deletedSessionIds: Array.from(new Set([
|
|
5930
|
+
...(current?.deletedSessionIds || []),
|
|
5931
|
+
...(incoming?.deletedSessionIds || [])
|
|
5932
|
+
]))
|
|
5933
|
+
};
|
|
6263
5934
|
}
|
|
6264
5935
|
|
|
6265
5936
|
function scheduleRunStateSave(kind) {
|
|
@@ -6511,7 +6182,15 @@
|
|
|
6511
6182
|
renderPendingInputs();
|
|
6512
6183
|
}
|
|
6513
6184
|
|
|
6514
|
-
function startRunView({
|
|
6185
|
+
function startRunView({
|
|
6186
|
+
task,
|
|
6187
|
+
mode,
|
|
6188
|
+
model,
|
|
6189
|
+
reasoningEffort,
|
|
6190
|
+
speedTier,
|
|
6191
|
+
queueItemId = '',
|
|
6192
|
+
executionSnapshot = null
|
|
6193
|
+
}) {
|
|
6515
6194
|
let attachments = [];
|
|
6516
6195
|
if (Array.isArray(arguments[0]?.attachments)) {
|
|
6517
6196
|
attachments = arguments[0].attachments;
|
|
@@ -6521,7 +6200,8 @@
|
|
|
6521
6200
|
id: createRunId(),
|
|
6522
6201
|
task,
|
|
6523
6202
|
mode,
|
|
6524
|
-
...providerSettingsCoordinator.getProviderSnapshot(state?.providerId),
|
|
6203
|
+
...providerSettingsCoordinator.getProviderSnapshot(executionSnapshot?.providerId || state?.providerId),
|
|
6204
|
+
providerRevision: executionSnapshot?.providerRevision || '',
|
|
6525
6205
|
model,
|
|
6526
6206
|
reasoningEffort,
|
|
6527
6207
|
speedTier,
|
|
@@ -6539,6 +6219,9 @@
|
|
|
6539
6219
|
nativeRequestId: '',
|
|
6540
6220
|
codexTurnId: '',
|
|
6541
6221
|
nativeEventSeq: 0,
|
|
6222
|
+
executionSnapshot: executionSnapshot
|
|
6223
|
+
? RunExecutionSnapshot.cloneSnapshot(executionSnapshot)
|
|
6224
|
+
: undefined,
|
|
6542
6225
|
// Welcome-panel + write-guard:
|
|
6543
6226
|
// Immutable per-run capture of the project this run was submitted
|
|
6544
6227
|
// against. Every writeback / accept / undo dispatch attaches this id
|
|
@@ -6546,7 +6229,8 @@
|
|
|
6546
6229
|
// is still acting on the same project before mutating the editor. No
|
|
6547
6230
|
// language-level enforcement — the field is treated as immutable by
|
|
6548
6231
|
// convention and never reassigned anywhere downstream.
|
|
6549
|
-
runProjectId: getCurrentProjectId() || ''
|
|
6232
|
+
runProjectId: getCurrentProjectId() || '',
|
|
6233
|
+
runAccountScopeId: cachedAccountScopeId || ''
|
|
6550
6234
|
};
|
|
6551
6235
|
const active = getActiveSession(state);
|
|
6552
6236
|
const titlePatch = active?.titleSource !== 'manual'
|
|
@@ -6576,6 +6260,8 @@
|
|
|
6576
6260
|
// Mirror the immutable runProjectId on the view so writeback /
|
|
6577
6261
|
// accept / undo dispatches don't need to re-look up the record.
|
|
6578
6262
|
runProjectId: record.runProjectId,
|
|
6263
|
+
runAccountScopeId: record.runAccountScopeId,
|
|
6264
|
+
executionSnapshot: record.executionSnapshot,
|
|
6579
6265
|
root,
|
|
6580
6266
|
runProcess: root.querySelector('[data-run-process]'),
|
|
6581
6267
|
processLabel: root.querySelector('[data-run-process-summary]'),
|
|
@@ -6590,6 +6276,19 @@
|
|
|
6590
6276
|
};
|
|
6591
6277
|
}
|
|
6592
6278
|
|
|
6279
|
+
function touchSessionForTerminalRun(sessionId, finishedAt) {
|
|
6280
|
+
const session = Array.isArray(state.sessions)
|
|
6281
|
+
? state.sessions.find(item => item?.id === sessionId)
|
|
6282
|
+
: null;
|
|
6283
|
+
if (!session) return '';
|
|
6284
|
+
const previousUpdatedAt = Date.parse(session.updatedAt || '') || 0;
|
|
6285
|
+
const requestedUpdatedAt = Date.parse(finishedAt || '') || Date.now();
|
|
6286
|
+
const updatedAt = new Date(Math.max(requestedUpdatedAt, previousUpdatedAt + 1)).toISOString();
|
|
6287
|
+
session.updatedAt = updatedAt;
|
|
6288
|
+
session.lastActivityAt = updatedAt;
|
|
6289
|
+
return updatedAt;
|
|
6290
|
+
}
|
|
6291
|
+
|
|
6593
6292
|
function finishRunView(text, status) {
|
|
6594
6293
|
if (!currentRunView) {
|
|
6595
6294
|
return;
|
|
@@ -6603,6 +6302,7 @@
|
|
|
6603
6302
|
record.status = status;
|
|
6604
6303
|
record.statusText = statusText;
|
|
6605
6304
|
record.finishedAt = new Date().toISOString();
|
|
6305
|
+
touchSessionForTerminalRun(currentRunView.sessionId, record.finishedAt);
|
|
6606
6306
|
currentRunView.terminalStatus = status;
|
|
6607
6307
|
runGuidanceController.settleView(currentRunView);
|
|
6608
6308
|
// Welcome-panel + write-guard:
|
|
@@ -7032,31 +6732,11 @@
|
|
|
7032
6732
|
|
|
7033
6733
|
|
|
7034
6734
|
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
|
-
)));
|
|
6735
|
+
return WritebackSettlement.isUndoResultEffectivelyApplied(
|
|
6736
|
+
run,
|
|
6737
|
+
result,
|
|
6738
|
+
buildNoTraceUndoRestore(run).operations
|
|
6739
|
+
);
|
|
7060
6740
|
}
|
|
7061
6741
|
|
|
7062
6742
|
async function undoRun(runId) {
|
|
@@ -7166,7 +6846,11 @@
|
|
|
7166
6846
|
// A subset undo never closes the ledger: unselected files can still
|
|
7167
6847
|
// be undone later, so the run stays 'partial' and the button usable.
|
|
7168
6848
|
const fullSelection = selectedOperations.length === undoOperations.length;
|
|
7169
|
-
|
|
6849
|
+
applyLegacyUndoSettlement(
|
|
6850
|
+
runId,
|
|
6851
|
+
undoApplied && fullSelection ? 'applied' : 'partial',
|
|
6852
|
+
result
|
|
6853
|
+
);
|
|
7170
6854
|
}
|
|
7171
6855
|
|
|
7172
6856
|
async function undoRunTrackedChanges(runId, run) {
|
|
@@ -7239,91 +6923,13 @@
|
|
|
7239
6923
|
}
|
|
7240
6924
|
});
|
|
7241
6925
|
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;
|
|
6926
|
+
WritebackSettlement.attachUndoNotVerifiedFailure(run, result, {
|
|
6927
|
+
buildFailure: buildContentFailure
|
|
7323
6928
|
});
|
|
7324
|
-
|
|
6929
|
+
applyTrackedChangeSettlement(runId, 'reject', result);
|
|
6930
|
+
return;
|
|
7325
6931
|
}
|
|
7326
|
-
|
|
6932
|
+
applyLegacyUndoSettlement(runId, result.skipped?.length ? 'partial' : 'applied', result);
|
|
7327
6933
|
}
|
|
7328
6934
|
|
|
7329
6935
|
async function acceptRun(runId) {
|
|
@@ -7345,8 +6951,6 @@
|
|
|
7345
6951
|
return;
|
|
7346
6952
|
}
|
|
7347
6953
|
|
|
7348
|
-
// UI-local in-flight lock: disables the acting button with progress. Never
|
|
7349
|
-
// written to trackedChangeStatus, never persisted.
|
|
7350
6954
|
trackedChangeInFlight.set(runId, 'accept');
|
|
7351
6955
|
refreshRunCardControls(runId);
|
|
7352
6956
|
appendRunRecordEvent(runId, {
|
|
@@ -7361,25 +6965,12 @@
|
|
|
7361
6965
|
trackedChanges: run.undoTrackedChanges || [],
|
|
7362
6966
|
expectedFiles: run.undoExpectedFiles || [],
|
|
7363
6967
|
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
6968
|
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
6969
|
runProjectId: getRunProjectIdForWriteback(run)
|
|
7373
6970
|
});
|
|
7374
6971
|
} finally {
|
|
7375
6972
|
trackedChangeInFlight.delete(runId);
|
|
7376
6973
|
}
|
|
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
6974
|
appendAcceptDiagnosticEvents(runId, Array.isArray(result.diagnostics) ? result.diagnostics : []);
|
|
7384
6975
|
appendRunRecordEvent(runId, {
|
|
7385
6976
|
title: tr('runAcceptTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
|
|
@@ -7396,10 +6987,6 @@
|
|
|
7396
6987
|
}))
|
|
7397
6988
|
}
|
|
7398
6989
|
});
|
|
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
6990
|
if (result.ok === false) {
|
|
7404
6991
|
appendRunRecordEvent(runId, {
|
|
7405
6992
|
title: tr('runAcceptTrackedFailed'),
|
|
@@ -7408,121 +6995,22 @@
|
|
|
7408
6995
|
[tr('detailReason')]: tr('runAcceptTrackedFailedReason')
|
|
7409
6996
|
}
|
|
7410
6997
|
});
|
|
7411
|
-
|
|
7412
|
-
|
|
6998
|
+
if (!WritebackSettlement.isSuccessfulTrackedChangeSettlement(result)) {
|
|
6999
|
+
refreshRunCardControls(runId);
|
|
7000
|
+
}
|
|
7413
7001
|
}
|
|
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.
|
|
7002
|
+
WritebackSettlement.attachAcceptNotVerifiedFailure(run, result, {
|
|
7003
|
+
buildFailure: buildContentFailure
|
|
7004
|
+
});
|
|
7005
|
+
applyTrackedChangeSettlement(runId, 'accept', result);
|
|
7006
|
+
}
|
|
7007
|
+
|
|
7439
7008
|
function getRunProjectIdForWriteback(run) {
|
|
7440
7009
|
return typeof run?.runProjectId === 'string' && run.runProjectId
|
|
7441
7010
|
? run.runProjectId
|
|
7442
7011
|
: '';
|
|
7443
7012
|
}
|
|
7444
7013
|
|
|
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
7014
|
// Translates the page bridge's Accept All `diagnostics` array into one
|
|
7527
7015
|
// run-card event row per step. Each entry is `{ step, info }`; the step
|
|
7528
7016
|
// names map 1:1 to the runAcceptTrackedStep* i18n keys. We render `info`
|
|
@@ -7532,14 +7020,14 @@
|
|
|
7532
7020
|
for (const entry of diagnostics || []) {
|
|
7533
7021
|
const step = entry && entry.step;
|
|
7534
7022
|
const info = (entry && entry.info) || {};
|
|
7535
|
-
const titleKey = ACCEPT_DIAGNOSTIC_TITLE_KEYS[step];
|
|
7023
|
+
const titleKey = WritebackSettlement.ACCEPT_DIAGNOSTIC_TITLE_KEYS[step];
|
|
7536
7024
|
if (!titleKey) {
|
|
7537
7025
|
continue;
|
|
7538
7026
|
}
|
|
7539
7027
|
const title = titleKey === 'runAcceptTrackedStepReplayStart' || titleKey === 'runAcceptTrackedStepReplayDone'
|
|
7540
7028
|
? tr(titleKey, { path: info.path || tr('unknownFile') })
|
|
7541
7029
|
: tr(titleKey);
|
|
7542
|
-
const status = acceptDiagnosticStatus(step, info);
|
|
7030
|
+
const status = WritebackSettlement.acceptDiagnosticStatus(step, info);
|
|
7543
7031
|
appendRunRecordEvent(runId, {
|
|
7544
7032
|
title,
|
|
7545
7033
|
kind: 'activity',
|
|
@@ -7549,289 +7037,28 @@
|
|
|
7549
7037
|
}
|
|
7550
7038
|
}
|
|
7551
7039
|
|
|
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 = {
|
|
7040
|
+
function buildContentFailure(code, operation, overrides) {
|
|
7041
|
+
return WritebackSettlement.buildContentFailure(
|
|
7665
7042
|
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)
|
|
7043
|
+
operation,
|
|
7044
|
+
overrides,
|
|
7045
|
+
FailureReasons
|
|
7776
7046
|
);
|
|
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
7047
|
}
|
|
7825
7048
|
|
|
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) {
|
|
7049
|
+
function applyTrackedChangeSettlement(runId, kind, result) {
|
|
7830
7050
|
const run = findRunRecord(runId);
|
|
7831
|
-
|
|
7051
|
+
const settlement = WritebackSettlement.settleTrackedChangeLifecycle({
|
|
7052
|
+
kind,
|
|
7053
|
+
run,
|
|
7054
|
+
result,
|
|
7055
|
+
failureReasons: FailureReasons
|
|
7056
|
+
});
|
|
7057
|
+
if (settlement.decision === 'blocked') {
|
|
7058
|
+
refreshRunCardControls(runId);
|
|
7832
7059
|
return;
|
|
7833
7060
|
}
|
|
7834
|
-
run.
|
|
7061
|
+
Object.assign(run, WritebackSettlement.applySettlementTransition(run, settlement));
|
|
7835
7062
|
saveStateSoon();
|
|
7836
7063
|
refreshRunCardControls(runId);
|
|
7837
7064
|
}
|
|
@@ -7908,13 +7135,7 @@
|
|
|
7908
7135
|
// possibly-divergent base. This keeps wide paragraph patches safe: their
|
|
7909
7136
|
// whole-paragraph `expected` would otherwise silently fail to re-apply.
|
|
7910
7137
|
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;
|
|
7138
|
+
return WritebackSettlement.attachVerifiedContentToOperation(operation, result);
|
|
7918
7139
|
}
|
|
7919
7140
|
|
|
7920
7141
|
function recordUndoFromApply(project, applyResult) {
|
|
@@ -7938,8 +7159,8 @@
|
|
|
7938
7159
|
];
|
|
7939
7160
|
record.appliedOperations = combinedAppliedOperations;
|
|
7940
7161
|
|
|
7941
|
-
if (state.requireReviewing === true) {
|
|
7942
|
-
const combinedTrackedChanges =
|
|
7162
|
+
if ((currentRunView?.executionSnapshot?.requireReviewing ?? state.requireReviewing) === true) {
|
|
7163
|
+
const combinedTrackedChanges = normalizeApplyTrackedChanges([
|
|
7943
7164
|
...(Array.isArray(record.undoTrackedChanges) ? record.undoTrackedChanges : []),
|
|
7944
7165
|
...trackedChanges
|
|
7945
7166
|
]);
|
|
@@ -8011,49 +7232,11 @@
|
|
|
8011
7232
|
}
|
|
8012
7233
|
|
|
8013
7234
|
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);
|
|
7235
|
+
return WritebackSettlement.normalizeApplyTrackedChanges(changes);
|
|
8034
7236
|
}
|
|
8035
7237
|
|
|
8036
7238
|
function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = []) {
|
|
8037
|
-
|
|
8038
|
-
for (const change of trackedChanges || []) {
|
|
8039
|
-
if (change.path) {
|
|
8040
|
-
paths.add(change.path);
|
|
8041
|
-
}
|
|
8042
|
-
}
|
|
8043
|
-
for (const operation of operations || []) {
|
|
8044
|
-
if (operation?.path) {
|
|
8045
|
-
paths.add(operation.path);
|
|
8046
|
-
}
|
|
8047
|
-
if (operation?.to) {
|
|
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
|
-
}));
|
|
7239
|
+
return WritebackSettlement.selectExpectedFilesForTrackedUndo(project, operations, trackedChanges);
|
|
8057
7240
|
}
|
|
8058
7241
|
|
|
8059
7242
|
function buildTrackedUndoPostFiles(run) {
|
|
@@ -8136,6 +7319,21 @@
|
|
|
8136
7319
|
refreshRunCardControls(runId);
|
|
8137
7320
|
}
|
|
8138
7321
|
|
|
7322
|
+
function applyLegacyUndoSettlement(runId, undoStatus, result) {
|
|
7323
|
+
const run = findRunRecord(runId);
|
|
7324
|
+
if (!run) return;
|
|
7325
|
+
Object.assign(run, WritebackSettlement.applySettlementTransition(
|
|
7326
|
+
run,
|
|
7327
|
+
WritebackSettlement.settleLegacyUndo({
|
|
7328
|
+
run,
|
|
7329
|
+
status: undoStatus,
|
|
7330
|
+
result
|
|
7331
|
+
})
|
|
7332
|
+
));
|
|
7333
|
+
saveStateSoon();
|
|
7334
|
+
refreshRunCardControls(runId);
|
|
7335
|
+
}
|
|
7336
|
+
|
|
8139
7337
|
function refreshRunCardControls(runId) {
|
|
8140
7338
|
const run = findRunRecord(runId);
|
|
8141
7339
|
const root = panel?.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
|
|
@@ -8290,13 +7488,16 @@
|
|
|
8290
7488
|
const operations = input.operations || [];
|
|
8291
7489
|
const applyResults = input.applyResults || [];
|
|
8292
7490
|
const record = currentRunView?.recordId ? findRunRecord(currentRunView.recordId, currentRunView.sessionId) : null;
|
|
8293
|
-
const
|
|
7491
|
+
const settlementProjection = WritebackSettlement.projectRunSettlement(record || {});
|
|
7492
|
+
const undoCount = settlementProjection.canUndo ? getRunUndoCount(record) : 0;
|
|
8294
7493
|
const report = buildHumanCompletionReport({
|
|
8295
7494
|
...input,
|
|
8296
7495
|
locale: getLocale(),
|
|
8297
7496
|
operations,
|
|
8298
7497
|
applyResults,
|
|
8299
7498
|
undoCount,
|
|
7499
|
+
settlementProjection,
|
|
7500
|
+
failure: input.failure || settlementProjection.primaryFailure || undefined,
|
|
8300
7501
|
includeWriteResult: true
|
|
8301
7502
|
});
|
|
8302
7503
|
|
|
@@ -8315,7 +7516,7 @@
|
|
|
8315
7516
|
// which recovery button to render. (Fixed in v1.7.5: callers passed
|
|
8316
7517
|
// input.failure since v1.6.2 but it was never forwarded, so recovery
|
|
8317
7518
|
// buttons on completion reports could never appear.)
|
|
8318
|
-
failure: input.failure || undefined,
|
|
7519
|
+
failure: input.failure || settlementProjection.primaryFailure || undefined,
|
|
8319
7520
|
compileErrors: input.compileErrors || undefined,
|
|
8320
7521
|
rejectedHunks: input.rejectedHunks || currentRunView?.rejectedHunks || undefined
|
|
8321
7522
|
});
|