codex-overleaf-link 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -24
- package/README.zh-CN.md +27 -24
- package/extension/src/content/contentRuntime.js +74 -73
- package/extension/src/content/generated/content.bundle.js +657 -445
- package/extension/src/content/generated/content.bundle.meta.json +3 -3
- package/extension/src/content/panelMaintenance.js +8 -4
- package/extension/src/content/recentProjects.js +354 -355
- package/extension/src/content/runTimelineView.js +8 -10
- package/extension/src/content/scopedPersistenceCoordinator.js +2 -1
- package/extension/src/content/scopedPersistencePanelState.js +45 -2
- package/extension/src/content/sessionPersistence.js +4 -1
- package/extension/src/content/settingsPanel.js +16 -2
- package/extension/src/page/saveState.js +44 -1
- package/extension/src/page/trackedChangesLifecycle.js +4 -1
- package/extension/src/page/treeOperations.js +10 -27
- package/extension/src/page/writebackRouter.js +1 -1
- package/extension/src/pageBridge.js +3 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/i18n.js +94 -40
- package/extension/src/shared/storageDb.js +1 -0
- package/extension/src/shared/storageRunActions.js +44 -0
- package/extension/styles/panel.css +199 -395
- package/native-host/src/codexSessionRunner.js +41 -38
- package/native-host/src/codexSessionTiming.js +68 -0
- package/native-host/src/readProgressGuard.js +84 -17
- package/package.json +1 -1
|
@@ -960,12 +960,12 @@
|
|
|
960
960
|
// same executable state as `pending`.
|
|
961
961
|
function configureLifecycleUndoButton(button, run) {
|
|
962
962
|
const status = run.trackedChangeStatus || '';
|
|
963
|
+
const inFlight = trackedChangeInFlight.get(run.id);
|
|
963
964
|
const projection = projectRunSettlement(run);
|
|
964
965
|
// §7 settlement matrix: needs_review keeps BOTH controls visible AND
|
|
965
966
|
// actionable. Branch placed before the terminal branches so a
|
|
966
967
|
// needs_review run is never treated as terminal.
|
|
967
968
|
if (status === 'needs_review') {
|
|
968
|
-
const inFlight = trackedChangeInFlight.get(run.id);
|
|
969
969
|
button.hidden = false;
|
|
970
970
|
button.disabled = !projection.canUndo || inFlight === 'reject' || inFlight === 'accept';
|
|
971
971
|
button.textContent = tr('undoRun');
|
|
@@ -976,14 +976,14 @@
|
|
|
976
976
|
});
|
|
977
977
|
return;
|
|
978
978
|
}
|
|
979
|
-
if (status === 'rejected') {
|
|
979
|
+
if (status === 'rejected' && !inFlight) {
|
|
980
980
|
button.hidden = false;
|
|
981
981
|
button.disabled = true;
|
|
982
982
|
button.textContent = tr('undoApplied');
|
|
983
983
|
button.title = tr('undoAppliedTitle');
|
|
984
984
|
return;
|
|
985
985
|
}
|
|
986
|
-
if (status === 'accepted') {
|
|
986
|
+
if (status === 'accepted' && !inFlight) {
|
|
987
987
|
// The run was accepted — terminal. Undo stays visible but greyed so the
|
|
988
988
|
// card shows both controls disabled, never removed.
|
|
989
989
|
button.hidden = false;
|
|
@@ -992,11 +992,10 @@
|
|
|
992
992
|
button.title = tr('undoRunTitle');
|
|
993
993
|
return;
|
|
994
994
|
}
|
|
995
|
-
if (status !== 'pending') {
|
|
995
|
+
if (status !== 'pending' && !inFlight) {
|
|
996
996
|
button.hidden = true;
|
|
997
997
|
return;
|
|
998
998
|
}
|
|
999
|
-
const inFlight = trackedChangeInFlight.get(run.id);
|
|
1000
999
|
button.hidden = false;
|
|
1001
1000
|
button.disabled = !projection.canUndo || inFlight === 'reject' || inFlight === 'accept';
|
|
1002
1001
|
button.textContent = tr('undoRun');
|
|
@@ -1034,12 +1033,12 @@
|
|
|
1034
1033
|
}
|
|
1035
1034
|
|
|
1036
1035
|
const status = run.trackedChangeStatus || '';
|
|
1036
|
+
const inFlight = trackedChangeInFlight.get(run.id);
|
|
1037
1037
|
const projection = projectRunSettlement(run);
|
|
1038
1038
|
// §7 settlement matrix: needs_review keeps BOTH controls visible AND
|
|
1039
1039
|
// actionable. It remains an internal retryable proof state, while the
|
|
1040
1040
|
// primary button label stays in the same executable state as `pending`.
|
|
1041
1041
|
if (status === 'needs_review') {
|
|
1042
|
-
const inFlight = trackedChangeInFlight.get(run.id);
|
|
1043
1042
|
button.hidden = false;
|
|
1044
1043
|
button.disabled = !projection.canAccept || inFlight === 'accept' || inFlight === 'reject';
|
|
1045
1044
|
if (inFlight === 'accept') {
|
|
@@ -1052,14 +1051,14 @@
|
|
|
1052
1051
|
wireAcceptInlineConfirm(button, run.id);
|
|
1053
1052
|
return;
|
|
1054
1053
|
}
|
|
1055
|
-
if (status === 'accepted') {
|
|
1054
|
+
if (status === 'accepted' && !inFlight) {
|
|
1056
1055
|
button.hidden = false;
|
|
1057
1056
|
button.disabled = true;
|
|
1058
1057
|
button.textContent = tr('runAcceptTrackedDone');
|
|
1059
1058
|
button.title = tr('runAcceptTrackedDoneTitle');
|
|
1060
1059
|
return;
|
|
1061
1060
|
}
|
|
1062
|
-
if (status === 'rejected') {
|
|
1061
|
+
if (status === 'rejected' && !inFlight) {
|
|
1063
1062
|
// The run was rejected — terminal. Accept All stays visible but greyed so
|
|
1064
1063
|
// the card shows both controls disabled, never removed.
|
|
1065
1064
|
button.hidden = false;
|
|
@@ -1068,12 +1067,11 @@
|
|
|
1068
1067
|
button.title = tr('runAcceptTrackedTitle');
|
|
1069
1068
|
return;
|
|
1070
1069
|
}
|
|
1071
|
-
if (status !== 'pending') {
|
|
1070
|
+
if (status !== 'pending' && !inFlight) {
|
|
1072
1071
|
button.hidden = true;
|
|
1073
1072
|
return;
|
|
1074
1073
|
}
|
|
1075
1074
|
|
|
1076
|
-
const inFlight = trackedChangeInFlight.get(run.id);
|
|
1077
1075
|
button.hidden = false;
|
|
1078
1076
|
button.disabled = !projection.canAccept || inFlight === 'accept' || inFlight === 'reject';
|
|
1079
1077
|
if (inFlight === 'accept') {
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
}
|
|
96
96
|
return Object.freeze({
|
|
97
97
|
createController: createController,
|
|
98
|
-
persistPanelState: PanelState?.persistPanelState || null
|
|
98
|
+
persistPanelState: PanelState?.persistPanelState || null,
|
|
99
|
+
applyReviewTransition: PanelState?.applyReviewTransition || null
|
|
99
100
|
});
|
|
100
101
|
});
|
|
@@ -15,6 +15,42 @@
|
|
|
15
15
|
return JSON.parse(JSON.stringify(value));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
async function applyReviewTransition({ getState, runId, transition, persist }) {
|
|
19
|
+
const targets = () => Array.from(new Set([
|
|
20
|
+
...(getState().runs || []),
|
|
21
|
+
...(getState().sessions || []).flatMap(session => session.runs || [])
|
|
22
|
+
])).filter(run => run.id === runId);
|
|
23
|
+
const records = targets();
|
|
24
|
+
const sessions = (getState().sessions || []).filter(session =>
|
|
25
|
+
(session.runs || []).some(run => run.id === runId));
|
|
26
|
+
if (!records.length || !sessions.length) throw reviewPersistenceError();
|
|
27
|
+
const previous = cloneSnapshot(records[0]);
|
|
28
|
+
for (const record of records) Object.assign(record, cloneSnapshot(transition));
|
|
29
|
+
for (const session of sessions) {
|
|
30
|
+
session.updatedAt = new Date(Math.max(Date.now(), (Date.parse(session.updatedAt) || 0) + 1,
|
|
31
|
+
(Date.parse(session.lastActivityAt) || 0) + 1)).toISOString();
|
|
32
|
+
session.lastActivityAt = session.updatedAt;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await persist({ preserveRunActionPayload: true, reviewRunIds: [runId] });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
// The remote action may have succeeded. Keep its recovery data and expose uncertainty.
|
|
38
|
+
for (const record of targets()) {
|
|
39
|
+
Object.assign(record, cloneSnapshot(previous), { trackedChangeStatus: 'needs_review' });
|
|
40
|
+
for (const field of ['settlement', 'settlementFacts']) {
|
|
41
|
+
if (!Object.prototype.hasOwnProperty.call(previous, field)) delete record[field];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function reviewPersistenceError() {
|
|
49
|
+
return Object.assign(new Error('The review outcome was not durably saved. Check Overleaf before retrying.'), {
|
|
50
|
+
code: 'review_state_not_persisted'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
18
54
|
async function persistPanelState(input) {
|
|
19
55
|
var state = cloneSnapshot(input.state || {});
|
|
20
56
|
var compactState = cloneSnapshot(input.compactState || {});
|
|
@@ -35,6 +71,9 @@
|
|
|
35
71
|
code: 'stale_view'
|
|
36
72
|
});
|
|
37
73
|
}
|
|
74
|
+
var reviewRuns = (input.reviewRunIds || []).map(id =>
|
|
75
|
+
(state.sessions || []).flatMap(session => session.runs || []).find(run => run.id === id));
|
|
76
|
+
if (reviewRuns.some(run => !run)) throw reviewPersistenceError();
|
|
38
77
|
var latestPrefs = typeof Migration.loadPrefs === 'function'
|
|
39
78
|
? await Migration.loadPrefs(accountScopeId, projectId)
|
|
40
79
|
: {};
|
|
@@ -94,18 +133,22 @@
|
|
|
94
133
|
.filter(function (item) { return !queueTombstones[item?.id]; })
|
|
95
134
|
}), { preserveRunActionPayload: true });
|
|
96
135
|
});
|
|
97
|
-
await input.SessionPersistence.writeSessions({
|
|
136
|
+
var written = await input.SessionPersistence.writeSessions({
|
|
98
137
|
Migration: Migration,
|
|
99
138
|
StorageDb: StorageDb,
|
|
100
139
|
projectId: projectId,
|
|
101
140
|
accountScopeId: accountScopeId,
|
|
102
141
|
deletedSessionIds: deletedSessionIds,
|
|
103
142
|
sessionRecords: sessionRecords,
|
|
143
|
+
reviewRunIds: input.reviewRunIds,
|
|
104
144
|
queueClaims: persistenceMeta.queueClaims || {},
|
|
105
145
|
queueTombstones: queueTombstones,
|
|
106
146
|
writerId: input.persistenceContext?.writerId || ''
|
|
107
147
|
});
|
|
148
|
+
if (!reviewRuns.every(reviewRun => (written || []).some(session => (session.runs || []).some(run =>
|
|
149
|
+
run.id === reviewRun.id && run.trackedChangeStatus === reviewRun.trackedChangeStatus
|
|
150
|
+
)))) throw reviewPersistenceError();
|
|
108
151
|
}
|
|
109
152
|
|
|
110
|
-
return Object.freeze({ persistPanelState: persistPanelState });
|
|
153
|
+
return Object.freeze({ persistPanelState: persistPanelState, applyReviewTransition: applyReviewTransition });
|
|
111
154
|
});
|
|
@@ -73,7 +73,10 @@
|
|
|
73
73
|
const existingById = new Map(existingSessions
|
|
74
74
|
.filter(record => !deleted.has(record.id))
|
|
75
75
|
.map(record => [record.id, record]));
|
|
76
|
-
const
|
|
76
|
+
const mergedRecords = sessionRecords.map(record => StorageDb.mergeSessionReviewState?.(
|
|
77
|
+
record, existingById.get(record.id), options.reviewRunIds
|
|
78
|
+
) || record);
|
|
79
|
+
const writable = mergedRecords.filter(record => {
|
|
77
80
|
const existing = existingById.get(record.id);
|
|
78
81
|
if (!existing) return true;
|
|
79
82
|
return (Date.parse(record.updatedAt || '') || 0) >= (Date.parse(existing.updatedAt || '') || 0);
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
<div class="codex-custom-instructions-head">
|
|
39
39
|
<button type="button" data-settings-back title="Back" aria-label="Back">‹</button>
|
|
40
40
|
<div>
|
|
41
|
-
<div class="codex-custom-instructions-title" data-i18n="projectSettingsTitle">Project Settings</div>
|
|
42
|
-
<div class="codex-custom-instructions-subtitle" data-i18n="projectSettingsSubtitle">Customize how Codex behaves in this and all projects.</div>
|
|
41
|
+
<div class="codex-custom-instructions-title" data-settings-title data-i18n="projectSettingsTitle">Project Settings</div>
|
|
42
|
+
<div class="codex-custom-instructions-subtitle" data-settings-subtitle data-i18n="projectSettingsSubtitle">Customize how Codex behaves in this and all projects.</div>
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
45
|
<div class="codex-project-settings-status" data-project-settings-status></div>
|
|
@@ -304,6 +304,7 @@
|
|
|
304
304
|
if (!root) {
|
|
305
305
|
return;
|
|
306
306
|
}
|
|
307
|
+
refreshSettingsScope(instance, root);
|
|
307
308
|
const button = getButton(instance);
|
|
308
309
|
if (button) {
|
|
309
310
|
button.dataset.active = 'true';
|
|
@@ -312,6 +313,18 @@
|
|
|
312
313
|
root.querySelector('[data-custom-instructions-input]')?.focus?.();
|
|
313
314
|
}
|
|
314
315
|
|
|
316
|
+
function refreshSettingsScope(instance, root) {
|
|
317
|
+
const accountScope = instance.container?.closest?.('[data-settings-scope]')?.dataset?.settingsScope === 'account';
|
|
318
|
+
const prefix = accountScope ? 'recentProjects_settings' : 'projectSettings';
|
|
319
|
+
for (const [selector, suffix] of [['[data-settings-title]', 'Title'], ['[data-settings-subtitle]', 'Subtitle']]) {
|
|
320
|
+
const label = root.querySelector(selector);
|
|
321
|
+
if (!label) continue;
|
|
322
|
+
const key = prefix + suffix;
|
|
323
|
+
label.setAttribute('data-i18n', key);
|
|
324
|
+
label.textContent = t(instance, key);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
315
328
|
function hide(target) {
|
|
316
329
|
const instance = target?._instance || target;
|
|
317
330
|
const root = getRoot(instance);
|
|
@@ -346,6 +359,7 @@
|
|
|
346
359
|
if (!root) {
|
|
347
360
|
return;
|
|
348
361
|
}
|
|
362
|
+
refreshSettingsScope(instance, root);
|
|
349
363
|
if (Object.prototype.hasOwnProperty.call(state, 'customInstructions')) {
|
|
350
364
|
const input = root.querySelector('[data-custom-instructions-input]');
|
|
351
365
|
if (input) {
|
|
@@ -288,5 +288,48 @@
|
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
|
|
291
|
+
async function confirmReviewWriteback({ params, result, readSnapshot, getProjectId, delay,
|
|
292
|
+
now = Date.now, timeoutMs = 45000 }) {
|
|
293
|
+
if (result?.ok !== true || result.skipped?.length) return result;
|
|
294
|
+
const expected = new Map((params.expectedFiles || [])
|
|
295
|
+
.filter(file => file?.path && typeof file.content === 'string').map(file => [file.path, file.content]));
|
|
296
|
+
for (const entry of result.applied || []) {
|
|
297
|
+
const path = entry.trackedChange?.path || entry.operation?.path;
|
|
298
|
+
if (expected.has(path) && typeof entry.result?.verifiedContent === 'string') {
|
|
299
|
+
expected.set(path, entry.result.verifiedContent);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const deadline = now() + Math.max(0, timeoutMs);
|
|
303
|
+
let attempts = 0;
|
|
304
|
+
if (expected.size && typeof readSnapshot === 'function') {
|
|
305
|
+
do {
|
|
306
|
+
if (!params.runProjectId || getProjectId() !== params.runProjectId) break;
|
|
307
|
+
try {
|
|
308
|
+
const snapshot = await readSnapshot({ force: true, maxAgeMs: 0,
|
|
309
|
+
zipTimeoutMs: Math.max(1, Math.min(8000, deadline - now())) });
|
|
310
|
+
attempts++;
|
|
311
|
+
if (getProjectId() !== params.runProjectId) break;
|
|
312
|
+
if (snapshot?.ok === true && Array.from(expected).every(([path, content]) =>
|
|
313
|
+
snapshot.files?.some(file => file.path === path && file.source === 'overleaf-zip'
|
|
314
|
+
&& file.content === content))) {
|
|
315
|
+
return { ...result, saveVerification: { ok: true, state: 'verified_saved',
|
|
316
|
+
source: 'overleaf-zip', attempts } };
|
|
317
|
+
}
|
|
318
|
+
} catch (_error) { /* A transient download failure grants no save proof. */ }
|
|
319
|
+
if (now() >= deadline) break;
|
|
320
|
+
await delay(Math.min(300, deadline - now()));
|
|
321
|
+
} while (now() < deadline);
|
|
322
|
+
}
|
|
323
|
+
const reason = 'Undo changed the editor, but its saved server content could not be confirmed. Keep this page open and check Overleaf before retrying.';
|
|
324
|
+
return { ...result, ok: false, saveVerification: { ok: false, state: 'unknown_timeout', attempts },
|
|
325
|
+
skipped: [...(result.skipped || []), { trackedChange: { path: expected.keys().next().value || '' },
|
|
326
|
+
result: { ok: false, code: 'undo_not_verified', reason, failure: {
|
|
327
|
+
code: 'undo_not_verified', stage: 'verification', severity: 'needs_review',
|
|
328
|
+
terminalState: 'needs_review', changedDocument: true, retryable: true, userMessage: reason
|
|
329
|
+
} } }] };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const api = { create, confirmReviewWriteback };
|
|
333
|
+
root.CodexOverleafSaveState = api;
|
|
334
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
292
335
|
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -1630,7 +1630,10 @@
|
|
|
1630
1630
|
}
|
|
1631
1631
|
return {
|
|
1632
1632
|
acceptTrackedChanges,
|
|
1633
|
-
rejectTrackedChanges
|
|
1633
|
+
rejectTrackedChanges: async params => {
|
|
1634
|
+
const result = await rejectTrackedChanges(params);
|
|
1635
|
+
return deps.confirmReviewWriteback ? deps.confirmReviewWriteback(params, result) : result;
|
|
1636
|
+
},
|
|
1634
1637
|
collectTrackedChangeRefsForPaths,
|
|
1635
1638
|
prepareTrackedChangeCapture, getTrackedChangeCaptureStatus,
|
|
1636
1639
|
captureTrackedWrite,
|
|
@@ -1052,7 +1052,7 @@
|
|
|
1052
1052
|
return labelPath;
|
|
1053
1053
|
}
|
|
1054
1054
|
|
|
1055
|
-
const textPath = normalizeProjectPathCandidate(node
|
|
1055
|
+
const textPath = normalizeProjectPathCandidate(readFileTreeNodeText(node));
|
|
1056
1056
|
if (!textPath) {
|
|
1057
1057
|
const docIdPath = resolveProjectPathFromDomDocId(node);
|
|
1058
1058
|
const domTreePath = resolveProjectPathFromDomTree(node);
|
|
@@ -1181,7 +1181,7 @@
|
|
|
1181
1181
|
node.getAttribute?.('data-name'),
|
|
1182
1182
|
node.getAttribute?.('aria-label'),
|
|
1183
1183
|
node.getAttribute?.('title'),
|
|
1184
|
-
node
|
|
1184
|
+
readFileTreeNodeText(node)
|
|
1185
1185
|
];
|
|
1186
1186
|
for (const value of candidates) {
|
|
1187
1187
|
const path = normalizeProjectPathCandidate(value);
|
|
@@ -1244,21 +1244,12 @@
|
|
|
1244
1244
|
}
|
|
1245
1245
|
|
|
1246
1246
|
function normalizeProjectPathCandidate(value) {
|
|
1247
|
-
const text =
|
|
1247
|
+
const text = String(value || '').trim();
|
|
1248
1248
|
if (!text || hasMultipleTextPathExtensions(text)) {
|
|
1249
1249
|
return '';
|
|
1250
1250
|
}
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
text
|
|
1254
|
-
];
|
|
1255
|
-
for (const candidate of candidates) {
|
|
1256
|
-
const normalized = normalizeSafeProjectPath(candidate);
|
|
1257
|
-
if (normalized && window.CodexOverleafProjectFiles.isTextProjectPath(normalized)) {
|
|
1258
|
-
return normalized;
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
return '';
|
|
1251
|
+
const normalized = normalizeSafeProjectPath(text);
|
|
1252
|
+
return normalized && window.CodexOverleafProjectFiles.isTextProjectPath(normalized) ? normalized : '';
|
|
1262
1253
|
}
|
|
1263
1254
|
|
|
1264
1255
|
function normalizeDomText(value) {
|
|
@@ -1275,19 +1266,11 @@
|
|
|
1275
1266
|
return false;
|
|
1276
1267
|
}
|
|
1277
1268
|
|
|
1278
|
-
function
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
.replace(/(?:more_vert\s*Menu|more_vert|Menu)$/i, '')
|
|
1284
|
-
.trim();
|
|
1285
|
-
if (next === text) {
|
|
1286
|
-
break;
|
|
1287
|
-
}
|
|
1288
|
-
text = next;
|
|
1289
|
-
}
|
|
1290
|
-
return text;
|
|
1269
|
+
function readFileTreeNodeText(node) {
|
|
1270
|
+
// Icon ligatures can be valid filename prefixes; use the actual label.
|
|
1271
|
+
const names = Array.from(node.querySelectorAll?.('.item-name') || []);
|
|
1272
|
+
if (names.length > 1) return '';
|
|
1273
|
+
return names.length === 1 ? names[0].textContent || '' : node.textContent || '';
|
|
1291
1274
|
}
|
|
1292
1275
|
|
|
1293
1276
|
function inferPathFromTreeAncestors(node, fileName) {
|
|
@@ -1890,7 +1890,7 @@
|
|
|
1890
1890
|
captureTrackedWrite, reconcileTrackedChangeCapture,
|
|
1891
1891
|
waitForTrackedChangeDiff
|
|
1892
1892
|
} = trackedChangesLifecycleModule.create({
|
|
1893
|
-
window,
|
|
1893
|
+
window, confirmReviewWriteback: deps.confirmReviewWriteback,
|
|
1894
1894
|
checkWritebackRunProjectId,
|
|
1895
1895
|
buildNoTraceUndoBlockedResult,
|
|
1896
1896
|
buildReviewingRequiredBlockedResult,
|
|
@@ -93,6 +93,9 @@
|
|
|
93
93
|
window
|
|
94
94
|
});
|
|
95
95
|
writebackRouter = requirePageModule('CodexOverleafWritebackRouter').create({
|
|
96
|
+
confirmReviewWriteback: (params, result) => requirePageModule('CodexOverleafSaveState').confirmReviewWriteback({
|
|
97
|
+
params, result, getProjectId, delay, readSnapshot: options => snapshotRouter.fetchProjectZipSnapshot(options)
|
|
98
|
+
}),
|
|
96
99
|
createTextFile: textFileCreator.createFile,
|
|
97
100
|
deleteTextFile: textFileCreator.deleteFile,
|
|
98
101
|
activeEditorIdentityChanged,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const MIN_NATIVE_VERSION = '1.0.0';
|
|
13
13
|
const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
|
|
14
14
|
const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
|
|
15
|
-
const BUILD_TARGET_VERSION = '2.4.
|
|
15
|
+
const BUILD_TARGET_VERSION = '2.4.1';
|
|
16
16
|
const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
|
|
17
17
|
const REQUIRED_CAPABILITIES = Object.freeze([
|
|
18
18
|
'bridgePing',
|
|
@@ -508,14 +508,39 @@
|
|
|
508
508
|
failureReason_editor_project_id_unavailable_next: 'Refresh the Overleaf tab and retry; if it persists, reload the extension.',
|
|
509
509
|
// Welcome-panel + write-guard: Recent-projects
|
|
510
510
|
// variant copy. Spec §5.3–§5.5, §5.8, §5.10 are the source of truth.
|
|
511
|
-
recentProjects_welcome: '
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
511
|
+
recentProjects_welcome: 'Continue working',
|
|
512
|
+
recentProjects_focusEyebrow: 'Last conversation',
|
|
513
|
+
recentProjects_historyHeading: 'Other recent conversations',
|
|
514
|
+
recentProjects_historyHint: 'Select to continue',
|
|
515
|
+
recentProjects_projectConversations: "View this project's conversations",
|
|
516
|
+
recentProjects_allProjects: 'All projects',
|
|
517
|
+
recentProjects_showAllConversations: 'View all {count} conversations',
|
|
518
|
+
recentProjects_unstarted: 'Not started \u00b7 {count}',
|
|
519
|
+
recentProjects_unstartedHint: 'Empty conversations are kept here, without deleting their records.',
|
|
520
|
+
recentProjects_noStarted: 'No conversation has been started yet. Empty conversations are available below.',
|
|
521
|
+
recentProjects_savedDraft: 'Saved draft, ready to continue.',
|
|
522
|
+
recentProjects_attention: 'Needs attention',
|
|
523
|
+
recentProjects_unavailableRecords: 'Records with unavailable project links',
|
|
524
|
+
recentProjects_loadingHistory: 'Loading conversations...',
|
|
525
|
+
recentProjects_loadFailed: 'Conversation history could not be read. Please retry.',
|
|
526
|
+
recentProjects_continueHint: 'Open this conversation without automatically starting a task.',
|
|
527
|
+
recentProjects_more: 'More options',
|
|
528
|
+
recentProjects_continue: 'Continue',
|
|
529
|
+
recentProjects_openProject: 'Open project',
|
|
530
|
+
recentProjects_loadingConversation: 'Loading conversation...',
|
|
531
|
+
recentProjects_noConversation: 'No saved conversation',
|
|
532
|
+
recentProjects_noRuns: 'No runs yet',
|
|
533
|
+
recentProjects_previewUnavailable: 'Conversation unavailable',
|
|
534
|
+
recentProjects_previewUnavailableHint: 'Saved conversation could not be loaded.',
|
|
535
|
+
recentProjects_conversationGone: 'This conversation is no longer available. Choose another conversation from the refreshed list.',
|
|
536
|
+
recentProjects_continueFailed: 'Could not open the conversation. Please try again.',
|
|
537
|
+
recentProjects_welcome_subtitle: 'Conversations saved in this browser',
|
|
538
|
+
recentProjects_empty: 'No conversations saved in this browser yet. Open an Overleaf project and start a Codex conversation.',
|
|
539
|
+
recentProjects_sessions_empty: 'No saved conversations in this project',
|
|
540
|
+
recentProjects_sessions_toggle: "Show or manage this project's conversations",
|
|
541
|
+
recentProjects_sessions_action: 'All conversations',
|
|
542
|
+
recentProjects_sessions_action_open: 'Hide conversations',
|
|
543
|
+
recentProjects_clearProject: 'Delete this project\'s conversations',
|
|
519
544
|
recentProjects_clearProject_title: 'Delete all Codex sessions for {project}?',
|
|
520
545
|
recentProjects_clearProject_message: 'This permanently deletes all {count} plugin sessions, run history, and matching plugin-local Codex thread history for {project}. It will not modify Overleaf files or global Codex history.',
|
|
521
546
|
recentProjects_clearProject_confirm: 'Delete all',
|
|
@@ -531,15 +556,17 @@
|
|
|
531
556
|
recentProjects_degraded: 'Sign in to Overleaf to see your recent Codex projects.',
|
|
532
557
|
recentProjects_row_projectLinkUnavailable: 'Project link unavailable.',
|
|
533
558
|
recentProjects_settings_entry: 'Settings & Diagnostics',
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
559
|
+
recentProjects_settingsTitle: 'General Settings',
|
|
560
|
+
recentProjects_settingsSubtitle: 'Manage Codex preferences and local history across all projects.',
|
|
561
|
+
recentProjects_badge_pending: 'Last run pending',
|
|
562
|
+
recentProjects_badge_accepted: 'Last changes accepted',
|
|
563
|
+
recentProjects_badge_rejected: 'Last changes undone',
|
|
564
|
+
recentProjects_badge_needs_review: 'Last run needs review',
|
|
565
|
+
recentProjects_badge_running: 'Last recorded as running',
|
|
566
|
+
recentProjects_badge_completed: 'Last run completed',
|
|
567
|
+
recentProjects_badge_failed: 'Last run failed',
|
|
568
|
+
recentProjects_badge_interrupted: 'Last run may have been interrupted',
|
|
569
|
+
recentProjects_badge_stale: 'Last run status unconfirmed',
|
|
543
570
|
recentProjects_unnamed: 'Project \u00b7 {prefix}',
|
|
544
571
|
recentProjects_zombieRunningNote: 'Marked as running, but it may be a leftover from an interrupted run.',
|
|
545
572
|
recentProjects_zombieDeleteConfirm: 'This session is marked as running. If no other tab is actually running it, you can delete it anyway.',
|
|
@@ -547,9 +574,9 @@
|
|
|
547
574
|
recentProjects_degraded_hint: 'Could not identify the Overleaf account on this page, so the project list is hidden to avoid mixing accounts. This usually resolves on a reload.',
|
|
548
575
|
recentProjects_sessionsCount: '{count} session(s)',
|
|
549
576
|
recentProjects_welcome_stats: '{projects} projects \u00b7 {sessions} Codex sessions',
|
|
550
|
-
recentProjects_badge_background_completed: 'completed in background',
|
|
551
|
-
recentProjects_badge_needs_review_after_navigation: '
|
|
552
|
-
recentProjects_badge_abandoned_after_navigation: '
|
|
577
|
+
recentProjects_badge_background_completed: 'Last run completed in background',
|
|
578
|
+
recentProjects_badge_needs_review_after_navigation: 'Last run needs review',
|
|
579
|
+
recentProjects_badge_abandoned_after_navigation: 'Last run stopped after navigation'
|
|
553
580
|
},
|
|
554
581
|
zh: {
|
|
555
582
|
resizePanel: '拖动调整 Codex 面板宽度,双击恢复默认宽度',
|
|
@@ -1047,14 +1074,39 @@
|
|
|
1047
1074
|
failureReason_editor_project_id_unavailable_next: '请刷新 Overleaf 页面后重试;如果仍然失败,请重新加载扩展。',
|
|
1048
1075
|
// Welcome-panel + write-guard: Recent-projects
|
|
1049
1076
|
// variant copy. 见 spec §5.3–§5.5、§5.8、§5.10。
|
|
1050
|
-
recentProjects_welcome: '
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1077
|
+
recentProjects_welcome: '继续工作',
|
|
1078
|
+
recentProjects_focusEyebrow: '上次对话',
|
|
1079
|
+
recentProjects_historyHeading: '其他最近对话',
|
|
1080
|
+
recentProjects_historyHint: '点击条目继续',
|
|
1081
|
+
recentProjects_projectConversations: '查看该项目的全部对话',
|
|
1082
|
+
recentProjects_allProjects: '所有项目',
|
|
1083
|
+
recentProjects_showAllConversations: '查看全部 {count} 段对话',
|
|
1084
|
+
recentProjects_unstarted: '未开始 · {count}',
|
|
1085
|
+
recentProjects_unstartedHint: '空白对话收纳在这里,记录不会被删除。',
|
|
1086
|
+
recentProjects_noStarted: '尚无已开始的对话,空白对话可在下方展开。',
|
|
1087
|
+
recentProjects_savedDraft: '草稿已保存,可继续补充。',
|
|
1088
|
+
recentProjects_attention: '需要查看',
|
|
1089
|
+
recentProjects_unavailableRecords: '项目链接不可用的记录',
|
|
1090
|
+
recentProjects_loadingHistory: '正在加载对话…',
|
|
1091
|
+
recentProjects_loadFailed: '暂时无法读取对话历史,请重试。',
|
|
1092
|
+
recentProjects_continueHint: '仅打开这段对话,不会自动启动任务。',
|
|
1093
|
+
recentProjects_more: '更多选项',
|
|
1094
|
+
recentProjects_continue: '继续',
|
|
1095
|
+
recentProjects_openProject: '打开项目',
|
|
1096
|
+
recentProjects_loadingConversation: '正在加载对话…',
|
|
1097
|
+
recentProjects_noConversation: '暂无保存的对话',
|
|
1098
|
+
recentProjects_noRuns: '尚未运行任务',
|
|
1099
|
+
recentProjects_previewUnavailable: '暂时无法读取对话',
|
|
1100
|
+
recentProjects_previewUnavailableHint: '已保存的对话暂时无法读取。',
|
|
1101
|
+
recentProjects_conversationGone: '该对话已不可用,请从更新后的列表中选择其他对话。',
|
|
1102
|
+
recentProjects_continueFailed: '暂时无法打开对话,请重试。',
|
|
1103
|
+
recentProjects_welcome_subtitle: '当前浏览器保存的对话',
|
|
1104
|
+
recentProjects_empty: '当前浏览器还没有保存的对话。打开一个 Overleaf 项目,与 Codex 开始对话即可。',
|
|
1105
|
+
recentProjects_sessions_empty: '该项目还没有保存的对话',
|
|
1106
|
+
recentProjects_sessions_toggle: '查看或管理该项目的对话',
|
|
1107
|
+
recentProjects_sessions_action: '全部对话',
|
|
1108
|
+
recentProjects_sessions_action_open: '收起对话',
|
|
1109
|
+
recentProjects_clearProject: '删除该项目的全部对话',
|
|
1058
1110
|
recentProjects_clearProject_title: '删除 {project} 的全部 Codex 会话?',
|
|
1059
1111
|
recentProjects_clearProject_message: '这会永久删除 {project} 的全部 {count} 个插件会话、运行历史及对应的插件本地 Codex 线程历史。不会修改 Overleaf 文件或全局 Codex 历史。',
|
|
1060
1112
|
recentProjects_clearProject_confirm: '全部删除',
|
|
@@ -1070,15 +1122,17 @@
|
|
|
1070
1122
|
recentProjects_degraded: '请登录 Overleaf 后查看你最近的 Codex 项目。',
|
|
1071
1123
|
recentProjects_row_projectLinkUnavailable: '项目链接不可用。',
|
|
1072
1124
|
recentProjects_settings_entry: '设置与诊断',
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1125
|
+
recentProjects_settingsTitle: '通用设置',
|
|
1126
|
+
recentProjects_settingsSubtitle: '管理适用于所有项目的 Codex 偏好设置与本地历史记录。',
|
|
1127
|
+
recentProjects_badge_pending: '上次运行待处理',
|
|
1128
|
+
recentProjects_badge_accepted: '上次修改已接受',
|
|
1129
|
+
recentProjects_badge_rejected: '上次修改已撤回',
|
|
1130
|
+
recentProjects_badge_needs_review: '上次运行需要检查',
|
|
1131
|
+
recentProjects_badge_running: '上次记录为运行中',
|
|
1132
|
+
recentProjects_badge_completed: '上次运行已完成',
|
|
1133
|
+
recentProjects_badge_failed: '上次运行失败',
|
|
1134
|
+
recentProjects_badge_interrupted: '上次运行可能已中断',
|
|
1135
|
+
recentProjects_badge_stale: '上次运行状态未确认',
|
|
1082
1136
|
recentProjects_unnamed: '项目 · {prefix}',
|
|
1083
1137
|
recentProjects_zombieRunningNote: '标记为运行中,但可能是被中断后的残留。',
|
|
1084
1138
|
recentProjects_zombieDeleteConfirm: '该会话标记为运行中。如果确认没有其他标签页在运行它,可以直接删除。',
|
|
@@ -1086,9 +1140,9 @@
|
|
|
1086
1140
|
recentProjects_degraded_hint: '无法识别本页的 Overleaf 账号,为避免账号间串数据已隐藏项目列表。通常刷新即可恢复。',
|
|
1087
1141
|
recentProjects_sessionsCount: '{count} 个会话',
|
|
1088
1142
|
recentProjects_welcome_stats: '{projects} 个项目 · {sessions} 个 Codex 会话',
|
|
1089
|
-
recentProjects_badge_background_completed: '
|
|
1090
|
-
recentProjects_badge_needs_review_after_navigation: '
|
|
1091
|
-
recentProjects_badge_abandoned_after_navigation: '
|
|
1143
|
+
recentProjects_badge_background_completed: '上次运行已在后台完成',
|
|
1144
|
+
recentProjects_badge_needs_review_after_navigation: '上次运行需要检查',
|
|
1145
|
+
recentProjects_badge_abandoned_after_navigation: '上次运行已在离开页面后停止'
|
|
1092
1146
|
}
|
|
1093
1147
|
};
|
|
1094
1148
|
|
|
@@ -1343,6 +1343,7 @@
|
|
|
1343
1343
|
clearStore: clearStore,
|
|
1344
1344
|
clearAllStores: clearAllStores,
|
|
1345
1345
|
buildSessionRecord: buildSessionRecord,
|
|
1346
|
+
mergeSessionReviewState: StorageRunActions.mergeSessionReviewState,
|
|
1346
1347
|
buildTurnRecord: buildTurnRecord,
|
|
1347
1348
|
buildEventRecord: buildEventRecord,
|
|
1348
1349
|
buildArtifactRecord: buildArtifactRecord,
|
|
@@ -159,6 +159,49 @@
|
|
|
159
159
|
return out;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Review terminal states are monotonic even when a later session snapshot is stale.
|
|
163
|
+
function mergeSessionReviewState(incoming, existing, reviewRunIds) {
|
|
164
|
+
if (!existing || incoming.id !== existing.id || incoming.projectId !== existing.projectId
|
|
165
|
+
|| !incoming.accountScopeId || incoming.accountScopeId !== existing.accountScopeId) return incoming;
|
|
166
|
+
var terminal = run => ['accepted', 'rejected'].includes(run?.trackedChangeStatus);
|
|
167
|
+
var incomingRuns = new Map((incoming.runs || []).map(run => [run.id, run]));
|
|
168
|
+
var existingRuns = new Map((existing.runs || []).map(run => [run.id, run]));
|
|
169
|
+
var requestedIds = new Set(reviewRunIds || []);
|
|
170
|
+
var explicitReview = Array.from(requestedIds).some(id =>
|
|
171
|
+
['accepted', 'rejected', 'needs_review'].includes(incomingRuns.get(id)?.trackedChangeStatus));
|
|
172
|
+
var newer = (Date.parse(incoming.updatedAt) || 0) >= (Date.parse(existing.updatedAt) || 0);
|
|
173
|
+
if (!newer && !explicitReview) return incoming;
|
|
174
|
+
var base = newer ? incoming : existing;
|
|
175
|
+
var fields = ['trackedChangeStatus', 'undoStatus', 'undoTrackedChanges', 'undoExpectedFiles',
|
|
176
|
+
'undoOperations', 'undoBaseFiles', 'appliedOperations', 'trackedChangeCaptures', 'settlement', 'settlementFacts'];
|
|
177
|
+
var changed = false;
|
|
178
|
+
var runs = (base.runs || []).map(function (run) {
|
|
179
|
+
var previous = existingRuns.get(run.id), candidate = incomingRuns.get(run.id);
|
|
180
|
+
// Never revive a removed run or change the first durably recorded review decision.
|
|
181
|
+
var source = terminal(previous) ? previous
|
|
182
|
+
: explicitReview && requestedIds.has(run.id) ? candidate : null;
|
|
183
|
+
if (!source || source === run) return run;
|
|
184
|
+
if (source.runProjectId && source.runProjectId !== incoming.projectId) return run;
|
|
185
|
+
var merged = { ...run };
|
|
186
|
+
for (var field of fields) {
|
|
187
|
+
if (Object.prototype.hasOwnProperty.call(source, field)) {
|
|
188
|
+
merged[field] = JSON.parse(JSON.stringify(source[field]));
|
|
189
|
+
} else {
|
|
190
|
+
delete merged[field];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
changed = true;
|
|
194
|
+
return merged;
|
|
195
|
+
});
|
|
196
|
+
if (!changed) return base;
|
|
197
|
+
var merged = { ...base, runs };
|
|
198
|
+
if (explicitReview && !newer) {
|
|
199
|
+
merged.updatedAt = new Date(Math.max(Date.now(), Date.parse(existing.updatedAt) || 0,
|
|
200
|
+
Date.parse(incoming.updatedAt) || 0) + 1).toISOString();
|
|
201
|
+
}
|
|
202
|
+
return merged;
|
|
203
|
+
}
|
|
204
|
+
|
|
162
205
|
function hashString(value) {
|
|
163
206
|
var hash = 2166136261;
|
|
164
207
|
var text = String(value || '');
|
|
@@ -171,6 +214,7 @@
|
|
|
171
214
|
|
|
172
215
|
return {
|
|
173
216
|
compactStructuredEventValue: compactStructuredEventValue,
|
|
217
|
+
mergeSessionReviewState: mergeSessionReviewState,
|
|
174
218
|
compactRunsForStorage: compactRunsForStorage,
|
|
175
219
|
compactRunActionPayload: compactRunActionPayload,
|
|
176
220
|
compactProviderSnapshot: compactProviderSnapshot,
|