codex-overleaf-link 2.5.1 → 2.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -398
- package/README.zh-CN.md +153 -404
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +148 -3
- package/extension/src/backgroundSharedSessions.js +55 -2
- package/extension/src/backgroundUpdateCoordinator.js +128 -12
- package/extension/src/content/activeTurnControl.js +164 -7
- package/extension/src/content/composerAttachments.js +46 -10
- package/extension/src/content/composerPanel.js +92 -12
- package/extension/src/content/contentRuntime.js +1114 -321
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +147 -42
- package/extension/src/content/generated/content.bundle.js +5761 -1237
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +4 -0
- package/extension/src/content/localSkillsPanel.js +6 -4
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +53 -14
- package/extension/src/content/modelPicker.js +29 -19
- package/extension/src/content/nativeCompatibilityController.js +52 -19
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +7 -7
- package/extension/src/content/panelMaintenance.js +10 -2
- package/extension/src/content/panelRenderer.js +55 -28
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectSettingsCoordinator.js +30 -6
- package/extension/src/content/providerSettingsDialog.js +28 -19
- package/extension/src/content/recentProjects.js +30 -19
- package/extension/src/content/runActivitySummary.js +122 -14
- package/extension/src/content/runCompletionMeta.js +194 -6
- package/extension/src/content/runController.js +2 -1
- package/extension/src/content/runFailureNotice.js +8 -1
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +27 -13
- package/extension/src/content/runRecoveryActions.js +2 -1
- package/extension/src/content/runResultActions.js +73 -24
- package/extension/src/content/runReviewController.js +102 -37
- package/extension/src/content/runTimelineView.js +224 -25
- package/extension/src/content/scopedPersistencePanelState.js +124 -46
- package/extension/src/content/selectionContextView.js +30 -3
- package/extension/src/content/sensitivePreflight.js +14 -1
- package/extension/src/content/sessionManager.js +49 -3
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +15 -2
- package/extension/src/content/settingsWorkbench.js +127 -89
- package/extension/src/content/sharedSessionViewSync.js +13 -4
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateNotice.js +60 -6
- package/extension/src/content/writebackOrchestrator.js +301 -72
- package/extension/src/content/writingStyleSettings.js +70 -17
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/overleafEditor.js +12 -3
- package/extension/src/page/overleafProjectSnapshot.js +56 -3
- package/extension/src/page/snapshotRouter.js +75 -66
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +9 -1
- package/extension/src/page/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +172 -113
- package/extension/src/pageBridge.js +124 -73
- package/extension/src/popup.js +1 -1
- package/extension/src/shared/agentTranscript.js +372 -18
- package/extension/src/shared/auditRecords.js +3 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/compileAdapter.js +58 -17
- package/extension/src/shared/composerAttachmentDraft.js +27 -7
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/failureReasons.js +57 -3
- package/extension/src/shared/i18n.js +315 -29
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +2 -1
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
- package/extension/src/shared/sessionState.js +63 -4
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/sharedSessionBridge.js +81 -13
- package/extension/src/shared/staleGuard.js +130 -63
- package/extension/src/shared/storageDb.js +35 -3
- package/extension/src/shared/storageMigration.js +111 -2
- package/extension/src/shared/storageRunActions.js +62 -8
- package/extension/src/shared/undoOperations.js +126 -28
- package/extension/src/shared/updateRuntimeIdentity.js +16 -2
- package/extension/src/shared/writebackSettlement.js +21 -3
- package/extension/styles/panel.css +1293 -549
- package/native-host/src/bootstrapUpdate.js +52 -16
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +10 -1
- package/native-host/src/chatCompletionsBridge.js +29 -2
- package/native-host/src/codexPromptAssembly.js +86 -118
- package/native-host/src/codexSessionRunner.js +127 -17
- package/native-host/src/debugLog.js +27 -3
- package/native-host/src/index.js +42 -11
- package/native-host/src/localSkills.js +1 -1
- package/native-host/src/mirrorWorkspace.js +47 -3
- package/native-host/src/nativeEnvironment.js +27 -5
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
- package/native-host/src/taskRunnerRuntime.js +28 -8
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/updateManager.js +144 -21
- package/native-host/src/writingStyleRuntime.js +1 -1
- package/package.json +5 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
(function initStorageDb(root, factory) {
|
|
2
2
|
if (typeof module === 'object' && module.exports) {
|
|
3
3
|
module.exports = factory(require('./storageRunActions'), require('./runInputQueue'),
|
|
4
|
-
require('./settlementFacts'), require('./storageSessionClaims'), require('./sessionState'), require('./runActivityModel'), require('./sharedSessionBridge'), require('./subagentActivityModel'), require('./storageValuePruning'), require('./composerAttachmentDraft'), require('./sharedSessionTransfer'));
|
|
4
|
+
require('./settlementFacts'), require('./storageSessionClaims'), require('./sessionState'), require('./runActivityModel'), require('./sharedSessionBridge'), require('./subagentActivityModel'), require('./storageValuePruning'), require('./composerAttachmentDraft'), require('./sharedSessionTransfer'), require('./contextUsage'));
|
|
5
5
|
} else {
|
|
6
6
|
root.CodexOverleafModuleRegistry.define('StorageDb', [
|
|
7
7
|
'StorageRunActions',
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
'SubagentActivityModel',
|
|
15
15
|
'StorageValuePruning',
|
|
16
16
|
'ComposerAttachmentDraft',
|
|
17
|
-
'SharedSessionTransfer'
|
|
17
|
+
'SharedSessionTransfer',
|
|
18
|
+
'ContextUsage'
|
|
18
19
|
], factory);
|
|
19
20
|
}
|
|
20
|
-
})(typeof globalThis !== 'undefined' ? globalThis : window, function storageDbFactory(StorageRunActions, RunInputQueue, SettlementFacts, StorageSessionClaims, SessionState, RunActivityModel, SharedSessionBridge, SubagentActivityModel, StorageValuePruning, ComposerAttachmentDraft, SharedSessionTransfer) {
|
|
21
|
+
})(typeof globalThis !== 'undefined' ? globalThis : window, function storageDbFactory(StorageRunActions, RunInputQueue, SettlementFacts, StorageSessionClaims, SessionState, RunActivityModel, SharedSessionBridge, SubagentActivityModel, StorageValuePruning, ComposerAttachmentDraft, SharedSessionTransfer, ContextUsage) {
|
|
21
22
|
'use strict';
|
|
22
23
|
const { removeEmptySummaryFields, removeEmptyFields } = StorageValuePruning;
|
|
23
24
|
if (!StorageRunActions?.compactRunsForStorage || !StorageRunActions?.compactRunActionPayload || !StorageRunActions?.compactProviderSnapshot || !StorageRunActions?.compactRunExecutionSnapshot || !StorageRunActions?.compactStructuredEventValue || !StorageRunActions?.hashString || !RunInputQueue?.compactForStorage || !SettlementFacts?.compactSettlementFacts || !StorageSessionClaims?.createSessionClaimer) {
|
|
@@ -276,6 +277,7 @@
|
|
|
276
277
|
title: normalizeSessionTitleForStorage(input.title, titleSource),
|
|
277
278
|
titleSource: titleSource,
|
|
278
279
|
codexThreadId: typeof input.codexThreadId === 'string' ? input.codexThreadId : '',
|
|
280
|
+
contextUsage: ContextUsage.normalize(input.contextUsage),
|
|
279
281
|
forkedFromSessionId: normalizeTextField(input.forkedFromSessionId, 160),
|
|
280
282
|
forkedFromRunId: normalizeTextField(input.forkedFromRunId, 160),
|
|
281
283
|
status: typeof input.status === 'string' && input.status ? input.status : 'active',
|
|
@@ -303,6 +305,17 @@
|
|
|
303
305
|
accountScopeUnavailable: accountScopeUnavailable,
|
|
304
306
|
safeTaskSummary: safeTaskSummary
|
|
305
307
|
};
|
|
308
|
+
// Optional provenance for the automatic clear of one captured submission.
|
|
309
|
+
// It is envelope metadata, not run/review authority; no schema migration is needed.
|
|
310
|
+
if (record.task === '' && typeof input.draftConsumption?.task === 'string'
|
|
311
|
+
&& input.draftConsumption.task && typeof input.draftConsumption.runId === 'string'
|
|
312
|
+
&& input.draftConsumption.runId && record.runs.some(run => run.id === input.draftConsumption.runId
|
|
313
|
+
&& !run.forkSnapshot && run.runProjectId === record.projectId)) {
|
|
314
|
+
record.draftConsumption = {
|
|
315
|
+
runId: input.draftConsumption.runId,
|
|
316
|
+
task: normalizeDisplayTextForStorage(input.draftConsumption.task, SESSION_STORAGE_LIMITS.taskChars)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
306
319
|
if (options.preserveRunActionPayload === true) {
|
|
307
320
|
// Count the real display record before allocating recovery space. Adding
|
|
308
321
|
// older checkpoints must not make an otherwise valid history unwritable.
|
|
@@ -436,6 +449,8 @@
|
|
|
436
449
|
turnId: typeof input.turnId === 'string' ? input.turnId : '',
|
|
437
450
|
createdAt: typeof input.createdAt === 'string' ? input.createdAt : now,
|
|
438
451
|
completedAt: typeof input.completedAt === 'string' ? input.completedAt : '',
|
|
452
|
+
requireReviewing: typeof input.requireReviewing === 'boolean' ? input.requireReviewing : undefined,
|
|
453
|
+
trackingDecision: ['continue', 'disable'].includes(input.trackingDecision) ? input.trackingDecision : '',
|
|
439
454
|
mode: typeof input.mode === 'string' ? input.mode : '', ...StorageRunActions.compactProviderSnapshot(input),
|
|
440
455
|
model: typeof input.model === 'string' ? input.model : '',
|
|
441
456
|
reasoningEffort: typeof input.reasoningEffort === 'string' ? input.reasoningEffort : '',
|
|
@@ -708,7 +723,9 @@
|
|
|
708
723
|
undoStatus: normalizeDisplayTextForStorage(run.undoStatus, SESSION_STORAGE_LIMITS.statusTextChars),
|
|
709
724
|
...(run.undoOwnerProtocol === 1 ? { undoOwnerProtocol: 1 } : {}),
|
|
710
725
|
nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
|
|
726
|
+
...(run.operation === 'compact' ? { operation: 'compact' } : {}),
|
|
711
727
|
codexThreadId: normalizeTextField(run.codexThreadId, 160), codexTurnId: normalizeTextField(run.codexTurnId, 160),
|
|
728
|
+
contextUsage: run.forkSnapshot === true ? null : ContextUsage.normalize(run.contextUsage),
|
|
712
729
|
nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
|
|
713
730
|
queueItemId: normalizeTextField(run.queueItemId, 160)
|
|
714
731
|
};
|
|
@@ -775,6 +792,19 @@
|
|
|
775
792
|
if (event.failure) {
|
|
776
793
|
compact.failure = compactStructuredEventValueForStorage(event.failure, 0);
|
|
777
794
|
}
|
|
795
|
+
if (Array.isArray(event.compileErrors) && event.compileErrors.length) {
|
|
796
|
+
compact.compileErrors = event.compileErrors.slice(0, 5)
|
|
797
|
+
.map(item => normalizeDisplayTextForStorage(String(item), SESSION_STORAGE_LIMITS.detailChars))
|
|
798
|
+
.filter(Boolean);
|
|
799
|
+
}
|
|
800
|
+
if (Array.isArray(event.rejectedHunks) && event.rejectedHunks.length) {
|
|
801
|
+
compact.rejectedHunks = event.rejectedHunks.slice(0, 10)
|
|
802
|
+
.map(item => ({
|
|
803
|
+
path: normalizeDisplayTextForStorage(String(item?.path || ''), SESSION_STORAGE_LIMITS.pathChars),
|
|
804
|
+
summary: normalizeDisplayTextForStorage(String(item?.summary || ''), SESSION_STORAGE_LIMITS.detailChars)
|
|
805
|
+
}))
|
|
806
|
+
.filter(item => item.path);
|
|
807
|
+
}
|
|
778
808
|
return compact;
|
|
779
809
|
});
|
|
780
810
|
}
|
|
@@ -1343,6 +1373,8 @@
|
|
|
1343
1373
|
projectRetentionWarnings: StorageRunActions.projectRetentionWarnings,
|
|
1344
1374
|
projectRetentionUpdates: StorageRunActions.projectRetentionUpdates,
|
|
1345
1375
|
mergeSessionReviewState: StorageRunActions.mergeSessionReviewState,
|
|
1376
|
+
copyRunReviewState: StorageRunActions.copyRunReviewState,
|
|
1377
|
+
reviewStateSnapshot: StorageRunActions.reviewStateSnapshot,
|
|
1346
1378
|
buildTurnRecord: buildTurnRecord,
|
|
1347
1379
|
buildEventRecord: buildEventRecord,
|
|
1348
1380
|
buildArtifactRecord: buildArtifactRecord,
|
|
@@ -19,9 +19,105 @@
|
|
|
19
19
|
'experimentalOtByProject',
|
|
20
20
|
'customInstructionsByProject',
|
|
21
21
|
'governanceRulesByProject',
|
|
22
|
+
'reviewingAvailabilityByProject',
|
|
22
23
|
'selectedLocalSkillIdsByProject'
|
|
23
24
|
];
|
|
24
25
|
var projectPreferenceLockTails = {};
|
|
26
|
+
var projectPreferenceBaselines = new Map();
|
|
27
|
+
var projectPreferenceIntents = new Map();
|
|
28
|
+
var projectPreferenceBaselineVersions = new WeakMap();
|
|
29
|
+
|
|
30
|
+
function getProjectPreferenceBaseline(accountScopeId, projectId) {
|
|
31
|
+
var key = buildScopedProjectPreferenceKey(accountScopeId, projectId);
|
|
32
|
+
var baseline = copyPreferenceValue(projectPreferenceBaselines.get(key));
|
|
33
|
+
var intents = projectPreferenceIntents.get(key);
|
|
34
|
+
if (baseline && intents) projectPreferenceBaselineVersions.set(baseline, {
|
|
35
|
+
owner: intents, versions: Object.assign({}, intents.acknowledged)
|
|
36
|
+
});
|
|
37
|
+
return baseline;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function adoptProjectPreferenceBaseline(prefs, accountScopeId, projectId) {
|
|
41
|
+
var key = buildScopedProjectPreferenceKey(accountScopeId, projectId);
|
|
42
|
+
if (key) {
|
|
43
|
+
projectPreferenceBaselines.set(key, copyPreferenceValue(prefs));
|
|
44
|
+
projectPreferenceIntents.set(key, { revision: 0, fields: {}, acknowledged: {} });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function captureProjectPreferenceWrite(input) {
|
|
49
|
+
var projectId = input.projectId;
|
|
50
|
+
var key = buildScopedProjectPreferenceKey(input.accountScopeId, projectId);
|
|
51
|
+
if (!key) return null;
|
|
52
|
+
var intents = projectPreferenceIntents.get(key);
|
|
53
|
+
if (!intents) {
|
|
54
|
+
intents = { revision: 0, fields: {}, acknowledged: {} };
|
|
55
|
+
projectPreferenceIntents.set(key, intents);
|
|
56
|
+
}
|
|
57
|
+
function values(prefs) {
|
|
58
|
+
return {
|
|
59
|
+
experimentalOtByProject: input.normalizeExperimentalOtByProject(prefs.experimentalOtByProject)[projectId] === true,
|
|
60
|
+
reviewingAvailabilityByProject: prefs.reviewingAvailabilityByProject?.[projectId],
|
|
61
|
+
governanceRulesByProject: input.normalizeGovernanceRulesByProject({
|
|
62
|
+
[projectId]: prefs.governanceRulesByProject?.[projectId] || {}
|
|
63
|
+
})[projectId],
|
|
64
|
+
customInstructionsByProject: input.normalizeCustomInstructionsByProject({
|
|
65
|
+
[projectId]: prefs.customInstructionsByProject?.[projectId] || ''
|
|
66
|
+
})[projectId]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var baseline = input.baseline || getProjectPreferenceBaseline(input.accountScopeId, projectId);
|
|
70
|
+
var captured = projectPreferenceBaselineVersions.get(baseline);
|
|
71
|
+
var versions = captured?.owner === intents ? captured.versions : {};
|
|
72
|
+
var previous = values(baseline || {});
|
|
73
|
+
var current = values(input.state);
|
|
74
|
+
var token = { key: key, owner: intents, projectId: projectId, fields: {}, versions: {}, written: [] };
|
|
75
|
+
for (var name of Object.keys(current)) {
|
|
76
|
+
var latest = intents.fields[name];
|
|
77
|
+
// A pending or later local intent supersedes the older captured read.
|
|
78
|
+
// Peer adoption alone does not turn an unchanged queued snapshot into an edit.
|
|
79
|
+
var reference = latest && latest.revision > (versions[name] || 0) ? latest.value : previous[name];
|
|
80
|
+
if (JSON.stringify(current[name]) !== JSON.stringify(reference)) {
|
|
81
|
+
latest = { revision: ++intents.revision, value: copyPreferenceValue(current[name]), dirty: true };
|
|
82
|
+
intents.fields[name] = latest;
|
|
83
|
+
}
|
|
84
|
+
token.fields[name] = latest && JSON.stringify(current[name]) === JSON.stringify(latest.value) ? latest : null;
|
|
85
|
+
token.versions[name] = latest?.revision || 0;
|
|
86
|
+
}
|
|
87
|
+
return token;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function projectPreferencePatch(token, accountScopeId, projectId) {
|
|
91
|
+
if (token.key !== buildScopedProjectPreferenceKey(accountScopeId, projectId)) {
|
|
92
|
+
throw Object.assign(new Error('Scoped project preferences require account and project identity.'), {
|
|
93
|
+
code: 'account_scope_unavailable'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
var current = projectPreferenceIntents.get(token.key) === token.owner;
|
|
97
|
+
var patch = {};
|
|
98
|
+
token.written = [];
|
|
99
|
+
for (var name of Object.keys(token.fields)) {
|
|
100
|
+
var intent = token.fields[name];
|
|
101
|
+
var changed = current && intent?.dirty && token.owner.fields[name] === intent;
|
|
102
|
+
patch[name] = changed ? { [token.projectId]: copyPreferenceValue(intent.value) } : {};
|
|
103
|
+
if (changed) token.written.push(name);
|
|
104
|
+
}
|
|
105
|
+
return patch;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function completeProjectPreferenceWrite(token, prefs) {
|
|
109
|
+
if (projectPreferenceIntents.get(token.key) !== token.owner) return [];
|
|
110
|
+
for (var name of token.written) {
|
|
111
|
+
var intent = token.fields[name];
|
|
112
|
+
intent.dirty = false;
|
|
113
|
+
token.owner.acknowledged[name] = Math.max(token.owner.acknowledged[name] || 0, intent.revision);
|
|
114
|
+
}
|
|
115
|
+
projectPreferenceBaselines.set(token.key, copyPreferenceValue(prefs));
|
|
116
|
+
return Object.keys(token.fields).filter(function (name) {
|
|
117
|
+
var latest = token.owner.fields[name];
|
|
118
|
+
return (latest?.revision || 0) === token.versions[name] && !latest?.dirty;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
25
121
|
|
|
26
122
|
function buildScopedProjectPreferenceKey(accountScopeId, projectId) {
|
|
27
123
|
var account = compactScopePart(accountScopeId);
|
|
@@ -130,7 +226,12 @@
|
|
|
130
226
|
? incoming[mapName]
|
|
131
227
|
: {};
|
|
132
228
|
if (Object.prototype.hasOwnProperty.call(incomingMap, projectId)) {
|
|
133
|
-
storedMap[scopedKey] =
|
|
229
|
+
const previous = storedMap[scopedKey], observation = incomingMap[projectId];
|
|
230
|
+
if (mapName !== 'reviewingAvailabilityByProject'
|
|
231
|
+
|| !(Number(previous?.observedAt) > Number(observation?.observedAt)
|
|
232
|
+
|| (previous?.observedAt === observation?.observedAt && previous?.status === 'available'))) {
|
|
233
|
+
storedMap[scopedKey] = copyPreferenceValue(observation);
|
|
234
|
+
}
|
|
134
235
|
}
|
|
135
236
|
delete storedMap[projectId];
|
|
136
237
|
next[mapName] = storedMap;
|
|
@@ -164,6 +265,7 @@
|
|
|
164
265
|
var visible = await StorageDb.claimSessionsForAccount(projectId, account, deleted);
|
|
165
266
|
function result(currentPrefs, sessions, migrated) {
|
|
166
267
|
var wanted = currentPrefs.activeSessionByProject?.[projectId] || '';
|
|
268
|
+
adoptProjectPreferenceBaseline(currentPrefs, account, projectId);
|
|
167
269
|
return { prefs: currentPrefs, sessions: sessions,
|
|
168
270
|
activeSessionId: sessions.some(session => session.id === wanted) ? wanted : sessions.at(-1)?.id || '',
|
|
169
271
|
migrated: migrated };
|
|
@@ -254,7 +356,9 @@
|
|
|
254
356
|
return chrome.storage.local.get([PREFS_KEY]).then(function (stored) {
|
|
255
357
|
var claimed = claimLegacyProjectPreferences(stored[PREFS_KEY] || {}, account, project);
|
|
256
358
|
var next = mergeScopedProjectPreferences(claimed.prefs, prefs, account, project);
|
|
257
|
-
return chrome.storage.local.set({ [PREFS_KEY]: next })
|
|
359
|
+
return chrome.storage.local.set({ [PREFS_KEY]: next }).then(function () {
|
|
360
|
+
return buildProjectPreferenceView(next, account, project);
|
|
361
|
+
});
|
|
258
362
|
});
|
|
259
363
|
});
|
|
260
364
|
}
|
|
@@ -458,6 +562,11 @@
|
|
|
458
562
|
runMigrationIfNeeded: runMigrationIfNeeded,
|
|
459
563
|
savePrefs: savePrefs,
|
|
460
564
|
loadPrefs: loadPrefs,
|
|
565
|
+
getProjectPreferenceBaseline: getProjectPreferenceBaseline,
|
|
566
|
+
adoptProjectPreferenceBaseline: adoptProjectPreferenceBaseline,
|
|
567
|
+
captureProjectPreferenceWrite: captureProjectPreferenceWrite,
|
|
568
|
+
projectPreferencePatch: projectPreferencePatch,
|
|
569
|
+
completeProjectPreferenceWrite: completeProjectPreferenceWrite,
|
|
461
570
|
loadSessionTombstones: loadSessionTombstones,
|
|
462
571
|
addSessionTombstones: addSessionTombstones,
|
|
463
572
|
getDeletedSessionIds: getDeletedSessionIds,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
(function initStorageRunActions(root, factory) {
|
|
2
2
|
if (typeof module === 'object' && module.exports) {
|
|
3
|
-
module.exports = factory(require('./selectionContext'), require('./writebackIntent'), require('./writingStyle'), require('./directoryCleanupProof'));
|
|
3
|
+
module.exports = factory(require('./selectionContext'), require('./writebackIntent'), require('./writingStyle'), require('./directoryCleanupProof'), require('./undoOperations'));
|
|
4
4
|
} else {
|
|
5
|
-
root.CodexOverleafModuleRegistry.define('StorageRunActions', ['SelectionContext', 'WritebackIntent', 'WritingStyle', 'DirectoryCleanupProof'], factory);
|
|
5
|
+
root.CodexOverleafModuleRegistry.define('StorageRunActions', ['SelectionContext', 'WritebackIntent', 'WritingStyle', 'DirectoryCleanupProof', 'UndoOperations'], factory);
|
|
6
6
|
}
|
|
7
|
-
})(typeof globalThis !== 'undefined' ? globalThis : window, function storageRunActionsFactory(SelectionContext, WritebackIntent, WritingStyle, DirectoryCleanupProof) {
|
|
7
|
+
})(typeof globalThis !== 'undefined' ? globalThis : window, function storageRunActionsFactory(SelectionContext, WritebackIntent, WritingStyle, DirectoryCleanupProof, UndoOperations) {
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
// Share a bounded budget across the retained history, rather than expiring
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
&& Array.isArray(run.undoExpectedFiles) && run.undoExpectedFiles.length > 0
|
|
130
130
|
&& Array.isArray(run.appliedOperations) && run.appliedOperations.length > 0;
|
|
131
131
|
var legacyUndo = Array.isArray(run && run.undoOperations) && run.undoOperations.length > 0;
|
|
132
|
-
return trackedLifecycle || reviewingEditorUndo || legacyUndo;
|
|
132
|
+
return trackedLifecycle || reviewingEditorUndo || legacyUndo || UndoOperations.getUndoUnavailableFiles(run).length > 0;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
function runDirectoryCleanup(run) {
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
function compactCoreActionPayload(run, keepActionPayload, options = {}) {
|
|
189
|
-
var empty = emptyActionPayload();
|
|
189
|
+
var empty = { ...emptyActionPayload(), appliedOperations: UndoOperations.compactUnavailableUndoOperations(run) };
|
|
190
190
|
if (!keepActionPayload || !hasReloadableRunActionPayload(run)) {
|
|
191
191
|
if (hasReloadableRunActionPayload(run)) empty.recoveryRetention = omitted('fallback');
|
|
192
192
|
else if (run?.recoveryRetention?.state === 'omitted' && !run.forkSnapshot
|
|
@@ -308,11 +308,48 @@
|
|
|
308
308
|
return out;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
const REVIEW_FIELDS = ['trackedChangeStatus', 'undoStatus', 'undoOwnerProtocol', 'undoTrackedChanges', 'undoExpectedFiles',
|
|
312
|
+
'undoOperations', 'undoBaseFiles', 'appliedOperations', 'trackedChangeCaptures', 'recoveryRetention',
|
|
313
|
+
'interruptedDraft', 'retryWriteback', 'saveCheck'];
|
|
314
|
+
const REVIEW_FACT_FIELDS = ['documentEffect', 'fileSettlements', 'failures', 'recoveryAvailability', 'recoveryDisposition'];
|
|
315
|
+
const terminalReview = run => ['accepted', 'rejected'].includes(run?.trackedChangeStatus) || run?.undoStatus === 'applied';
|
|
316
|
+
const factsOf = run => run?.settlement || run?.settlementFacts || {};
|
|
317
|
+
|
|
318
|
+
function copyRunReviewState(target, source) {
|
|
319
|
+
const merged = { ...target };
|
|
320
|
+
const copyFields = (into, from, fields) => {
|
|
321
|
+
for (const field of fields) {
|
|
322
|
+
if (Object.hasOwn(from, field)) into[field] = JSON.parse(JSON.stringify(from[field]));
|
|
323
|
+
else delete into[field];
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
copyFields(merged, source, REVIEW_FIELDS);
|
|
327
|
+
const sourceFacts = factsOf(source), targetFacts = factsOf(target);
|
|
328
|
+
const facts = { ...targetFacts };
|
|
329
|
+
copyFields(facts, sourceFacts, REVIEW_FACT_FIELDS);
|
|
330
|
+
const sourceEvidence = sourceFacts.evidence || {};
|
|
331
|
+
const evidence = { ...(targetFacts.evidence || {}) };
|
|
332
|
+
copyFields(evidence, sourceEvidence, [...new Set([...Object.keys(evidence), ...Object.keys(sourceEvidence)])]
|
|
333
|
+
.filter(key => !['mirrored', 'compiled'].includes(key)));
|
|
334
|
+
if (Object.keys(evidence).length) facts.evidence = evidence; else delete facts.evidence;
|
|
335
|
+
delete merged.settlementFacts;
|
|
336
|
+
if (Object.keys(facts).length) merged.settlement = facts; else delete merged.settlement;
|
|
337
|
+
return merged;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function reviewStateSnapshot(run) {
|
|
341
|
+
const facts = factsOf(run);
|
|
342
|
+
return JSON.stringify([REVIEW_FIELDS.map(key => run?.[key]), REVIEW_FACT_FIELDS.map(key => facts[key]),
|
|
343
|
+
Object.entries(facts.evidence || {}).filter(([key]) => !['mirrored', 'compiled'].includes(key))
|
|
344
|
+
.sort(([a], [b]) => a.localeCompare(b))]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Shared submissions retain the caller's envelope and private baseline. The
|
|
348
|
+
// legacy site-local path explicitly keeps its historical timestamp merge.
|
|
349
|
+
function mergeSessionReviewState(incoming, existing, reviewRunIds, options = {}) {
|
|
313
350
|
if (!existing || incoming.id !== existing.id || incoming.projectId !== existing.projectId
|
|
314
351
|
|| !incoming.accountScopeId || incoming.accountScopeId !== existing.accountScopeId) return incoming;
|
|
315
|
-
var terminal =
|
|
352
|
+
var terminal = terminalReview;
|
|
316
353
|
var incomingRuns = new Map((incoming.runs || []).map(run => [run.id, run]));
|
|
317
354
|
var existingRuns = new Map((existing.runs || []).map(run => [run.id, run]));
|
|
318
355
|
var requestedIds = new Set(reviewRunIds || []);
|
|
@@ -321,6 +358,21 @@
|
|
|
321
358
|
return ['accepted', 'rejected', 'needs_review'].includes(run?.trackedChangeStatus)
|
|
322
359
|
|| ['applied', 'partial'].includes(run?.undoStatus);
|
|
323
360
|
});
|
|
361
|
+
if (options.shared !== false) {
|
|
362
|
+
const runs = (incoming.runs || []).map(run => {
|
|
363
|
+
const previous = existingRuns.get(run.id);
|
|
364
|
+
const source = terminal(previous) ? previous : null;
|
|
365
|
+
if (!source || (source.runProjectId && source.runProjectId !== incoming.projectId)) return run;
|
|
366
|
+
// The same decision can still gain save/recovery evidence. Let the
|
|
367
|
+
// three-way merge retain those edits and reject genuine collisions.
|
|
368
|
+
if (run.trackedChangeStatus === source.trackedChangeStatus && run.undoStatus === source.undoStatus) return run;
|
|
369
|
+
// An absent local settlement has no edits to its metadata or auxiliary
|
|
370
|
+
// evidence. Retain the stored container as well as the terminal fields.
|
|
371
|
+
const target = !run.settlement && !run.settlementFacts ? { ...run, settlement: factsOf(source) } : run;
|
|
372
|
+
return copyRunReviewState(target, source);
|
|
373
|
+
});
|
|
374
|
+
return { ...incoming, runs };
|
|
375
|
+
}
|
|
324
376
|
var newer = (Date.parse(incoming.updatedAt) || 0) >= (Date.parse(existing.updatedAt) || 0);
|
|
325
377
|
if (!newer && !explicitReview) return incoming;
|
|
326
378
|
var base = newer ? incoming : existing;
|
|
@@ -404,6 +456,8 @@
|
|
|
404
456
|
compactSessionBatchActions: compactSessionBatchActions,
|
|
405
457
|
compactStructuredEventValue: compactStructuredEventValue,
|
|
406
458
|
mergeSessionReviewState: mergeSessionReviewState,
|
|
459
|
+
copyRunReviewState: copyRunReviewState,
|
|
460
|
+
reviewStateSnapshot: reviewStateSnapshot,
|
|
407
461
|
compactRunsForStorage: compactRunsForStorage,
|
|
408
462
|
compactRunActionPayload: compactRunActionPayload,
|
|
409
463
|
compactProviderSnapshot: compactProviderSnapshot,
|
|
@@ -8,6 +8,41 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
function buildUndoOperations(project, appliedOperations) {
|
|
11
|
+
return buildUndoPlan(project, appliedOperations).undoOperations;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getUndoUnavailableFiles(run = {}) {
|
|
15
|
+
const payload = run.recoveryPayload || run;
|
|
16
|
+
const files = new Map();
|
|
17
|
+
for (const operation of payload.appliedOperations || []) {
|
|
18
|
+
const reason = operation?.undoUnavailable;
|
|
19
|
+
if (typeof reason?.path === 'string' && reason.path === operation?.path
|
|
20
|
+
&& ['stale_source_changed', 'snapshot_undo_unavailable'].includes(reason.code)) {
|
|
21
|
+
files.set(reason.path, { path: reason.path, code: reason.code });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [...files.values()];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function withAvailableUndo(run = {}) {
|
|
28
|
+
const unavailable = new Set(getUndoUnavailableFiles(run).map(item => item.path));
|
|
29
|
+
if (!unavailable.size) return run;
|
|
30
|
+
const result = { ...run };
|
|
31
|
+
for (const field of ['undoOperations', 'undoBaseFiles', 'undoExpectedFiles', 'undoTrackedChanges', 'trackedChangeCaptures']) {
|
|
32
|
+
result[field] = (run[field] || []).filter(item => !unavailable.has(item?.path) && !unavailable.has(item?.to));
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fallback history keeps only bounded reasons, never document bytes or
|
|
38
|
+
// authority to reconstruct a withheld checkpoint.
|
|
39
|
+
function compactUnavailableUndoOperations(run = {}) {
|
|
40
|
+
if (run.forkSnapshot || run.undoStatus === 'applied' || ['accepted', 'rejected'].includes(run.trackedChangeStatus)) return [];
|
|
41
|
+
return getUndoUnavailableFiles(run).filter(item => item.path.length <= 1024).slice(0, 100)
|
|
42
|
+
.map(item => ({ type: 'edit', path: item.path, undoUnavailable: item }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildUndoPlan(project, appliedOperations) {
|
|
11
46
|
const workingFilesByPath = new Map();
|
|
12
47
|
for (const file of project?.files || []) {
|
|
13
48
|
if (typeof file?.path === 'string' && typeof file.content === 'string') {
|
|
@@ -17,6 +52,10 @@
|
|
|
17
52
|
|
|
18
53
|
const sourceFilesByPath = new Map(workingFilesByPath);
|
|
19
54
|
const undo = [];
|
|
55
|
+
const inverseBases = new Map();
|
|
56
|
+
const mappedPaths = new Set((appliedOperations || []).filter(operation => Array.isArray(operation?.appliedPatches))
|
|
57
|
+
.map(operation => operation.path));
|
|
58
|
+
const unavailable = new Map(getUndoUnavailableFiles({ appliedOperations }).map(item => [item.path, item.code]));
|
|
20
59
|
const fullRestoreEditPaths = new Set();
|
|
21
60
|
|
|
22
61
|
for (const operation of appliedOperations || []) {
|
|
@@ -30,23 +69,33 @@
|
|
|
30
69
|
}
|
|
31
70
|
const currentBeforeEdit = workingFilesByPath.get(operation.path);
|
|
32
71
|
if (!fullRestoreEditPaths.has(operation.path)) {
|
|
33
|
-
const
|
|
34
|
-
|
|
72
|
+
const hasAppliedPatches = Array.isArray(operation.appliedPatches) && typeof operation.verifiedContent === 'string';
|
|
73
|
+
const inversePatches = hasAppliedPatches
|
|
74
|
+
? buildVerifiedInversePatches(operation.appliedPatches, operation.verifiedContent)
|
|
75
|
+
: buildInverseTextPatches(sourceFilesByPath.get(operation.path), operation.patches,
|
|
76
|
+
operation.verifiedContent, operation.path);
|
|
77
|
+
let inverse;
|
|
35
78
|
if (inversePatches.length) {
|
|
36
|
-
|
|
79
|
+
inverse = normalizeUndoOperation({
|
|
37
80
|
type: 'edit',
|
|
38
81
|
path: operation.path,
|
|
39
82
|
patches: inversePatches,
|
|
40
83
|
reason: 'Undo edit'
|
|
41
|
-
})
|
|
42
|
-
} else {
|
|
84
|
+
});
|
|
85
|
+
} else if (!hasAppliedPatches) {
|
|
43
86
|
fullRestoreEditPaths.add(operation.path);
|
|
44
|
-
|
|
87
|
+
inverse = normalizeUndoOperation({
|
|
45
88
|
type: 'edit',
|
|
46
89
|
path: operation.path,
|
|
47
90
|
replaceAll: currentBeforeEdit,
|
|
48
91
|
reason: 'Undo edit'
|
|
49
|
-
})
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
unavailable.set(operation.path, 'snapshot_undo_unavailable');
|
|
95
|
+
}
|
|
96
|
+
if (inverse) {
|
|
97
|
+
undo.push(inverse);
|
|
98
|
+
if (typeof operation.verifiedContent === 'string') inverseBases.set(inverse, operation.verifiedContent);
|
|
50
99
|
}
|
|
51
100
|
}
|
|
52
101
|
} else if (operation.type === 'create') {
|
|
@@ -97,7 +146,51 @@
|
|
|
97
146
|
applyOperationToFiles(sourceFilesByPath, { ...operation, verifiedContent: undefined });
|
|
98
147
|
}
|
|
99
148
|
|
|
100
|
-
|
|
149
|
+
// Each inverse was measured in its own verified post-image. Compose in
|
|
150
|
+
// reverse order against the latest image, preserving changes between
|
|
151
|
+
// entries. Empty inverse insertions must never reuse an older offset.
|
|
152
|
+
const composed = [];
|
|
153
|
+
for (const inverse of undo.reverse()) {
|
|
154
|
+
let operation = inverse;
|
|
155
|
+
if (mappedPaths.has(inverse.path) && inverseBases.has(inverse)) {
|
|
156
|
+
const current = workingFilesByPath.get(inverse.path);
|
|
157
|
+
const base = StaleGuard.buildBaseFileLookup([{ path: inverse.path, content: inverseBases.get(inverse) }]);
|
|
158
|
+
const fresh = StaleGuard.checkOperationFreshness(inverse, current, base);
|
|
159
|
+
const prepared = fresh.ok && inverse.patches
|
|
160
|
+
? fresh.preparedPatches || StaleGuard.prepareOperationPatches(inverse, current, base) : fresh;
|
|
161
|
+
if (typeof current !== 'string' || !prepared.ok) {
|
|
162
|
+
unavailable.set(inverse.path, 'stale_source_changed');
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (inverse.patches) operation = { ...inverse, patches: prepared.patches };
|
|
166
|
+
}
|
|
167
|
+
composed.push(operation);
|
|
168
|
+
applyOperationToFiles(workingFilesByPath, operation);
|
|
169
|
+
}
|
|
170
|
+
return { undoOperations: composed.filter(operation => !unavailable.has(operation.path)),
|
|
171
|
+
undoUnavailable: [...unavailable].map(([path, code]) => ({ path, code })) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Invert the ranges actually dispatched to C, not the original Native
|
|
175
|
+
// coordinates in B. Verify against the observed post-image before storing
|
|
176
|
+
// recovery; invalid evidence must not fall back to overwriting C with B.
|
|
177
|
+
function buildVerifiedInversePatches(patches, postImage) {
|
|
178
|
+
const beforeLength = patches.reduce((length, patch) =>
|
|
179
|
+
length + Number(patch?.to) - Number(patch?.from) - String(patch?.insert ?? '').length, postImage.length);
|
|
180
|
+
if (!Number.isSafeInteger(beforeLength) || beforeLength < 0) return [];
|
|
181
|
+
const normalized = normalizeTextPatches(patches, beforeLength);
|
|
182
|
+
if (!normalized.ok || normalized.patches.some(patch => patch.expected.length !== patch.to - patch.from)) return [];
|
|
183
|
+
const inverse = invertPatches(normalized.patches);
|
|
184
|
+
return applyTextPatches(postImage, inverse).ok ? inverse : [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function invertPatches(patches) {
|
|
188
|
+
let offset = 0;
|
|
189
|
+
return patches.map(patch => {
|
|
190
|
+
const from = patch.from + offset;
|
|
191
|
+
offset += patch.insert.length - (patch.to - patch.from);
|
|
192
|
+
return { from, to: from + patch.insert.length, expected: patch.insert, insert: patch.expected };
|
|
193
|
+
});
|
|
101
194
|
}
|
|
102
195
|
|
|
103
196
|
function buildInverseTextPatches(textBeforeEdit, patches, verifiedContent, path) {
|
|
@@ -113,28 +206,20 @@
|
|
|
113
206
|
return [];
|
|
114
207
|
}
|
|
115
208
|
|
|
116
|
-
|
|
117
|
-
const inverse = normalized.patches.map(patch => {
|
|
118
|
-
const from = patch.from + offset;
|
|
119
|
-
const to = from + patch.insert.length;
|
|
120
|
-
offset += patch.insert.length - (patch.to - patch.from);
|
|
121
|
-
return {
|
|
122
|
-
from,
|
|
123
|
-
to,
|
|
124
|
-
expected: patch.insert,
|
|
125
|
-
insert: patch.expected
|
|
126
|
-
};
|
|
127
|
-
});
|
|
209
|
+
const inverse = invertPatches(normalized.patches);
|
|
128
210
|
if (typeof verifiedContent !== 'string' || verifiedContent === applied.text) return inverse;
|
|
129
211
|
// The stored checkpoint uses the verified editor post-image. Its inverse
|
|
130
212
|
// must use those same offsets, while Native patches retain their source
|
|
131
213
|
// representation for later operations in the batch.
|
|
132
|
-
const prepared = StaleGuard.
|
|
214
|
+
const prepared = StaleGuard.prepareStoredInversePatches({ type: 'edit', path, patches: inverse }, verifiedContent,
|
|
133
215
|
StaleGuard.buildBaseFileLookup([{ path, content: applied.text }]));
|
|
134
216
|
return prepared.ok && applyTextPatches(verifiedContent, prepared.patches).ok ? prepared.patches : [];
|
|
135
217
|
}
|
|
136
218
|
|
|
137
219
|
function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = [], previousExpectedFiles = []) {
|
|
220
|
+
const checkpoint = buildUndoCheckpoint(project, operations);
|
|
221
|
+
const unavailable = new Set(checkpoint.undoUnavailable.map(item => item.path));
|
|
222
|
+
const mappedPaths = new Set(operations.filter(operation => Array.isArray(operation?.appliedPatches)).map(operation => operation.path));
|
|
138
223
|
const paths = new Set();
|
|
139
224
|
for (const change of trackedChanges || []) if (change?.path) paths.add(change.path);
|
|
140
225
|
for (const operation of operations || []) {
|
|
@@ -143,7 +228,8 @@
|
|
|
143
228
|
}
|
|
144
229
|
const expectedByPath = new Map();
|
|
145
230
|
for (const file of previousExpectedFiles || []) {
|
|
146
|
-
if (paths.has(file?.path) &&
|
|
231
|
+
if (paths.has(file?.path) && !unavailable.has(file.path) && !mappedPaths.has(file.path)
|
|
232
|
+
&& typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
|
|
147
233
|
expectedByPath.set(file.path, { path: file.path, content: file.content });
|
|
148
234
|
}
|
|
149
235
|
}
|
|
@@ -152,10 +238,9 @@
|
|
|
152
238
|
// content, including when the ZIP baseline used a different representation.
|
|
153
239
|
const hasVerifiedPatches = operations.some(operation => typeof operation?.verifiedContent === 'string'
|
|
154
240
|
&& Array.isArray(operation.patches) && operation.patches.length);
|
|
155
|
-
const
|
|
156
|
-
const restored = checkpoint ? buildExpectedFilesAfterOperations({ files: checkpoint.undoBaseFiles }, checkpoint.undoOperations) : new Map();
|
|
241
|
+
const restored = hasVerifiedPatches ? buildExpectedFilesAfterOperations({ files: checkpoint.undoBaseFiles }, checkpoint.undoOperations) : new Map();
|
|
157
242
|
for (const file of project?.files || []) {
|
|
158
|
-
if (paths.has(file?.path) && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
|
|
243
|
+
if (paths.has(file?.path) && !unavailable.has(file.path) && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
|
|
159
244
|
expectedByPath.set(file.path, { path: file.path, content: restored.get(file.path) ?? file.content });
|
|
160
245
|
}
|
|
161
246
|
}
|
|
@@ -163,11 +248,15 @@
|
|
|
163
248
|
}
|
|
164
249
|
|
|
165
250
|
function buildUndoCheckpoint(project, appliedOperations) {
|
|
166
|
-
const undoOperations =
|
|
251
|
+
const { undoOperations, undoUnavailable } = buildUndoPlan(project, appliedOperations);
|
|
252
|
+
const unavailable = new Map(undoUnavailable.map(item => [item.path, item]));
|
|
167
253
|
const expectedFiles = buildExpectedFilesAfterOperations(project, appliedOperations);
|
|
168
254
|
return {
|
|
169
255
|
undoOperations,
|
|
170
|
-
undoBaseFiles: filesMapToList(selectUndoBaseFiles(expectedFiles, undoOperations))
|
|
256
|
+
undoBaseFiles: filesMapToList(selectUndoBaseFiles(expectedFiles, undoOperations)),
|
|
257
|
+
undoUnavailable,
|
|
258
|
+
appliedOperations: (appliedOperations || []).map(operation => unavailable.has(operation?.path)
|
|
259
|
+
? { ...operation, undoUnavailable: unavailable.get(operation.path) } : operation)
|
|
171
260
|
};
|
|
172
261
|
}
|
|
173
262
|
|
|
@@ -236,6 +325,7 @@
|
|
|
236
325
|
// file creation, backed by both the forward delete and the captured original.
|
|
237
326
|
// This also recovers older Track-on records that omitted that inverse.
|
|
238
327
|
function getFileUndoOperations(run = {}) {
|
|
328
|
+
run = withAvailableUndo(run);
|
|
239
329
|
const operations = (run.undoOperations || []).filter(isFileUndoOperation);
|
|
240
330
|
const before = filesListToMap(run.undoExpectedFiles);
|
|
241
331
|
const after = buildExpectedFilesAfterOperations({ files: run.undoExpectedFiles }, run.appliedOperations);
|
|
@@ -246,6 +336,8 @@
|
|
|
246
336
|
}
|
|
247
337
|
|
|
248
338
|
function prepareRunUndo(run = {}) {
|
|
339
|
+
const original = run;
|
|
340
|
+
run = withAvailableUndo(run);
|
|
249
341
|
const files = getFileUndoOperations(run);
|
|
250
342
|
const restored = new Set(files.filter(operation => operation.type === 'create').map(operation => operation.path));
|
|
251
343
|
const pending = [...files], operations = [];
|
|
@@ -255,10 +347,12 @@
|
|
|
255
347
|
if (index >= 0) operations.push(...pending.splice(index, 1));
|
|
256
348
|
} else if (!restored.has(operation.path)) operations.push(operation);
|
|
257
349
|
}
|
|
258
|
-
return { ...
|
|
350
|
+
return { ...original, undoOperations: [...operations, ...pending],
|
|
351
|
+
...(run !== original ? { undoBaseFiles: run.undoBaseFiles, undoExpectedFiles: run.undoExpectedFiles } : {}) };
|
|
259
352
|
}
|
|
260
353
|
|
|
261
354
|
function textUndoRun(run = {}) {
|
|
355
|
+
run = withAvailableUndo(run);
|
|
262
356
|
const restored = new Set(getFileUndoOperations(run).flatMap(operation => operation.type === 'create' ? [operation.path]
|
|
263
357
|
: operation.type === 'move' ? [operation.path, operation.to] : []));
|
|
264
358
|
return { ...run,
|
|
@@ -332,6 +426,7 @@
|
|
|
332
426
|
}
|
|
333
427
|
|
|
334
428
|
function buildSnapshotRestoreUndo(run = {}) {
|
|
429
|
+
run = withAvailableUndo(run);
|
|
335
430
|
const undoOperations = Array.isArray(run.undoOperations) ? run.undoOperations : [];
|
|
336
431
|
const originalByPath = buildOriginalFilesForSnapshotUndo(run, undoOperations);
|
|
337
432
|
if (!originalByPath.size) {
|
|
@@ -571,6 +666,9 @@
|
|
|
571
666
|
}
|
|
572
667
|
|
|
573
668
|
return {
|
|
669
|
+
getUndoUnavailableFiles,
|
|
670
|
+
withAvailableUndo,
|
|
671
|
+
compactUnavailableUndoOperations,
|
|
574
672
|
isFileUndoOperation,
|
|
575
673
|
getFileUndoOperations,
|
|
576
674
|
prepareRunUndo,
|
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function verifiedRollback(status = {}) {
|
|
16
|
+
const transaction = status.transaction;
|
|
17
|
+
const pair = status.restoredPair;
|
|
18
|
+
const digest = value => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
|
|
19
|
+
return Boolean(transaction?.id && transaction.state === 'rolled_back'
|
|
20
|
+
&& version(transaction.sourceVersion) && version(transaction.targetVersion)
|
|
21
|
+
&& status.installedAligned === true && status.activeVersion === transaction.sourceVersion
|
|
22
|
+
&& pair?.extensionVersion === transaction.sourceVersion && pair.nativeVersion === transaction.sourceVersion
|
|
23
|
+
&& digest(pair.extensionSha256) && digest(pair.nativeSha256)
|
|
24
|
+
&& (!transaction.bootstrapRevision || digest(pair.bootstrapSha256)));
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
function inspectInstalledRuntime(status = {}, options = {}) {
|
|
16
28
|
const result = { installedVersion: version(status.activeVersion), nativeVersion: version(status.runtimeVersion),
|
|
17
29
|
extensionVersion: version(options.extensionVersion), runtimeVersion: version(options.runtimeVersion), state: 'unknown' };
|
|
@@ -24,9 +36,11 @@
|
|
|
24
36
|
if (status.installedAligned !== true) return { ...result, state: 'repair_required' };
|
|
25
37
|
const loaded = [result.nativeVersion, result.extensionVersion, result.runtimeVersion];
|
|
26
38
|
if (loaded.every(value => value === result.installedVersion)) return { ...result, state: 'aligned' };
|
|
27
|
-
|
|
39
|
+
const rollbackReload = verifiedRollback(status) && loaded.every(value =>
|
|
40
|
+
value === status.transaction.sourceVersion || value === status.transaction.targetVersion);
|
|
41
|
+
return { ...result, state: rollbackReload || loaded.every(value => compare(result.installedVersion, value) >= 0)
|
|
28
42
|
? 'reload_required' : 'repair_required' };
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
return Object.freeze({ inspectInstalledRuntime });
|
|
45
|
+
return Object.freeze({ inspectInstalledRuntime, verifiedRollback });
|
|
32
46
|
});
|