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
|
@@ -42,10 +42,15 @@
|
|
|
42
42
|
reason: status === 'completed' ? 'completed' : 'continue_queue'
|
|
43
43
|
})
|
|
44
44
|
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
}
|
|
46
|
+
const canStart = shouldContinue ? options.canStart?.(sessionId) : true;
|
|
47
|
+
const latestQueue = options.getQueue?.(sessionId) || originalQueue;
|
|
48
|
+
const inactive = shouldContinue && canStart !== 'session_transition'
|
|
49
|
+
&& (canStart === false || options.isRunning?.())
|
|
50
|
+
&& latestQueue.some(item => item?.status === 'queued');
|
|
51
|
+
if (!shouldContinue || inactive) {
|
|
52
|
+
const reason = inactive ? 'session_inactive' : status || 'run_did_not_complete';
|
|
53
|
+
const paused = latestQueue.length ? Queue.pauseAll(latestQueue, reason) : latestQueue;
|
|
49
54
|
options.setQueue?.(sessionId, paused);
|
|
50
55
|
try {
|
|
51
56
|
await options.persist?.({
|
|
@@ -55,19 +60,23 @@
|
|
|
55
60
|
const latestQueue = options.getQueue?.(sessionId) || paused;
|
|
56
61
|
options.setQueue?.(
|
|
57
62
|
sessionId,
|
|
58
|
-
Queue.pauseAll(latestQueue,
|
|
63
|
+
Queue.pauseAll(latestQueue, reason)
|
|
59
64
|
);
|
|
60
65
|
throw error;
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
options.onChange?.(sessionId);
|
|
64
|
-
if (shouldContinue) {
|
|
69
|
+
if (shouldContinue && !inactive) {
|
|
65
70
|
scheduleOne(sessionId);
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
function scheduleOne(sessionId) {
|
|
70
|
-
if (scheduling || options.isRunning?.()
|
|
75
|
+
if (scheduling || options.isRunning?.()) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const canStart = options.canStart?.(sessionId);
|
|
79
|
+
if (canStart === false || canStart === 'session_transition') {
|
|
71
80
|
return false;
|
|
72
81
|
}
|
|
73
82
|
const initialQueue = options.getQueue?.(sessionId) || [];
|
|
@@ -195,7 +204,7 @@
|
|
|
195
204
|
setQueue: (sessionId, pendingInputs) => options.setQueue?.(sessionId, pendingInputs),
|
|
196
205
|
persist: persistenceOptions => options.save?.(persistenceOptions),
|
|
197
206
|
isRunning: () => Boolean(options.getCurrentRun?.()),
|
|
198
|
-
canStart: sessionId => options.canStart?.(sessionId)
|
|
207
|
+
canStart: sessionId => options.canStart?.(sessionId),
|
|
199
208
|
start: (item, sessionId) => startQueuedInput(item, sessionId),
|
|
200
209
|
onChange: () => render(),
|
|
201
210
|
onDequeued: (item, context) => options.removeGuidance?.(item.id, {
|
|
@@ -444,12 +453,17 @@
|
|
|
444
453
|
}
|
|
445
454
|
|
|
446
455
|
async function startQueuedInput(item, sessionId) {
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
456
|
+
const assertCanStart = () => {
|
|
457
|
+
const canStart = options.canStart?.(sessionId);
|
|
458
|
+
if (canStart === false || canStart === 'session_transition' || options.getCurrentRun?.()) {
|
|
459
|
+
const error = new Error('Queued input cannot start in the current session.');
|
|
460
|
+
error.code = 'queue_session_not_active';
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
assertCanStart();
|
|
452
465
|
await options.applyQueuedInput?.(item);
|
|
466
|
+
assertCanStart();
|
|
453
467
|
await options.run?.(item);
|
|
454
468
|
}
|
|
455
469
|
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
const RETRYABLE_FAILURE_CODES = new Set([
|
|
24
24
|
'native_request_failed', 'codex_timeout', 'codex_no_usable_result',
|
|
25
|
-
'codex_output_limit', 'stale_source_changed', 'patch_anchor_not_found',
|
|
25
|
+
'codex_output_limit', 'codex_context_limit', 'stale_source_changed', 'patch_anchor_not_found',
|
|
26
26
|
'target_editor_not_ready', 'write_timeout', 'partial_write_needs_review',
|
|
27
27
|
'write_operation_failed'
|
|
28
28
|
]);
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
const retry = buildRecoveryButton(actionKey,
|
|
57
57
|
tx('Edit & resend', '编辑后重发'),
|
|
58
58
|
tx('Put this run\u2019s task back into the composer so you can adjust and resend it.', '把本轮任务填回输入框,修改后可直接重发。'));
|
|
59
|
+
retry.dataset.recoveryAction = 'edit-resend';
|
|
59
60
|
retry.addEventListener('click', clickEvent => {
|
|
60
61
|
clickEvent.stopPropagation();
|
|
61
62
|
refillComposerForRetry(run || null);
|
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
})(typeof window !== 'undefined' ? window : globalThis, function (RunCompletionMeta) {
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
|
-
const { splitFlatCompletionReport, projectCompletionBody, projectCompletionMeta, summarizeCompletionMeta } = RunCompletionMeta;
|
|
7
|
+
const { splitFlatCompletionReport, projectNotStartedRun, projectFinishedRun, projectCompletionConclusion, projectCompletionBody, projectCompletionMeta, summarizeCompletionMeta } = RunCompletionMeta;
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
function create(deps = {}) {
|
|
11
11
|
const {
|
|
12
12
|
tr,
|
|
13
|
-
getLocale,
|
|
14
13
|
forkRunFromNode,
|
|
15
14
|
document: documentRef = globalThis.document,
|
|
16
15
|
navigator: navigatorRef = globalThis.navigator
|
|
@@ -21,11 +20,27 @@
|
|
|
21
20
|
const reports = root.querySelectorAll('.run-completion-report');
|
|
22
21
|
const report = reports[reports.length - 1];
|
|
23
22
|
if (!actions || !report) return;
|
|
24
|
-
|
|
23
|
+
// Share one row with the facts summary when it exists; the expandable
|
|
24
|
+
// details list stays below both.
|
|
25
|
+
const factsToggle = report.querySelector(':scope > .run-final-answer__facts > .run-final-answer__facts-sum');
|
|
26
|
+
if (factsToggle) factsToggle.after(actions);
|
|
27
|
+
else {
|
|
28
|
+
// Without a facts summary, the report's own Details toggle shares the row.
|
|
29
|
+
const details = report.querySelector(':scope > .run-outcome-details, :scope > .run-outcome-footer > .run-outcome-details');
|
|
30
|
+
if (details) {
|
|
31
|
+
let footer = details.parentElement.matches('.run-outcome-footer') ? details.parentElement : null;
|
|
32
|
+
if (!footer) {
|
|
33
|
+
footer = documentRef.createElement('div');
|
|
34
|
+
footer.className = 'run-outcome-footer';
|
|
35
|
+
details.before(footer);
|
|
36
|
+
footer.append(details);
|
|
37
|
+
}
|
|
38
|
+
footer.append(actions);
|
|
39
|
+
} else report.append(actions);
|
|
40
|
+
}
|
|
25
41
|
actions.hidden = false;
|
|
26
42
|
configureCopyButton(root);
|
|
27
43
|
configureForkButton(root, run);
|
|
28
|
-
configureCompletedTime(actions, run);
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
function configureForkButton(root, run) {
|
|
@@ -40,7 +55,7 @@
|
|
|
40
55
|
button.disabled = !available;
|
|
41
56
|
button.title = tr(available ? 'forkRunTitle' : 'forkRunUnavailable');
|
|
42
57
|
button.setAttribute('aria-label', button.title);
|
|
43
|
-
button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 10h3.5c3.5 0 4.2-4 7.5-4h2m-2.5-2.5L16 6l-2.5 2.5M6.5 10c3.5 0 4.2 4 7.5 4h2m-2.5-2.5L16 14l-2.5 2.5"/></svg>';
|
|
58
|
+
button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><path transform="translate(.5 0)" d="M3 10h3.5c3.5 0 4.2-4 7.5-4h2m-2.5-2.5L16 6l-2.5 2.5M6.5 10c3.5 0 4.2 4 7.5 4h2m-2.5-2.5L16 14l-2.5 2.5"/></svg>';
|
|
44
59
|
if (!available) return;
|
|
45
60
|
button.addEventListener('click', async event => {
|
|
46
61
|
event.stopPropagation();
|
|
@@ -55,34 +70,22 @@
|
|
|
55
70
|
});
|
|
56
71
|
}
|
|
57
72
|
|
|
58
|
-
function configureCompletedTime(actions, run) {
|
|
59
|
-
const target = actions.querySelector('[data-run-completed-time]');
|
|
60
|
-
const finishedAt = String(run.finishedAt || '').trim();
|
|
61
|
-
const date = finishedAt ? new Date(finishedAt) : null;
|
|
62
|
-
if (!target || !date || !Number.isFinite(date.getTime())) return;
|
|
63
|
-
target.hidden = false;
|
|
64
|
-
target.textContent = new Intl.DateTimeFormat(getLocale(), {
|
|
65
|
-
hour: '2-digit',
|
|
66
|
-
minute: '2-digit'
|
|
67
|
-
}).format(date);
|
|
68
|
-
target.title = date.toLocaleString(getLocale());
|
|
69
|
-
}
|
|
70
|
-
|
|
71
73
|
function configureCopyButton(root) {
|
|
72
74
|
const existing = root.querySelector('[data-run-copy]');
|
|
73
75
|
const button = existing.cloneNode(true);
|
|
74
76
|
existing.replaceWith(button);
|
|
75
|
-
const
|
|
76
|
-
const copyText = String(finalAnswer?.innerText || finalAnswer?.textContent || '').trim();
|
|
77
|
+
const available = Boolean(readConclusionText(root).trim());
|
|
77
78
|
button.hidden = false;
|
|
78
|
-
button.disabled = !
|
|
79
|
+
button.disabled = !available;
|
|
79
80
|
button.title = tr('copyConclusionTitle');
|
|
80
81
|
button.setAttribute('aria-label', button.title);
|
|
81
|
-
button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><rect x="6.5" y="6.5" width="9" height="9" rx="2"/><path d="M13.5 6.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6.5a2 2 0 0 0 2 2h1.5"/></svg>';
|
|
82
|
-
if (!
|
|
82
|
+
button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><g transform="translate(.75 .75)"><rect x="6.5" y="6.5" width="9" height="9" rx="2"/><path d="M13.5 6.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6.5a2 2 0 0 0 2 2h1.5"/></g></svg>';
|
|
83
|
+
if (!available) return;
|
|
83
84
|
button.addEventListener('click', async event => {
|
|
84
85
|
event.stopPropagation();
|
|
85
86
|
try {
|
|
87
|
+
const copyText = readConclusionText(root);
|
|
88
|
+
if (!copyText.trim()) return;
|
|
86
89
|
await writeClipboardText(copyText);
|
|
87
90
|
button.title = tr('copyConclusionDone');
|
|
88
91
|
} catch (_error) {
|
|
@@ -92,6 +95,52 @@
|
|
|
92
95
|
});
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
function readConclusionText(root) {
|
|
99
|
+
const answer = root.querySelector('.run-completion-report .run-final-answer');
|
|
100
|
+
if (!answer) return '';
|
|
101
|
+
return extractConclusionText(answer).replace(/\u200b/g, '').replace(/[ \t]+$/gm, '').trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extractConclusionText(node) {
|
|
105
|
+
if (node.nodeType === 3) return String(node.textContent || '').replace(/\s+/g, ' ');
|
|
106
|
+
const tag = node.tagName?.toLowerCase();
|
|
107
|
+
if (tag === 'br') return '\n';
|
|
108
|
+
if (tag === 'pre' || tag === 'code') return node.textContent || '';
|
|
109
|
+
if (node.classList?.contains('run-math')) {
|
|
110
|
+
const source = node.dataset.mathRendered === 'false'
|
|
111
|
+
? node.textContent || ''
|
|
112
|
+
: node.querySelector('annotation[encoding="application/x-tex"]')?.textContent || '';
|
|
113
|
+
const delimiter = node.dataset.mathDisplay === 'block' ? '$$' : '$';
|
|
114
|
+
return source ? `${delimiter}${source}${delimiter}` : '';
|
|
115
|
+
}
|
|
116
|
+
const children = Array.from(node.childNodes || []);
|
|
117
|
+
if (tag === 'ul' || tag === 'ol') {
|
|
118
|
+
const start = Number(node.getAttribute('start') || 1);
|
|
119
|
+
return children.filter(child => child.tagName?.toLowerCase() === 'li')
|
|
120
|
+
.map((child, index) => `${tag === 'ol' ? `${start + index}. ` : '- '}${extractConclusionText(child)}`)
|
|
121
|
+
.join('\n');
|
|
122
|
+
}
|
|
123
|
+
if (tag === 'table') {
|
|
124
|
+
return Array.from(node.querySelectorAll('tr'), row => Array.from(row.children,
|
|
125
|
+
cell => extractConclusionText(cell)).join('\t')).join('\n');
|
|
126
|
+
}
|
|
127
|
+
let text = '';
|
|
128
|
+
let previousBlock = false;
|
|
129
|
+
for (const child of children) {
|
|
130
|
+
const value = extractConclusionText(child);
|
|
131
|
+
if (!value) continue;
|
|
132
|
+
const block = /^(p|h[1-6]|blockquote|ul|ol|pre|table|div)$/.test(child.tagName?.toLowerCase())
|
|
133
|
+
|| child.dataset?.mathDisplay === 'block';
|
|
134
|
+
if (text && (previousBlock || block)) {
|
|
135
|
+
const newlines = (text.match(/\n*$/)?.[0].length || 0) + (value.match(/^\n*/)?.[0].length || 0);
|
|
136
|
+
text += '\n'.repeat(Math.max(0, 2 - newlines));
|
|
137
|
+
}
|
|
138
|
+
text += value;
|
|
139
|
+
previousBlock = block;
|
|
140
|
+
}
|
|
141
|
+
return text;
|
|
142
|
+
}
|
|
143
|
+
|
|
95
144
|
async function writeClipboardText(text) {
|
|
96
145
|
if (navigatorRef?.clipboard?.writeText) {
|
|
97
146
|
await navigatorRef.clipboard.writeText(text);
|
|
@@ -109,7 +158,7 @@
|
|
|
109
158
|
if (!copied) throw new Error('clipboard_copy_failed');
|
|
110
159
|
}
|
|
111
160
|
|
|
112
|
-
return Object.freeze({ configureResultActions, projectUndoAvailability, splitFlatCompletionReport, projectCompletionBody, projectCompletionMeta, summarizeCompletionMeta });
|
|
161
|
+
return Object.freeze({ configureResultActions, projectUndoAvailability, splitFlatCompletionReport, projectNotStartedRun, projectFinishedRun, projectCompletionConclusion, projectCompletionBody, projectCompletionMeta, summarizeCompletionMeta });
|
|
113
162
|
}
|
|
114
163
|
|
|
115
164
|
|
|
@@ -58,9 +58,12 @@
|
|
|
58
58
|
if (Modules.StorageDb?.sharedSessionsEnabled?.() !== true) return;
|
|
59
59
|
findRunRecord(runId).undoOwnerProtocol = 1;
|
|
60
60
|
try { await flushQueuedSaveState({ requireDurable: true, reviewRunIds: [runId] }); }
|
|
61
|
-
catch (cause) {
|
|
61
|
+
catch (cause) {
|
|
62
|
+
if (cause.code === 'shared_session_conflict' && cause.durableReviewRunIds?.includes(runId)) notify?.(cause.message);
|
|
63
|
+
else throw Object.assign(new Error('The Undo owner marker could not be durably saved.'), { code: 'review_state_not_persisted', cause });
|
|
64
|
+
}
|
|
62
65
|
reviewOwners.get(runId)?.();
|
|
63
|
-
const saved = await Modules.StorageDb.getRecord('sessions', getState().activeSessionId);
|
|
66
|
+
const saved = await Modules.StorageDb.getRecord('sessions', getState().activeSessionId, { adopt: false });
|
|
64
67
|
if (saved?.runs?.find(run => run.id === runId)?.undoOwnerProtocol !== 1)
|
|
65
68
|
throw Object.assign(new Error('The Undo owner marker was not durably retained.'), { code: 'review_state_not_persisted' });
|
|
66
69
|
reviewOwners.get(runId)?.();
|
|
@@ -68,7 +71,7 @@
|
|
|
68
71
|
async function withSharedReviewOwnership(runId, work) {
|
|
69
72
|
const db = Modules.StorageDb;
|
|
70
73
|
if (db?.sharedSessionsEnabled?.() !== true) return work();
|
|
71
|
-
|
|
74
|
+
let run = findRunRecord(runId);
|
|
72
75
|
if (!run || trackedChangeInFlight.has(runId)) return;
|
|
73
76
|
const assertCurrent = undoOwner(run);
|
|
74
77
|
const sessionId = getState().activeSessionId;
|
|
@@ -77,8 +80,9 @@
|
|
|
77
80
|
reviewOwners.set(runId, assertOwner);
|
|
78
81
|
try {
|
|
79
82
|
assertCurrent();
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
// Only the run is refreshed here; keep the visible session's merge baseline.
|
|
84
|
+
const session = await db.getRecord('sessions', sessionId, { adopt: false });
|
|
85
|
+
run = assertCurrent();
|
|
82
86
|
const latest = session?.runs?.find(record => record.id === runId);
|
|
83
87
|
if (!latest || session.projectId !== getRunProjectIdForWriteback(run) || session.accountScopeId !== getAccountScopeId())
|
|
84
88
|
throw Object.assign(new Error('The shared Undo run is no longer available in its original scope.'), { code: 'review_context_changed' });
|
|
@@ -162,9 +166,10 @@
|
|
|
162
166
|
trackedChangeInFlight.set(runId, 'reject'); refreshRunCardControls(runId);
|
|
163
167
|
try { return await work(); }
|
|
164
168
|
catch (error) {
|
|
165
|
-
let current
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
let current; try { current = assertOwner(); } catch (_stale) {}
|
|
170
|
+
const durableReviewConflict = error.code === 'shared_session_conflict' && error.durableReviewRunIds?.includes(runId);
|
|
171
|
+
if (current && error.code !== 'review_context_changed' && !durableReviewConflict) {
|
|
172
|
+
if (current.undoStatus === 'running') setRunUndoStatus(runId, 'partial');
|
|
168
173
|
appendRunRecordEvent(runId, { title: tr('undoNoTraceTitle'), status: 'failed',
|
|
169
174
|
detail: { [tr('detailReason')]: error.message } });
|
|
170
175
|
}
|
|
@@ -174,18 +179,59 @@
|
|
|
174
179
|
} finally { trackedChangeInFlight.delete(runId); refreshRunCardControls(runId); }
|
|
175
180
|
}
|
|
176
181
|
const withUndoLock = (runId, work) => withSharedReviewOwnership(runId, () => withLocalUndoLock(runId, work));
|
|
177
|
-
|
|
182
|
+
// Undo restores the source; with Auto compile on, refresh the PDF the way a write does.
|
|
183
|
+
async function recompileAfterUndo(runId, work) {
|
|
184
|
+
const before = findRunRecord(runId);
|
|
185
|
+
const prior = `${before?.undoStatus || ''}|${before?.trackedChangeStatus || ''}`;
|
|
186
|
+
const result = await work();
|
|
187
|
+
const run = findRunRecord(runId);
|
|
188
|
+
const undone = ['applied', 'partial'].includes(run?.undoStatus) || run?.trackedChangeStatus === 'rejected';
|
|
189
|
+
if (undone && `${run.undoStatus || ''}|${run.trackedChangeStatus || ''}` !== prior
|
|
190
|
+
&& getState()?.autoRecompile !== false && run.runProjectId && run.runProjectId === getCurrentProjectId?.()) {
|
|
191
|
+
Promise.resolve(callPageBridge('triggerCompile', { preferUiClick: true, waitForSaveMs: 5000,
|
|
192
|
+
runProjectId: run.runProjectId })).catch(() => {});
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
// Inside the lock: a stale page that adopts another page's finished Undo never reaches its work.
|
|
197
|
+
const undoRun = runId => withUndoLock(runId, () => recompileAfterUndo(runId, () => executeUndo(runId)));
|
|
178
198
|
const acceptRun = runId => withSharedReviewOwnership(runId, () => executeAcceptRun(runId));
|
|
179
|
-
const undoRunTrackedChanges = (runId, run) => withUndoLock(runId,
|
|
199
|
+
const undoRunTrackedChanges = (runId, run) => withUndoLock(runId,
|
|
200
|
+
() => recompileAfterUndo(runId, () => executeTrackedUndo(runId, run)));
|
|
180
201
|
|
|
181
202
|
function undoOwner(run) {
|
|
182
|
-
const
|
|
203
|
+
const sessionId = getState().activeSessionId;
|
|
183
204
|
const scope = [getCurrentProjectId?.(), getAccountScopeId?.(), getRouteGeneration?.()];
|
|
205
|
+
const provenance = value => JSON.stringify([value?.id, value?.runProjectId || '', value?.startedAt || '',
|
|
206
|
+
value?.runAccountScopeId || scope[1] || '', value?.nativeEventSeq || 0, value?.codexThreadId || '', value?.codexTurnId || '', value?.executionSnapshot || null]);
|
|
207
|
+
const sessionScope = () => {
|
|
208
|
+
const session = getState().sessions?.find(item => item.id === sessionId);
|
|
209
|
+
return JSON.stringify([session?.projectId || '', session?.accountScopeId || '']);
|
|
210
|
+
};
|
|
211
|
+
const source = provenance(run), owningSession = sessionScope();
|
|
184
212
|
return () => {
|
|
185
213
|
reviewOwners.get(run.id)?.();
|
|
186
|
-
|
|
214
|
+
const live = findRunRecord(run.id);
|
|
215
|
+
if (getState().activeSessionId !== sessionId || sessionScope() !== owningSession || !live
|
|
216
|
+
|| !(live === run || Modules.SessionState?.sameRunIdentity?.(run, live)) || provenance(live) !== source
|
|
187
217
|
|| JSON.stringify(scope) !== JSON.stringify([getCurrentProjectId?.(), getAccountScopeId?.(), getRouteGeneration?.()]))
|
|
188
218
|
throw Object.assign(new Error('The Undo project, account or session changed.'), { code: 'review_context_changed' });
|
|
219
|
+
return live;
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function checkpointGuard(run, assertOwner) {
|
|
224
|
+
const snapshot = value => {
|
|
225
|
+
const normalized = Modules.SessionState?.normalizeRuns?.([value])?.[0] || value;
|
|
226
|
+
return JSON.stringify(['appliedOperations', 'undoOperations', 'undoBaseFiles', 'undoExpectedFiles',
|
|
227
|
+
'undoTrackedChanges', 'trackedChangeCaptures', 'undoStatus', 'trackedChangeStatus', 'forkSnapshot']
|
|
228
|
+
.map(key => normalized[key]));
|
|
229
|
+
};
|
|
230
|
+
const expected = snapshot(run);
|
|
231
|
+
return () => {
|
|
232
|
+
const live = assertOwner();
|
|
233
|
+
if (snapshot(live) !== expected) throw Object.assign(new Error('The Undo project, account or session changed.'), { code: 'review_context_changed' });
|
|
234
|
+
return live;
|
|
189
235
|
};
|
|
190
236
|
}
|
|
191
237
|
|
|
@@ -198,9 +244,12 @@ function undoOwner(run) {
|
|
|
198
244
|
}
|
|
199
245
|
|
|
200
246
|
async function executeUndo(runId) {
|
|
201
|
-
|
|
247
|
+
let run = findRunRecord(runId);
|
|
202
248
|
if (run) Object.assign(run, Modules.UndoOperations.prepareRunUndo(run));
|
|
249
|
+
const mirrorOwner = { projectId: getRunProjectIdForWriteback(run), runAccountScopeId: run?.runAccountScopeId || getAccountScopeId?.() || '',
|
|
250
|
+
sessionId: getState().activeSessionId };
|
|
203
251
|
const assertCurrent = undoOwner(run);
|
|
252
|
+
let assertInputs = checkpointGuard(run, assertCurrent);
|
|
204
253
|
if (!getRunUndoCount(run) || run.undoStatus === 'applied') {
|
|
205
254
|
return;
|
|
206
255
|
}
|
|
@@ -210,7 +259,7 @@ async function executeUndo(runId) {
|
|
|
210
259
|
const pendingMirror = getPendingMirrorRefresh?.();
|
|
211
260
|
if (pendingMirror) {
|
|
212
261
|
await pendingMirror.catch(() => {});
|
|
213
|
-
|
|
262
|
+
run = assertInputs();
|
|
214
263
|
}
|
|
215
264
|
|
|
216
265
|
if ((Array.isArray(run.undoTrackedChanges) && run.undoTrackedChanges.length) || hasTrackedEditorUndo(run)
|
|
@@ -272,8 +321,9 @@ async function executeUndo(runId) {
|
|
|
272
321
|
}
|
|
273
322
|
}
|
|
274
323
|
|
|
275
|
-
|
|
324
|
+
run = assertInputs();
|
|
276
325
|
const deadlineAt = await prepareUndoDispatch(runId);
|
|
326
|
+
run = assertInputs();
|
|
277
327
|
setRunUndoStatus(runId, 'running');
|
|
278
328
|
appendRunRecordEvent(runId, {
|
|
279
329
|
title: tr('undoNoTraceStarted'),
|
|
@@ -281,7 +331,8 @@ async function executeUndo(runId) {
|
|
|
281
331
|
detail: { [tr('detailWillUndo')]: formatOperationFiles(selectedOperations) }
|
|
282
332
|
});
|
|
283
333
|
|
|
284
|
-
|
|
334
|
+
assertInputs = checkpointGuard(run, assertCurrent);
|
|
335
|
+
await persistUndoOwnerMarker(runId); run = assertInputs();
|
|
285
336
|
const result = await callPageBridge('applyOperations', {
|
|
286
337
|
operations: selectedOperations, deadlineAt, reviewOwnerToken: reviewOwners.get(runId)?.token,
|
|
287
338
|
baseFiles: run.undoBaseFiles || [],
|
|
@@ -289,9 +340,11 @@ async function executeUndo(runId) {
|
|
|
289
340
|
// Undo remains bound to its original project, including after navigation.
|
|
290
341
|
runProjectId: getRunProjectIdForWriteback(run)
|
|
291
342
|
});
|
|
292
|
-
|
|
343
|
+
// The Page may have changed the original document even if this view moved
|
|
344
|
+
// while awaiting its reply. Fence that project's mirror before rechecking.
|
|
345
|
+
writebackOrchestrator.invalidateMirrorAfterUndo(runId, mirrorOwner.projectId, result, mirrorOwner);
|
|
346
|
+
run = assertInputs();
|
|
293
347
|
const undoApplied = isUndoResultEffectivelyApplied(run, result);
|
|
294
|
-
writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
|
|
295
348
|
appendUndoReviewingPolicyEvent(runId, result.reviewingPolicy);
|
|
296
349
|
appendRunRecordEvent(runId, {
|
|
297
350
|
title: tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
|
|
@@ -320,7 +373,10 @@ async function executeUndo(runId) {
|
|
|
320
373
|
}
|
|
321
374
|
|
|
322
375
|
async function executeTrackedUndo(runId, run) {
|
|
376
|
+
const mirrorOwner = { projectId: getRunProjectIdForWriteback(run), runAccountScopeId: run?.runAccountScopeId || getAccountScopeId?.() || '',
|
|
377
|
+
sessionId: getState().activeSessionId };
|
|
323
378
|
const assertCurrent = undoOwner(run);
|
|
379
|
+
let assertInputs = checkpointGuard(run, assertCurrent);
|
|
324
380
|
const fileUndoOperations = Modules.UndoOperations.getFileUndoOperations(run);
|
|
325
381
|
const textRun = Modules.UndoOperations.textUndoRun(run);
|
|
326
382
|
const trackedUndo = textRun.undoTrackedChanges.length > 0;
|
|
@@ -343,8 +399,9 @@ async function executeTrackedUndo(runId, run) {
|
|
|
343
399
|
return;
|
|
344
400
|
}
|
|
345
401
|
|
|
346
|
-
|
|
402
|
+
run = assertInputs();
|
|
347
403
|
const deadlineAt = await prepareUndoDispatch(runId);
|
|
404
|
+
run = assertInputs();
|
|
348
405
|
// Keep the review lifecycle until every pending part has settled.
|
|
349
406
|
// Its local lock stays active while verified recovery progress is persisted.
|
|
350
407
|
const lifecycleReject = (trackedUndo || fileUndoOperations.length > 0) && isTrackedChangeLifecycleRun(run);
|
|
@@ -363,7 +420,8 @@ async function executeTrackedUndo(runId, run) {
|
|
|
363
420
|
|
|
364
421
|
let result;
|
|
365
422
|
try {
|
|
366
|
-
|
|
423
|
+
assertInputs = checkpointGuard(run, assertCurrent);
|
|
424
|
+
await persistUndoOwnerMarker(runId); run = assertInputs();
|
|
367
425
|
const trackedUndoPostFilesAtDispatch = buildTrackedUndoPostFiles(textRun);
|
|
368
426
|
result = hasTextUndo ? await callPageBridge('rejectTrackedChanges', {
|
|
369
427
|
trackedChanges: textRun.undoTrackedChanges || [], deadlineAt, reviewOwnerToken: reviewOwners.get(runId)?.token,
|
|
@@ -375,12 +433,13 @@ async function executeTrackedUndo(runId, run) {
|
|
|
375
433
|
// The page-side project guard still refuses writes after navigation.
|
|
376
434
|
runProjectId: getRunProjectIdForWriteback(run)
|
|
377
435
|
}) : { ok: true, applied: [], skipped: [] };
|
|
378
|
-
|
|
436
|
+
run = assertInputs();
|
|
379
437
|
if (hasTextUndo) WritebackSettlement.attachUndoNotVerifiedFailure(textRun, result, {
|
|
380
438
|
buildFailure: buildContentFailure, postFiles: trackedUndoPostFilesAtDispatch });
|
|
381
439
|
if (fileUndoOperations.length && lifecycleReject && hasTextUndo) {
|
|
382
440
|
WritebackSettlement.attachUndoNotVerifiedFailure(textRun, result, { buildFailure: buildContentFailure, postFiles: trackedUndoPostFilesAtDispatch });
|
|
383
441
|
await applyTrackedChangeSettlement(runId, 'reject', { ...result, ok: false }, { assertCurrent });
|
|
442
|
+
run = assertCurrent(); assertInputs = checkpointGuard(run, assertCurrent);
|
|
384
443
|
}
|
|
385
444
|
if (fileUndoOperations.length) {
|
|
386
445
|
let createdResult;
|
|
@@ -399,8 +458,8 @@ async function executeTrackedUndo(runId, run) {
|
|
|
399
458
|
operation, result: { ok: false, code: 'undo_operation_failed', reason: error.message }
|
|
400
459
|
})) };
|
|
401
460
|
}
|
|
402
|
-
assertCurrent();
|
|
403
461
|
result = Modules.UndoOperations.mergeFileUndoResult(result, createdResult, fileUndoOperations);
|
|
462
|
+
run = assertInputs();
|
|
404
463
|
}
|
|
405
464
|
appendRunRecordEvent(runId, {
|
|
406
465
|
title: fileUndoOperations.length ? tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }) : trackedUndo
|
|
@@ -431,28 +490,29 @@ async function executeTrackedUndo(runId, run) {
|
|
|
431
490
|
await applyLegacyUndoSettlement(runId, result.ok === true && !result.skipped?.length ? 'applied' : 'partial', result);
|
|
432
491
|
} finally {
|
|
433
492
|
// Known Page changes require invalidation even when a durable checkpoint fails.
|
|
434
|
-
if (result) writebackOrchestrator.invalidateMirrorAfterUndo(runId,
|
|
493
|
+
if (result) writebackOrchestrator.invalidateMirrorAfterUndo(runId, mirrorOwner.projectId, result, mirrorOwner);
|
|
435
494
|
}
|
|
436
495
|
}
|
|
437
496
|
|
|
438
497
|
function reviewOwner(run) {
|
|
439
|
-
const
|
|
440
|
-
|
|
498
|
+
const projectId = getCurrentProjectId(), accountScopeId = getAccountScopeId();
|
|
499
|
+
const assertRun = undoOwner(run);
|
|
441
500
|
const assert = () => {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
501
|
+
let live;
|
|
502
|
+
try { live = assertRun(); } catch (error) {
|
|
503
|
+
if (error.code === 'review_context_changed') error.message = 'The original review project, account or session changed.';
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
if (!projectId || !accountScopeId || live.runProjectId !== projectId
|
|
507
|
+
|| (live.runAccountScopeId && live.runAccountScopeId !== accountScopeId))
|
|
448
508
|
throw Object.assign(new Error('The original review project, account or session changed.'), { code: 'review_context_changed' });
|
|
449
|
-
return
|
|
509
|
+
return live;
|
|
450
510
|
};
|
|
451
|
-
assert(); return {
|
|
511
|
+
assert(); return { projectId, accountScopeId, assert };
|
|
452
512
|
}
|
|
453
513
|
|
|
454
514
|
async function executeAcceptRun(runId) {
|
|
455
|
-
|
|
515
|
+
let run = findRunRecord(runId);
|
|
456
516
|
if (!run || !isTrackedChangeLifecycleRun(run)) {
|
|
457
517
|
return;
|
|
458
518
|
}
|
|
@@ -471,6 +531,7 @@ async function executeAcceptRun(runId) {
|
|
|
471
531
|
}
|
|
472
532
|
|
|
473
533
|
const owner = reviewOwner(run);
|
|
534
|
+
const assertInputs = checkpointGuard(run, owner.assert);
|
|
474
535
|
trackedChangeInFlight.set(runId, 'accept');
|
|
475
536
|
refreshRunCardControls(runId);
|
|
476
537
|
appendRunRecordEvent(runId, {
|
|
@@ -481,6 +542,7 @@ async function executeAcceptRun(runId) {
|
|
|
481
542
|
|
|
482
543
|
try {
|
|
483
544
|
const deadlineAt = await prepareUndoDispatch(runId);
|
|
545
|
+
run = assertInputs();
|
|
484
546
|
const result = await callPageBridge('acceptTrackedChanges', { deadlineAt, reviewOwnerToken: reviewOwners.get(runId)?.token,
|
|
485
547
|
trackedChanges: run.undoTrackedChanges || [],
|
|
486
548
|
expectedFiles: run.undoExpectedFiles || [],
|
|
@@ -488,7 +550,7 @@ async function executeAcceptRun(runId) {
|
|
|
488
550
|
appliedOperations: Array.isArray(run.appliedOperations) ? run.appliedOperations : [],
|
|
489
551
|
runProjectId: owner.projectId, accountScopeId: owner.accountScopeId
|
|
490
552
|
});
|
|
491
|
-
|
|
553
|
+
run = assertInputs();
|
|
492
554
|
appendAcceptDiagnosticEvents(runId, Array.isArray(result.diagnostics) ? result.diagnostics : []);
|
|
493
555
|
appendRunRecordEvent(runId, {
|
|
494
556
|
title: tr('runAcceptTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
|
|
@@ -565,10 +627,13 @@ async function applyLegacyUndoSettlement(runId, undoStatus, result) {
|
|
|
565
627
|
})
|
|
566
628
|
);
|
|
567
629
|
await Modules.ScopedPersistenceCoordinator.applyReviewTransition({ getState, runId, transition,
|
|
568
|
-
legacyUndo: true, persist: flushQueuedSaveState, assertCurrent });
|
|
630
|
+
legacyUndo: true, persist: flushQueuedSaveState, assertCurrent, SessionState: Modules.SessionState });
|
|
569
631
|
refreshRunCardControls(runId);
|
|
570
632
|
}
|
|
571
|
-
|
|
633
|
+
// From the shared review lock until settlement, a replaced view would
|
|
634
|
+
// invalidate the action's ownership checks after Page effects.
|
|
635
|
+
const hasActiveReview = () => reviewOwners.size > 0;
|
|
636
|
+
return { recoverInterruptedUndoStatuses, undoRun, undoRunTrackedChanges, acceptRun, applyLegacyUndoSettlement, hasActiveReview };
|
|
572
637
|
}
|
|
573
638
|
return { create };
|
|
574
639
|
});
|