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
|
@@ -15,7 +15,50 @@
|
|
|
15
15
|
return JSON.parse(JSON.stringify(value));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Review owns recovery progress and its settlement decision. Log events,
|
|
19
|
+
// compilation/mirror evidence, and later draft/display data have other writers.
|
|
20
|
+
const REVIEW_FIELDS = ['appliedOperations', 'undoOperations', 'undoBaseFiles', 'undoTrackedChanges',
|
|
21
|
+
'undoExpectedFiles', 'undoStatus', 'trackedChangeStatus', 'trackedChangeCaptures', 'interruptedDraft',
|
|
22
|
+
'retryWriteback', 'saveCheck'];
|
|
23
|
+
const REVIEW_FACT_FIELDS = ['documentEffect', 'fileSettlements', 'failures', 'recoveryAvailability', 'recoveryDisposition'];
|
|
24
|
+
const AUXILIARY_EVIDENCE = ['mirrored', 'compiled'];
|
|
25
|
+
const REVIEW_SOURCE_FIELDS = ['id', 'runProjectId', 'runAccountScopeId', 'startedAt', 'nativeEventSeq',
|
|
26
|
+
'codexThreadId', 'codexTurnId', 'executionSnapshot', 'forkSnapshot'];
|
|
27
|
+
const factsOf = record => record?.settlement || record?.settlementFacts || {};
|
|
28
|
+
function reviewSnapshot(record, SessionState) {
|
|
29
|
+
const normalized = SessionState?.normalizeRuns?.([record])?.[0] || record;
|
|
30
|
+
const facts = factsOf(normalized);
|
|
31
|
+
return JSON.stringify([
|
|
32
|
+
[...REVIEW_SOURCE_FIELDS, ...REVIEW_FIELDS].map(key => normalized[key]),
|
|
33
|
+
REVIEW_FACT_FIELDS.map(key => facts[key]),
|
|
34
|
+
Object.entries(facts.evidence || {}).filter(([key]) => !AUXILIARY_EVIDENCE.includes(key)).sort(([a], [b]) => a.localeCompare(b))
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
function restoreReviewFields(record, previous, staged, legacyUndo) {
|
|
38
|
+
for (const key of REVIEW_FIELDS) {
|
|
39
|
+
if (Object.hasOwn(previous, key)) record[key] = cloneSnapshot(previous[key]);
|
|
40
|
+
else delete record[key];
|
|
41
|
+
}
|
|
42
|
+
Object.assign(record, legacyUndo ? { undoStatus: 'partial' } : { trackedChangeStatus: 'needs_review' });
|
|
43
|
+
const currentFacts = factsOf(record), stagedFacts = factsOf(staged);
|
|
44
|
+
const restored = cloneSnapshot(factsOf(previous));
|
|
45
|
+
const preserveLateFields = (target, current, initial, excluded) => {
|
|
46
|
+
for (const key of new Set([...Object.keys(current), ...Object.keys(initial)])) {
|
|
47
|
+
if (excluded.includes(key) || JSON.stringify(current[key]) === JSON.stringify(initial[key])) continue;
|
|
48
|
+
if (Object.hasOwn(current, key)) target[key] = cloneSnapshot(current[key]); else delete target[key];
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
preserveLateFields(restored, currentFacts, stagedFacts, [...REVIEW_FACT_FIELDS, 'evidence']);
|
|
52
|
+
const evidence = { ...(restored.evidence || {}) };
|
|
53
|
+
const currentEvidence = currentFacts.evidence || {}, stagedEvidence = stagedFacts.evidence || {};
|
|
54
|
+
preserveLateFields(evidence, currentEvidence, stagedEvidence,
|
|
55
|
+
[...Object.keys(currentEvidence), ...Object.keys(stagedEvidence)].filter(key => !AUXILIARY_EVIDENCE.includes(key)));
|
|
56
|
+
if (Object.keys(evidence).length) restored.evidence = evidence; else delete restored.evidence;
|
|
57
|
+
delete record.settlementFacts;
|
|
58
|
+
if (Object.keys(restored).length) record.settlement = restored; else delete record.settlement;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function applyReviewTransition({ getState, runId, transition, persist, legacyUndo = false, assertCurrent, SessionState }) {
|
|
19
62
|
const targets = () => Array.from(new Set([
|
|
20
63
|
...(getState().runs || []),
|
|
21
64
|
...(getState().sessions || []).flatMap(session => session.runs || [])
|
|
@@ -33,22 +76,31 @@
|
|
|
33
76
|
(Date.parse(session.lastActivityAt) || 0) + 1)).toISOString();
|
|
34
77
|
session.lastActivityAt = session.updatedAt;
|
|
35
78
|
}
|
|
36
|
-
const
|
|
79
|
+
const snapshot = record => reviewSnapshot(record, SessionState);
|
|
80
|
+
const stagedRecords = records.map(cloneSnapshot), staged = records.map(snapshot);
|
|
81
|
+
const sameRecord = (original, record) => original === record
|
|
82
|
+
|| (assertCurrent && SessionState?.sameRunIdentity?.(original, record));
|
|
83
|
+
const stillStaged = record => records.some((original, index) =>
|
|
84
|
+
sameRecord(original, record) && snapshot(record) === staged[index]);
|
|
85
|
+
const sameView = () => getState().activeSessionId === capturedSessionId
|
|
86
|
+
&& (getState() === capturedState || (assertCurrent && SessionState?.sameRunIdentity));
|
|
37
87
|
try {
|
|
38
88
|
await persist({ preserveRunActionPayload: true, reviewRunIds: [runId], requireDurable: true });
|
|
39
89
|
assertCurrent?.();
|
|
40
|
-
|
|
41
|
-
|
|
90
|
+
const current = targets();
|
|
91
|
+
if (!sameView() || !current.length || current.some(record => !stillStaged(record))
|
|
92
|
+
|| records.some(original => !current.some(record => sameRecord(original, record))))
|
|
93
|
+
throw reviewPersistenceError();
|
|
42
94
|
} catch (error) {
|
|
43
95
|
try { assertCurrent?.(); } catch (_stale) { throw error; }
|
|
96
|
+
// The envelope still failed closed, but an exact run-scoped readback
|
|
97
|
+
// already acknowledges this outcome. Keep the error/notice, not a false retry.
|
|
98
|
+
if (error.code === 'shared_session_conflict' && error.durableReviewRunIds?.includes(runId)) throw error;
|
|
44
99
|
// The remote action may have succeeded. Keep its recovery data and expose uncertainty.
|
|
45
|
-
for (const record of
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
for (const field of ['settlement', 'settlementFacts']) {
|
|
50
|
-
if (!Object.prototype.hasOwnProperty.call(previous, field)) delete record[field];
|
|
51
|
-
}
|
|
100
|
+
for (const record of targets()) {
|
|
101
|
+
if (!sameView() || !stillStaged(record)) continue;
|
|
102
|
+
const index = records.findIndex((original, index) => sameRecord(original, record) && snapshot(record) === staged[index]);
|
|
103
|
+
restoreReviewFields(record, previous, stagedRecords[index], legacyUndo);
|
|
52
104
|
}
|
|
53
105
|
throw error;
|
|
54
106
|
}
|
|
@@ -82,6 +134,19 @@
|
|
|
82
134
|
}
|
|
83
135
|
}
|
|
84
136
|
|
|
137
|
+
function projectPreferenceValues(input, prefs, projectId) {
|
|
138
|
+
return {
|
|
139
|
+
experimentalOtByProject: input.normalizeExperimentalOtByProject(prefs.experimentalOtByProject)[projectId] === true,
|
|
140
|
+
reviewingAvailabilityByProject: prefs.reviewingAvailabilityByProject?.[projectId],
|
|
141
|
+
governanceRulesByProject: input.normalizeGovernanceRulesByProject({
|
|
142
|
+
[projectId]: prefs.governanceRulesByProject?.[projectId] || {}
|
|
143
|
+
})[projectId],
|
|
144
|
+
customInstructionsByProject: input.normalizeCustomInstructionsByProject({
|
|
145
|
+
[projectId]: prefs.customInstructionsByProject?.[projectId] || ''
|
|
146
|
+
})[projectId]
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
85
150
|
async function persistPanelState(input) {
|
|
86
151
|
var state = cloneSnapshot(input.state || {});
|
|
87
152
|
var compactState = cloneSnapshot(input.compactState || {});
|
|
@@ -111,6 +176,11 @@
|
|
|
111
176
|
var reviewRuns = (input.reviewRunIds || []).map(id =>
|
|
112
177
|
(state.sessions || []).flatMap(session => session.runs || []).find(run => run.id === id));
|
|
113
178
|
if (reviewRuns.some(run => !run)) throw reviewPersistenceError();
|
|
179
|
+
var preferenceBaseline = input.projectPreferenceBaseline
|
|
180
|
+
|| Migration.getProjectPreferenceBaseline?.(accountScopeId, projectId);
|
|
181
|
+
var preferenceWrite = input.projectPreferenceWrite || Migration.captureProjectPreferenceWrite?.({
|
|
182
|
+
...input, state: state, baseline: preferenceBaseline, accountScopeId: accountScopeId, projectId: projectId
|
|
183
|
+
});
|
|
114
184
|
var latestPrefs = typeof Migration.loadPrefs === 'function'
|
|
115
185
|
? await Migration.loadPrefs(accountScopeId, projectId)
|
|
116
186
|
: {};
|
|
@@ -124,30 +194,23 @@
|
|
|
124
194
|
projectId,
|
|
125
195
|
compactState.activeSessionId || state.activeSessionId || ''
|
|
126
196
|
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
var
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
if (customProjectId) {
|
|
145
|
-
prefs.customInstructionsByProject[customProjectId] =
|
|
146
|
-
Object.prototype.hasOwnProperty.call(currentCustom, customProjectId)
|
|
147
|
-
? currentCustom[customProjectId]
|
|
148
|
-
: '';
|
|
197
|
+
var currentPreferences = projectPreferenceValues(input, state, projectId);
|
|
198
|
+
if (preferenceWrite) {
|
|
199
|
+
Object.assign(prefs, Migration.projectPreferencePatch(preferenceWrite, accountScopeId, projectId));
|
|
200
|
+
} else {
|
|
201
|
+
var baselinePreferences = projectPreferenceValues(input, preferenceBaseline || latestPrefs || {}, projectId);
|
|
202
|
+
for (var name of Object.keys(currentPreferences)) {
|
|
203
|
+
prefs[name] = JSON.stringify(currentPreferences[name]) === JSON.stringify(baselinePreferences[name])
|
|
204
|
+
? {} : { [projectId]: currentPreferences[name] };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
var savedPrefs = await Migration.savePrefs(prefs, accountScopeId, projectId);
|
|
208
|
+
if (savedPrefs) {
|
|
209
|
+
var fields = preferenceWrite ? Migration.completeProjectPreferenceWrite(preferenceWrite, savedPrefs)
|
|
210
|
+
: Object.keys(currentPreferences);
|
|
211
|
+
var applied = input.applyProjectPreferences?.(savedPrefs, state, fields);
|
|
212
|
+
if (!preferenceWrite && applied !== false) Migration.adoptProjectPreferenceBaseline?.(savedPrefs, accountScopeId, projectId);
|
|
149
213
|
}
|
|
150
|
-
await Migration.savePrefs(prefs, accountScopeId, projectId);
|
|
151
214
|
var persistenceMeta = input.persistenceContext?.nextMeta || {};
|
|
152
215
|
var queueTombstones = persistenceMeta.queueTombstones || {};
|
|
153
216
|
var sessionTombstones = persistenceMeta.sessionTombstones || {};
|
|
@@ -171,18 +234,33 @@
|
|
|
171
234
|
}), { preserveRunActionPayload: true });
|
|
172
235
|
});
|
|
173
236
|
sessionRecords = StorageDb.compactSessionBatchActions?.(sessionRecords, { activeSessionId: state.activeSessionId }) || sessionRecords;
|
|
174
|
-
var written
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
237
|
+
var written;
|
|
238
|
+
try {
|
|
239
|
+
written = await input.SessionPersistence.writeSessions({
|
|
240
|
+
Migration: Migration,
|
|
241
|
+
StorageDb: StorageDb,
|
|
242
|
+
projectId: projectId,
|
|
243
|
+
accountScopeId: accountScopeId,
|
|
244
|
+
deletedSessionIds: deletedSessionIds,
|
|
245
|
+
sessionRecords: sessionRecords,
|
|
246
|
+
reviewRunIds: input.reviewRunIds,
|
|
247
|
+
queueClaims: persistenceMeta.queueClaims || {},
|
|
248
|
+
queueTombstones: queueTombstones,
|
|
249
|
+
writerId: input.persistenceContext?.writerId || ''
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error.code === 'shared_session_conflict' && error.reviewReceipts && StorageDb.reviewStateSnapshot) {
|
|
253
|
+
error.durableReviewRunIds = (input.reviewRunIds || []).filter(id => sessionRecords.some(expected => {
|
|
254
|
+
const proposed = expected.runs?.find(run => run.id === id);
|
|
255
|
+
const saved = error.reviewReceipts.find(record => record.id === expected.id
|
|
256
|
+
&& record.accountScopeId === expected.accountScopeId && record.projectId === expected.projectId)
|
|
257
|
+
?.runs?.find(run => run.id === id);
|
|
258
|
+
return proposed && saved && StorageDb.reviewStateSnapshot(proposed) === StorageDb.reviewStateSnapshot(saved);
|
|
259
|
+
}));
|
|
260
|
+
delete error.reviewReceipts;
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
186
264
|
if (!reviewRuns.every(reviewRun => (written || []).some(session => (session.runs || []).some(run =>
|
|
187
265
|
run.id === reviewRun.id
|
|
188
266
|
&& (!reviewRun.trackedChangeStatus && ['applied', 'partial'].includes(reviewRun.undoStatus)
|
|
@@ -133,6 +133,28 @@
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
async function attachInstruction(action, selected) {
|
|
137
|
+
const selection = await attach('edit', selected);
|
|
138
|
+
if (!selection) return;
|
|
139
|
+
const task = deps.getPanel()?.querySelector('[data-task]');
|
|
140
|
+
if (!task) return;
|
|
141
|
+
if (task.value.trim()) {
|
|
142
|
+
deps.toast?.(tx('Kept your draft. The selection is attached for editing.', '已保留你的草稿,选区已附上,仅编辑此处。'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const instructions = {
|
|
146
|
+
polish: tx('Polish the selected passage for clarity and flow. Keep the meaning, citations, labels and math unchanged.',
|
|
147
|
+
'润色选中段落,让表达更清晰流畅,保持原意以及引用、标签、公式不变。'),
|
|
148
|
+
shorten: tx('Shorten the selected passage by about 20%. Keep citations, labels, math and the main claim unchanged.',
|
|
149
|
+
'将选中段落精简约 20%,保留引用、标签、公式和核心论点不变。'),
|
|
150
|
+
translate: tx('Translate the selected passage into English. Keep LaTeX commands, citations, labels and math unchanged.',
|
|
151
|
+
'将选中段落翻译成英文,保留 LaTeX 命令、引用、标签和公式不变。')
|
|
152
|
+
};
|
|
153
|
+
task.value = instructions[action];
|
|
154
|
+
task.setSelectionRange(task.value.length, task.value.length);
|
|
155
|
+
task.dispatchEvent(new Event('input', { bubbles: true }));
|
|
156
|
+
}
|
|
157
|
+
|
|
136
158
|
function editorFor(node) {
|
|
137
159
|
const element = node?.nodeType === 1 ? node : node?.parentElement;
|
|
138
160
|
const editor = element?.closest?.('.cm-editor');
|
|
@@ -164,6 +186,9 @@
|
|
|
164
186
|
toolbar.setAttribute('aria-label', tx('Selected text actions', '选区操作'));
|
|
165
187
|
toolbar.querySelector('[data-selection-action="reference"]').textContent = tx('Add to Chat', '加入对话');
|
|
166
188
|
toolbar.querySelector('[data-selection-action="edit"]').textContent = tx('Edit Selection', '仅编辑此处');
|
|
189
|
+
toolbar.querySelector('[data-selection-action="polish"]').textContent = tx('Polish', '润色');
|
|
190
|
+
toolbar.querySelector('[data-selection-action="shorten"]').textContent = tx('Shorten', '精简');
|
|
191
|
+
toolbar.querySelector('[data-selection-action="translate"]').textContent = tx('Translate', '翻译');
|
|
167
192
|
}
|
|
168
193
|
|
|
169
194
|
function ensureToolbar() {
|
|
@@ -174,8 +199,8 @@
|
|
|
174
199
|
toolbar.setAttribute('role', 'toolbar');
|
|
175
200
|
toolbar.hidden = true;
|
|
176
201
|
if (typeof toolbar.showPopover === 'function') toolbar.setAttribute('popover', 'manual');
|
|
177
|
-
for (const mode of ['reference', 'edit']) {
|
|
178
|
-
if (mode === 'edit') {
|
|
202
|
+
for (const mode of ['reference', 'edit', 'polish', 'shorten', 'translate']) {
|
|
203
|
+
if (mode === 'edit' || mode === 'polish') {
|
|
179
204
|
const divider = document.createElement('span');
|
|
180
205
|
divider.className = 'codex-selection-action-divider';
|
|
181
206
|
divider.setAttribute('aria-hidden', 'true');
|
|
@@ -186,7 +211,9 @@
|
|
|
186
211
|
button.dataset.selectionAction = mode;
|
|
187
212
|
button.addEventListener('click', () => {
|
|
188
213
|
const selected = candidate;
|
|
189
|
-
if (selected)
|
|
214
|
+
if (!selected) return;
|
|
215
|
+
if (mode === 'reference' || mode === 'edit') void attach(mode, selected);
|
|
216
|
+
else void attachInstruction(mode, selected);
|
|
190
217
|
});
|
|
191
218
|
toolbar.append(button);
|
|
192
219
|
}
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
})(typeof window !== 'undefined' ? window : globalThis, function sensitivePreflightFactory() {
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
+
// Report evidence admits detector identities only, never matched text or previews.
|
|
11
|
+
const REPORT_DETECTOR_IDS = new Set(['private-key', 'bearer-token', 'api-token', 'aws-access-key',
|
|
12
|
+
'google-api-key', 'huggingface-token', 'gitlab-token', 'stripe-live-secret', 'jwt-token', 'secret-assignment']);
|
|
13
|
+
|
|
10
14
|
function create({
|
|
11
15
|
SensitiveScan, normalizeGovernanceRules, isTextSnapshotFile, sendBackgroundNative,
|
|
12
16
|
getCurrentProjectId, appendRunEvent, appendCompletionReport, showPluginConfirm, tr, tx
|
|
@@ -62,12 +66,21 @@
|
|
|
62
66
|
}
|
|
63
67
|
// Blocking an extra message must not finish the task already in progress.
|
|
64
68
|
if (guidance) return { blocked: true, findings };
|
|
65
|
-
|
|
69
|
+
const detectorIds = [...new Set(findings.map(finding => finding?.detectorId))];
|
|
70
|
+
appendCompletionReport({ conclusionKey: 'sensitive_blocked',
|
|
66
71
|
conclusion: tx(
|
|
67
72
|
'This run was blocked before Codex received project context because sensitive content was detected.',
|
|
68
73
|
'检测到敏感内容,本轮已在向 Codex 发送项目上下文前阻止。'
|
|
69
74
|
),
|
|
70
75
|
status: 'blocked',
|
|
76
|
+
unchangedReason: tx('Sensitive content was found before sending.', '发送前发现了敏感内容。'),
|
|
77
|
+
failure: { code: 'sensitive_content_blocked', stage: 'preflight', severity: 'blocked',
|
|
78
|
+
retryable: false, changedDocument: false,
|
|
79
|
+
evidence: {
|
|
80
|
+
// An unknown type makes the cause generic, even alongside one known type.
|
|
81
|
+
detectorIds: detectorIds.every(id => REPORT_DETECTOR_IDS.has(id)) ? detectorIds.slice(0, 5) : [],
|
|
82
|
+
findingCount: findings.length
|
|
83
|
+
} },
|
|
71
84
|
operations: [],
|
|
72
85
|
applyResults: [],
|
|
73
86
|
nextStep: tx(
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
const {
|
|
12
12
|
SessionPanel,
|
|
13
13
|
SessionState,
|
|
14
|
-
SessionMenuView
|
|
14
|
+
SessionMenuView,
|
|
15
|
+
PanelIcons
|
|
15
16
|
} = deps;
|
|
16
17
|
const {
|
|
17
18
|
createSession,
|
|
@@ -30,6 +31,9 @@
|
|
|
30
31
|
saveState,
|
|
31
32
|
saveStateSoon,
|
|
32
33
|
readPanelInputs,
|
|
34
|
+
flushComposerDraft,
|
|
35
|
+
setSessionTransition,
|
|
36
|
+
getDraftScope,
|
|
33
37
|
applyStateToPanel,
|
|
34
38
|
syncSessionProvider,
|
|
35
39
|
getPanel,
|
|
@@ -47,6 +51,7 @@
|
|
|
47
51
|
getActiveSession,
|
|
48
52
|
getPanel,
|
|
49
53
|
getSessionDisplayTitle,
|
|
54
|
+
icons: PanelIcons,
|
|
50
55
|
getState,
|
|
51
56
|
isDisplayableSession,
|
|
52
57
|
isSessionRunning,
|
|
@@ -60,7 +65,31 @@
|
|
|
60
65
|
closeSessionMenu
|
|
61
66
|
} = sessionMenuView;
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
let transitionToken = null;
|
|
69
|
+
|
|
70
|
+
async function withSessionTransition(change) {
|
|
71
|
+
if (transitionToken) return;
|
|
72
|
+
const token = {};
|
|
73
|
+
transitionToken = token;
|
|
74
|
+
const scope = getDraftScope?.();
|
|
75
|
+
try {
|
|
76
|
+
// Lock before the first wait, including an outstanding IME commit.
|
|
77
|
+
await setSessionTransition?.(true);
|
|
78
|
+
if (getDraftScope?.() !== scope) return;
|
|
79
|
+
return await change(scope);
|
|
80
|
+
} finally {
|
|
81
|
+
if (transitionToken === token) {
|
|
82
|
+
transitionToken = null;
|
|
83
|
+
setSessionTransition?.(false);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function startNewSession() {
|
|
89
|
+
return withSessionTransition(createNewSession);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function createNewSession(draftScope) {
|
|
64
93
|
// Leaving a still-running session is allowed but never silent: the run
|
|
65
94
|
// keeps going in the background and lands in its own session.
|
|
66
95
|
const runningActive = getActiveSession(getState());
|
|
@@ -74,7 +103,10 @@
|
|
|
74
103
|
return;
|
|
75
104
|
}
|
|
76
105
|
}
|
|
106
|
+
if (getDraftScope?.() !== draftScope) return;
|
|
77
107
|
readPanelInputs();
|
|
108
|
+
await flushComposerDraft?.();
|
|
109
|
+
if (getDraftScope?.() !== draftScope) return;
|
|
78
110
|
// Reuse an already-empty, idle active session instead of minting a ghost.
|
|
79
111
|
// Empty sessions are invisible in the list yet still consume the storage cap
|
|
80
112
|
// (slice(-20) / maxSessions), so spamming "New Session" could silently evict
|
|
@@ -99,6 +131,7 @@
|
|
|
99
131
|
if (!evictionApproved) {
|
|
100
132
|
return;
|
|
101
133
|
}
|
|
134
|
+
if (getDraftScope?.() !== draftScope) return;
|
|
102
135
|
}
|
|
103
136
|
const session = createSession({
|
|
104
137
|
mode: getState().mode,
|
|
@@ -114,15 +147,22 @@
|
|
|
114
147
|
activeSessionId: session.id
|
|
115
148
|
}));
|
|
116
149
|
applyStateToPanel();
|
|
150
|
+
const destinationScope = getDraftScope?.();
|
|
117
151
|
await syncSessionProvider?.();
|
|
152
|
+
if (getDraftScope?.() !== destinationScope) return;
|
|
118
153
|
await saveState();
|
|
154
|
+
if (getDraftScope?.() !== destinationScope) return;
|
|
119
155
|
getPanel().querySelector('[data-task]')?.focus();
|
|
120
156
|
}
|
|
121
157
|
|
|
122
|
-
|
|
158
|
+
function switchSession(sessionId) {
|
|
123
159
|
if (sessionId === getState().activeSessionId) {
|
|
124
160
|
return;
|
|
125
161
|
}
|
|
162
|
+
return withSessionTransition(scope => switchToSession(sessionId, scope));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function switchToSession(sessionId, draftScope) {
|
|
126
166
|
const runningActive = getActiveSession(getState());
|
|
127
167
|
if (runningActive && isSessionRunning(runningActive)) {
|
|
128
168
|
const approved = await showPluginConfirm({
|
|
@@ -134,10 +174,15 @@
|
|
|
134
174
|
return;
|
|
135
175
|
}
|
|
136
176
|
}
|
|
177
|
+
if (getDraftScope?.() !== draftScope) return;
|
|
137
178
|
readPanelInputs();
|
|
179
|
+
await flushComposerDraft?.();
|
|
180
|
+
if (getDraftScope?.() !== draftScope) return;
|
|
138
181
|
setState(setActiveSession(getState(), sessionId));
|
|
139
182
|
applyStateToPanel();
|
|
183
|
+
const destinationScope = getDraftScope?.();
|
|
140
184
|
await syncSessionProvider?.();
|
|
185
|
+
if (getDraftScope?.() !== destinationScope) return;
|
|
141
186
|
await saveState();
|
|
142
187
|
}
|
|
143
188
|
|
|
@@ -295,6 +340,7 @@
|
|
|
295
340
|
bindActiveSessionHeader,
|
|
296
341
|
startNewSession,
|
|
297
342
|
switchSession,
|
|
343
|
+
isTransitioning: () => transitionToken !== null,
|
|
298
344
|
deleteSessionWithConfirm,
|
|
299
345
|
renderSessionList,
|
|
300
346
|
commitSessionRename,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
getPanel,
|
|
12
12
|
getSessionDisplayTitle,
|
|
13
13
|
getState,
|
|
14
|
+
icons = null,
|
|
14
15
|
isDisplayableSession,
|
|
15
16
|
isSessionRunning,
|
|
16
17
|
startNewSession,
|
|
@@ -20,19 +21,20 @@
|
|
|
20
21
|
|
|
21
22
|
function applySessionLabel() {
|
|
22
23
|
const trigger = getPanel().querySelector('[data-session-menu-trigger]');
|
|
23
|
-
if (trigger) {
|
|
24
|
-
trigger.title = tr('sessionMenuOpen');
|
|
25
|
-
trigger.setAttribute('aria-label', tr('sessionMenuOpen'));
|
|
26
|
-
}
|
|
27
24
|
const textNode = getPanel().querySelector('[data-session-label-text]');
|
|
28
25
|
const deleteButton = getPanel().querySelector('[data-session-delete]');
|
|
29
26
|
const renameButton = getPanel().querySelector('[data-session-rename]');
|
|
30
27
|
const renameInput = getPanel().querySelector('[data-session-rename-input]');
|
|
31
28
|
const active = getActiveSession(getState());
|
|
29
|
+
const label = active && isDisplayableSession(active)
|
|
30
|
+
? getSessionDisplayTitle(active)
|
|
31
|
+
: tr('newSessionFallback');
|
|
32
32
|
if (textNode) {
|
|
33
|
-
textNode.textContent =
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
textNode.textContent = label;
|
|
34
|
+
}
|
|
35
|
+
if (trigger) {
|
|
36
|
+
trigger.title = `${label} · ${tr('sessionMenuOpen')}`;
|
|
37
|
+
trigger.setAttribute('aria-label', `${tr('sessionMenuOpen')}: ${label}`);
|
|
36
38
|
}
|
|
37
39
|
if (deleteButton) {
|
|
38
40
|
const running = active ? isSessionRunning(active) : false;
|
|
@@ -111,11 +113,15 @@
|
|
|
111
113
|
.slice()
|
|
112
114
|
.sort((a, b) => String(b.updatedAt || b.createdAt || '').localeCompare(String(a.updatedAt || a.createdAt || '')));
|
|
113
115
|
for (const session of sessions) {
|
|
114
|
-
const row = document.createElement('
|
|
115
|
-
row.type = 'button';
|
|
116
|
+
const row = document.createElement('div');
|
|
116
117
|
row.className = 'codex-session-menu-item';
|
|
118
|
+
row.setAttribute('role', 'none');
|
|
117
119
|
row.dataset.active = session.id === state?.activeSessionId ? 'true' : 'false';
|
|
118
120
|
row.dataset.running = isSessionRunning(session) ? 'true' : 'false';
|
|
121
|
+
const switchButton = document.createElement('button');
|
|
122
|
+
switchButton.type = 'button';
|
|
123
|
+
switchButton.className = 'codex-session-menu-switch';
|
|
124
|
+
switchButton.setAttribute('role', 'menuitem');
|
|
119
125
|
const title = document.createElement('span');
|
|
120
126
|
title.className = 'codex-session-menu-item-title';
|
|
121
127
|
title.textContent = getSessionDisplayTitle(session);
|
|
@@ -127,26 +133,56 @@
|
|
|
127
133
|
if (absoluteTs) {
|
|
128
134
|
time.title = new Date(absoluteTs).toLocaleString();
|
|
129
135
|
}
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
switchButton.append(title, time);
|
|
137
|
+
switchButton.addEventListener('click', event => {
|
|
132
138
|
event.stopPropagation();
|
|
133
139
|
closeSessionMenu();
|
|
134
140
|
if (session.id !== getState()?.activeSessionId) {
|
|
135
141
|
switchSession(session.id);
|
|
136
142
|
}
|
|
137
143
|
});
|
|
144
|
+
const deleteButton = menuAction('codex-session-menu-item-delete', 'close', '',
|
|
145
|
+
row.dataset.running === 'true' ? tr('deleteRunningSession') : tr('deleteSession'),
|
|
146
|
+
() => deleteSessionWithConfirm(session.id));
|
|
147
|
+
deleteButton.setAttribute('role', 'menuitem');
|
|
148
|
+
deleteButton.disabled = row.dataset.running === 'true';
|
|
149
|
+
row.append(switchButton, deleteButton);
|
|
138
150
|
menu.append(row);
|
|
139
151
|
}
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
152
|
+
const footer = document.createElement('div');
|
|
153
|
+
footer.className = 'codex-session-menu-footer';
|
|
154
|
+
const active = getActiveSession(state);
|
|
155
|
+
const running = active ? isSessionRunning(active) : false;
|
|
156
|
+
const soleEmpty = Boolean(active) && !isDisplayableSession(active) && (state?.sessions || []).length <= 1;
|
|
157
|
+
const createButton = menuAction('codex-session-menu-new', 'plus', tr('sessionMenuNewShort'), tr('newSession'), () => startNewSession());
|
|
158
|
+
const renameButton = menuAction('codex-session-menu-rename', 'pencil', tr('sessionMenuRenameShort'), tr('renameSession'), () => beginActiveSessionRename());
|
|
159
|
+
renameButton.disabled = !active;
|
|
160
|
+
const deleteButton = menuAction('codex-session-menu-delete', 'close', tr('sessionMenuDeleteShort'),
|
|
161
|
+
running ? tr('deleteRunningSession') : tr('deleteSession'), () => {
|
|
162
|
+
if (getState()?.activeSessionId) deleteSessionWithConfirm(getState().activeSessionId);
|
|
163
|
+
});
|
|
164
|
+
deleteButton.disabled = !active || running || soleEmpty;
|
|
165
|
+
footer.append(createButton, renameButton, deleteButton);
|
|
166
|
+
menu.append(footer);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function menuAction(className, iconName, label, description, onSelect) {
|
|
170
|
+
const button = document.createElement('button');
|
|
171
|
+
button.type = 'button';
|
|
172
|
+
button.className = className;
|
|
173
|
+
button.title = description;
|
|
174
|
+
button.setAttribute('aria-label', description);
|
|
175
|
+
const glyph = icons?.svg?.(iconName);
|
|
176
|
+
if (glyph) button.insertAdjacentHTML('beforeend', glyph);
|
|
177
|
+
const text = document.createElement('span');
|
|
178
|
+
text.textContent = label;
|
|
179
|
+
button.append(text);
|
|
180
|
+
button.addEventListener('click', event => {
|
|
145
181
|
event.stopPropagation();
|
|
146
182
|
closeSessionMenu();
|
|
147
|
-
|
|
183
|
+
onSelect();
|
|
148
184
|
});
|
|
149
|
-
|
|
185
|
+
return button;
|
|
150
186
|
}
|
|
151
187
|
|
|
152
188
|
function beginActiveSessionRename() {
|
|
@@ -74,20 +74,40 @@
|
|
|
74
74
|
.filter(record => !deleted.has(record.id))
|
|
75
75
|
.map(record => [record.id, record]));
|
|
76
76
|
const mergedRecords = sessionRecords.map(record => StorageDb.mergeSessionReviewState?.(
|
|
77
|
-
record, existingById.get(record.id), options.reviewRunIds
|
|
77
|
+
record, existingById.get(record.id), options.reviewRunIds, { shared: StorageDb.sharedSessionsEnabled?.() === true }
|
|
78
78
|
) || record);
|
|
79
|
+
const reviewRunIds = new Set(options.reviewRunIds || []);
|
|
80
|
+
// Shared storage merges local edits and explicitly requested Undo markers
|
|
81
|
+
// against the page baseline. Legacy and ordinary stale saves keep the age guard.
|
|
79
82
|
const writable = mergedRecords.filter(record => {
|
|
80
83
|
const existing = existingById.get(record.id);
|
|
81
84
|
if (!existing) return true;
|
|
82
|
-
return (Date.parse(record.updatedAt || '') || 0) >= (Date.parse(existing.updatedAt || '') || 0)
|
|
85
|
+
return (Date.parse(record.updatedAt || '') || 0) >= (Date.parse(existing.updatedAt || '') || 0)
|
|
86
|
+
|| (StorageDb.sharedSessionsEnabled?.() === true
|
|
87
|
+
&& (StorageDb.hasUnpersistedSharedSession?.(record) === true
|
|
88
|
+
|| (record.runs || []).some(run => reviewRunIds.has(run.id) && run.undoOwnerProtocol === 1)));
|
|
83
89
|
});
|
|
84
90
|
const cleanedExisting = existingSessions.filter((record, index) =>
|
|
85
91
|
record !== rawExistingSessions[index] && !writable.some(item => item.id === record.id)
|
|
86
92
|
);
|
|
87
93
|
if (cleanedExisting.length || writable.length) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
const reviewReceipts = [];
|
|
95
|
+
if (StorageDb.sharedSessionsEnabled?.() === true && reviewRunIds.size) {
|
|
96
|
+
for (const record of writable) {
|
|
97
|
+
const ids = (record.runs || []).filter(run => reviewRunIds.has(run.id)).map(run => run.id);
|
|
98
|
+
if (ids.length) reviewReceipts.push(await StorageDb.putSessionReviewState(record, ids));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let written;
|
|
102
|
+
try {
|
|
103
|
+
written = await StorageDb.putRecords('sessions', [...cleanedExisting, ...writable], {
|
|
104
|
+
reviewRunIds: options.reviewRunIds, reviewReceipts
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error.code === 'shared_session_conflict' && reviewReceipts.length)
|
|
108
|
+
Object.defineProperty(error, 'reviewReceipts', { value: reviewReceipts, configurable: true });
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
91
111
|
// Check the actual durable result, including a decision already saved
|
|
92
112
|
// from another origin, instead of acknowledging only the proposed view.
|
|
93
113
|
return written.filter(record => writable.some(candidate => candidate.id === record.id));
|