codex-overleaf-link 2.3.4 → 2.3.6
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 +325 -211
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +9 -0
- package/extension/src/backgroundUpdateCoordinator.js +88 -4
- package/extension/src/content/composerPanel.js +1 -1
- package/extension/src/content/contentRuntime.js +51 -35
- package/extension/src/content/diagnosticsController.js +13 -3
- package/extension/src/content/diagnosticsPanel.js +1 -1
- package/extension/src/content/generated/content.bundle.js +1233 -298
- package/extension/src/content/generated/content.bundle.meta.json +8 -3
- package/extension/src/content/globalPreferencesController.js +125 -0
- package/extension/src/content/legacyGlobalRegistry.js +4 -0
- package/extension/src/content/otWarmMirror.js +95 -65
- package/extension/src/content/otWarmMirrorController.js +102 -1
- package/extension/src/content/panelMaintenance.js +26 -15
- package/extension/src/content/panelRenderer.js +6 -1
- package/extension/src/content/projectSettingsCoordinator.js +1 -0
- package/extension/src/content/providerSettingsCoordinator.js +16 -0
- package/extension/src/content/providerSettingsDialog.js +24 -15
- package/extension/src/content/runFailureNotice.js +104 -0
- package/extension/src/content/runScrollLayout.js +124 -0
- package/extension/src/content/runTimelineView.js +36 -10
- package/extension/src/content/settingsPanel.js +32 -13
- package/extension/src/content/themeController.js +3 -0
- package/extension/src/content/updateNotice.js +24 -4
- package/extension/src/page/overleafRealtimeObserver.js +116 -24
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/globalPreferences.js +131 -0
- package/extension/src/shared/i18n.js +79 -39
- package/extension/src/shared/managedUpdateProjection.js +1 -1
- package/extension/src/shared/otText.js +0 -1
- package/extension/src/shared/sessionState.js +17 -21
- package/extension/src/shared/sessionTitle.js +120 -0
- package/extension/src/shared/updateRuntimeIdentity.js +32 -0
- package/extension/styles/panel.css +90 -16
- package/native-host/src/mirrorWorkspace.js +20 -1
- package/native-host/src/releaseProxy.js +177 -0
- package/native-host/src/releaseTransport.js +154 -0
- package/native-host/src/updateManager.js +40 -39
- package/package.json +1 -1
- package/scripts/install-managed.mjs +1 -1
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
const OT_POLL_INTERVAL_MS = 1000;
|
|
11
11
|
const OT_PATCH_DEBOUNCE_MS = 500;
|
|
12
12
|
const OT_MAX_PATCH_BATCH = 25;
|
|
13
|
+
const OT_FRESHNESS_MAX_AGE_MS = 30000;
|
|
14
|
+
const OT_MAX_FILE_BYTES = 1024 * 1024;
|
|
15
|
+
const OT_MAX_PENDING_EVENTS = 64;
|
|
16
|
+
const OT_MAX_PENDING_BYTES = 4 * 1024 * 1024;
|
|
17
|
+
const OT_MAX_BATCH_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
|
|
13
19
|
const PAUSE_STATES = new Set(['running', 'writing', 'undoing', 'compiling']);
|
|
14
20
|
|
|
15
21
|
function buildPatchFilesRequest({ projectId, events, nativeCompatibility } = {}) {
|
|
@@ -67,13 +73,14 @@
|
|
|
67
73
|
return { pause: false };
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
function canUseOtWarmStart({ enabled, focusFiles, mirrorStatus } = {}) {
|
|
76
|
+
function canUseOtWarmStart({ enabled, focusFiles, mirrorStatus, now = Date.now() } = {}) {
|
|
71
77
|
if (!enabled) {
|
|
72
78
|
return { ok: false, reason: 'disabled' };
|
|
73
79
|
}
|
|
74
80
|
if (!mirrorStatus || mirrorStatus.exists !== true) {
|
|
75
81
|
return { ok: false, reason: 'mirror_missing' };
|
|
76
82
|
}
|
|
83
|
+
if (mirrorStatus.dirty === true) return { ok: false, reason: 'mirror_dirty' };
|
|
77
84
|
|
|
78
85
|
const normalizedFocusFiles = normalizePaths(focusFiles);
|
|
79
86
|
if (!normalizedFocusFiles.length) {
|
|
@@ -85,6 +92,8 @@
|
|
|
85
92
|
if (file?.state !== 'fresh') {
|
|
86
93
|
continue;
|
|
87
94
|
}
|
|
95
|
+
const age = now - Date.parse(file.observedAt ?? file.lastPatchAt ?? '');
|
|
96
|
+
if (!Number.isFinite(age) || age < 0 || age > OT_FRESHNESS_MAX_AGE_MS) continue;
|
|
88
97
|
const filePath = normalizePath(file.path);
|
|
89
98
|
if (filePath) {
|
|
90
99
|
freshFiles.add(filePath);
|
|
@@ -102,6 +111,88 @@
|
|
|
102
111
|
return { ok: false, reason: 'missing_ot_fresh_focus' };
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
function textBytes(value) {
|
|
115
|
+
const text = String(value || '');
|
|
116
|
+
return encoder ? encoder.encode(text).byteLength : text.length * 3;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function patchBytes(event) { return textBytes(event.nextContent) + textBytes(event.path) + 512; }
|
|
120
|
+
|
|
121
|
+
function queuePatchEvents(queue, events, now = Date.now()) {
|
|
122
|
+
if (!Array.isArray(events) || events.length > OT_MAX_PENDING_EVENTS) return { ok: false, reason: 'ot_queue_limit' };
|
|
123
|
+
const next = Array.isArray(queue) ? queue.slice() : [];
|
|
124
|
+
let bytes = next.reduce((sum, event) => sum + patchBytes(event), 0);
|
|
125
|
+
if (next.length > OT_MAX_PENDING_EVENTS || bytes > OT_MAX_PENDING_BYTES) return { ok: false, reason: 'ot_queue_limit' };
|
|
126
|
+
for (const input of events) {
|
|
127
|
+
const path = normalizePath(input?.path);
|
|
128
|
+
const time = typeof input?.observedAt === 'number' ? input.observedAt : Date.parse(input?.observedAt || '');
|
|
129
|
+
if (!isSafeRelativePath(path) || path.length > 1024 || typeof input?.nextContent !== 'string' ||
|
|
130
|
+
typeof input.baseHash !== 'string' || !input.baseHash || input.baseHash.length > 128) {
|
|
131
|
+
return { ok: false, reason: 'ot_event_invalid' };
|
|
132
|
+
}
|
|
133
|
+
if (!Number.isFinite(time) || now - time < 0 || now - time > OT_FRESHNESS_MAX_AGE_MS) return { ok: false, reason: 'ot_event_expired' };
|
|
134
|
+
if (textBytes(input.nextContent) > OT_MAX_FILE_BYTES) return { ok: false, reason: 'ot_file_limit' };
|
|
135
|
+
const event = { path, baseHash: input.baseHash, nextContent: input.nextContent,
|
|
136
|
+
nextHash: typeof input.nextHash === 'string' && input.nextHash.length <= 128 ? input.nextHash : '',
|
|
137
|
+
observedAt: new Date(time).toISOString() };
|
|
138
|
+
if (isPrimitiveMetadata(input.observedVersion) && String(input.observedVersion).length <= 128) event.observedVersion = input.observedVersion;
|
|
139
|
+
const tail = next[next.length - 1];
|
|
140
|
+
if (tail?.path === path && tail.nextHash && tail.nextHash === event.baseHash) {
|
|
141
|
+
event.baseHash = tail.baseHash;
|
|
142
|
+
bytes -= patchBytes(next.pop());
|
|
143
|
+
}
|
|
144
|
+
if (event.nextHash !== event.baseHash) { next.push(event); bytes += patchBytes(event); }
|
|
145
|
+
if (next.length > OT_MAX_PENDING_EVENTS || bytes > OT_MAX_PENDING_BYTES) return { ok: false, reason: 'ot_queue_limit' };
|
|
146
|
+
}
|
|
147
|
+
return { ok: true, queue: next, bytes };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function takePatchBatch(queue) {
|
|
151
|
+
let bytes = 0;
|
|
152
|
+
let count = 0;
|
|
153
|
+
while (count < Math.min(queue.length, OT_MAX_PATCH_BATCH)) {
|
|
154
|
+
const size = patchBytes(queue[count]);
|
|
155
|
+
if (bytes + size > OT_MAX_BATCH_BYTES) break;
|
|
156
|
+
bytes += size;
|
|
157
|
+
count += 1;
|
|
158
|
+
}
|
|
159
|
+
return queue.splice(0, count);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function validatePatchReceipt(events, result = {}, now = Date.now()) {
|
|
163
|
+
if ((Array.isArray(result.skippedFiles) && result.skippedFiles.length) || Number(result.skippedCount || 0) > 0) {
|
|
164
|
+
return { ok: false, reason: 'mirror_patch_skipped' };
|
|
165
|
+
}
|
|
166
|
+
if (!events.length || Number(result.appliedCount) !== events.length || !Array.isArray(result.appliedFiles) ||
|
|
167
|
+
result.appliedFiles.length !== events.length || !result.appliedFiles.every((file, index) =>
|
|
168
|
+
normalizePath(file?.path) === events[index].path && (!events[index].nextHash || file.hash === events[index].nextHash))) {
|
|
169
|
+
return { ok: false, reason: 'mirror_patch_invalid_result' };
|
|
170
|
+
}
|
|
171
|
+
if (events.some(event => { const time = Date.parse(event.observedAt);
|
|
172
|
+
return !Number.isFinite(time) || time > now || now - time > OT_FRESHNESS_MAX_AGE_MS;
|
|
173
|
+
})) return { ok: false, reason: 'ot_event_expired' };
|
|
174
|
+
return { ok: true };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeFailureCode(value, fallback = 'ot_error') {
|
|
178
|
+
const code = typeof value === 'string' ? value : value?.code;
|
|
179
|
+
if (/^[a-z][a-z0-9_]{0,79}$/.test(code || '')) return code;
|
|
180
|
+
if (/page bridge|capability|bridge timed out/i.test(String(code || ''))) return 'ot_bridge_unavailable';
|
|
181
|
+
return fallback;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getFailureMessageKey(value) {
|
|
185
|
+
const code = normalizeFailureCode(value);
|
|
186
|
+
if (/bridge|ot_status|ot_events/.test(code)) return 'otFailureBridge';
|
|
187
|
+
if (/missing_ot_text|observer_create|observer_unavailable/.test(code)) return 'otFailureObserver';
|
|
188
|
+
if (code === 'ot_file_limit') return 'otFailureFileLimit';
|
|
189
|
+
if (code === 'ot_queue_limit') return 'otFailureQueueLimit';
|
|
190
|
+
if (code === 'ot_event_expired') return 'otFailureExpired';
|
|
191
|
+
if (/editor|active_path|document/.test(code)) return 'otFailureEditor';
|
|
192
|
+
if (/mirror|hash|baseline/.test(code)) return 'otFailureMirror';
|
|
193
|
+
return 'otFailureGeneric';
|
|
194
|
+
}
|
|
195
|
+
|
|
105
196
|
function normalizePath(value) {
|
|
106
197
|
return String(value ?? '')
|
|
107
198
|
.trim()
|
|
@@ -165,6 +256,16 @@
|
|
|
165
256
|
OT_POLL_INTERVAL_MS,
|
|
166
257
|
OT_PATCH_DEBOUNCE_MS,
|
|
167
258
|
OT_MAX_PATCH_BATCH,
|
|
259
|
+
OT_FRESHNESS_MAX_AGE_MS,
|
|
260
|
+
OT_MAX_FILE_BYTES,
|
|
261
|
+
OT_MAX_PENDING_EVENTS,
|
|
262
|
+
OT_MAX_PENDING_BYTES,
|
|
263
|
+
OT_MAX_BATCH_BYTES,
|
|
264
|
+
queuePatchEvents,
|
|
265
|
+
takePatchBatch,
|
|
266
|
+
validatePatchReceipt,
|
|
267
|
+
normalizeFailureCode,
|
|
268
|
+
getFailureMessageKey,
|
|
168
269
|
buildPatchFilesRequest,
|
|
169
270
|
canUseOtWarmStart,
|
|
170
271
|
normalizePath,
|
|
@@ -129,29 +129,40 @@
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// --- History & storage (v1.7.5): usage summary + clear-all in Settings ---
|
|
132
|
+
let storageUsageRequestId = 0;
|
|
132
133
|
function refreshStorageUsageSummary() {
|
|
133
134
|
const node = getSettingsPanelInstance()?.container?.querySelector('[data-storage-usage]');
|
|
134
135
|
if (!node) {
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
137
|
-
const
|
|
138
|
-
const
|
|
138
|
+
const requestId = ++storageUsageRequestId;
|
|
139
|
+
const projectId = getCurrentProjectId?.();
|
|
140
|
+
const isCurrent = () => requestId === storageUsageRequestId && projectId === getCurrentProjectId?.()
|
|
141
|
+
&& getSettingsPanelInstance()?.container?.querySelector('[data-storage-usage]') === node;
|
|
142
|
+
const current = getState();
|
|
143
|
+
const sessionCount = Array.isArray(current?.sessions) ? current.sessions.length : 0;
|
|
144
|
+
const runCount = Array.isArray(current?.runs) ? current.runs.length : 0;
|
|
139
145
|
const counts = tx(
|
|
140
|
-
`${sessionCount} session(s) · ${runCount}
|
|
141
|
-
|
|
146
|
+
`${sessionCount} loaded session(s) in this project · ${runCount} run(s) in the active session`,
|
|
147
|
+
`当前项目已加载 ${sessionCount} 个会话 · 当前会话 ${runCount} 轮运行`
|
|
142
148
|
);
|
|
149
|
+
// Dynamic text belongs to this renderer, not the loading-label translator.
|
|
150
|
+
node.removeAttribute?.('data-i18n');
|
|
143
151
|
node.textContent = counts;
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
const unavailable = () => {
|
|
153
|
+
if (isCurrent()) node.textContent = tx(`${counts} · Site storage estimate unavailable`, `${counts} · 本站点容量暂无法估算`);
|
|
154
|
+
};
|
|
155
|
+
let timer;
|
|
156
|
+
const estimate = Promise.resolve().then(() => navigator.storage?.estimate?.());
|
|
157
|
+
const deadline = new Promise(resolve => { timer = setTimeout(() => resolve(null), 2000); });
|
|
158
|
+
return Promise.race([estimate, deadline]).then(info => {
|
|
159
|
+
if (!isCurrent()) return;
|
|
160
|
+
const usage = info?.usage;
|
|
161
|
+
if (!Number.isFinite(usage) || usage < 0) { unavailable(); return; }
|
|
162
|
+
const mb = usage / (1024 * 1024);
|
|
163
|
+
const size = usage === 0 ? '0 KB' : mb >= 1 ? `${mb.toFixed(1)} MB` : `${Math.max(1, Math.round(usage / 1024))} KB`;
|
|
164
|
+
node.textContent = tx(`${counts} · Site total ~${size}`, `${counts} · 本站点总占用约 ${size}`);
|
|
165
|
+
}).catch(unavailable).finally(() => clearTimeout(timer));
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
async function clearAllHistoryWithConfirm() {
|
|
@@ -327,7 +327,8 @@
|
|
|
327
327
|
|
|
328
328
|
return new Promise(resolve => {
|
|
329
329
|
let settled = false;
|
|
330
|
-
const
|
|
330
|
+
const returnFocus = doc.activeElement;
|
|
331
|
+
const overlay = doc.createElement('dialog');
|
|
331
332
|
overlay.className = 'codex-plugin-confirm';
|
|
332
333
|
overlay.setAttribute('data-plugin-confirm', 'true');
|
|
333
334
|
overlay.setAttribute('role', 'dialog');
|
|
@@ -371,13 +372,16 @@
|
|
|
371
372
|
card.append(head, body, actions);
|
|
372
373
|
overlay.append(card);
|
|
373
374
|
panel.append(overlay);
|
|
375
|
+
overlay.showModal?.();
|
|
374
376
|
|
|
375
377
|
const cleanup = value => {
|
|
376
378
|
if (settled) return;
|
|
377
379
|
settled = true;
|
|
378
380
|
activeResolve = null;
|
|
379
381
|
doc.removeEventListener('keydown', onKeydown, true);
|
|
382
|
+
overlay.close?.();
|
|
380
383
|
overlay.remove();
|
|
384
|
+
returnFocus?.focus?.();
|
|
381
385
|
resolve(value);
|
|
382
386
|
};
|
|
383
387
|
activeResolve = cleanup;
|
|
@@ -398,6 +402,7 @@
|
|
|
398
402
|
overlay.addEventListener('click', event => {
|
|
399
403
|
if (event.target === overlay) cleanup(false);
|
|
400
404
|
});
|
|
405
|
+
overlay.addEventListener('cancel', event => { event.preventDefault(); cleanup(false); });
|
|
401
406
|
cancel.addEventListener('click', () => cleanup(false));
|
|
402
407
|
confirm.addEventListener('click', () => cleanup(true));
|
|
403
408
|
doc.addEventListener('keydown', onKeydown, true);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
getSettingsPanelInstance: options.getSettingsPanelInstance || (() => null),
|
|
15
15
|
getSelectedModel: options.getSelectedModel || (() => ''),
|
|
16
16
|
getSelectedProviderId: options.getSelectedProviderId || (() => 'builtin'),
|
|
17
|
+
hasCurrentProject: options.hasCurrentProject || (() => true),
|
|
17
18
|
setSelectedProviderId: options.setSelectedProviderId || (() => {}),
|
|
18
19
|
confirmProviderSwitch: options.confirmProviderSwitch || (() => true),
|
|
19
20
|
clearSelectedModel: options.clearSelectedModel || (() => {}),
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
tx: instance.tx,
|
|
44
45
|
ProviderProfiles: Profiles,
|
|
45
46
|
callbacks: {
|
|
47
|
+
hasCurrentProject: instance.hasCurrentProject,
|
|
46
48
|
getCurrentProjectProviderId: () => instance.getSelectedProviderId() || 'builtin',
|
|
47
49
|
onTest: context => testProvider(instance, context),
|
|
48
50
|
onCancelTest: () => cancelProviderTest(instance),
|
|
@@ -57,6 +59,7 @@
|
|
|
57
59
|
return {
|
|
58
60
|
open: () => open(instance),
|
|
59
61
|
refreshSummary: () => refreshSummary(instance),
|
|
62
|
+
renderSummary: () => updateSettingsSummary(instance),
|
|
60
63
|
ensureLoaded: () => ensureLoaded(instance),
|
|
61
64
|
getRunSelection: providerId => instance.loaded
|
|
62
65
|
? Profiles.buildRunSelection(instance.catalog, providerId || instance.getSelectedProviderId())
|
|
@@ -208,6 +211,7 @@
|
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
async function prepareProviderActivation(instance, providerId) {
|
|
214
|
+
requireCurrentProject(instance);
|
|
211
215
|
const previousProviderId = instance.getSelectedProviderId() || 'builtin';
|
|
212
216
|
const provider = instance.Profiles.getProviderById(instance.catalog, providerId);
|
|
213
217
|
if (providerId !== previousProviderId) {
|
|
@@ -301,6 +305,7 @@
|
|
|
301
305
|
async function saveProvider(instance, context, action = {}) {
|
|
302
306
|
instance.dialog.setBusy('saving', instance.tx('Saving provider…', '正在保存模型服务…'));
|
|
303
307
|
try {
|
|
308
|
+
if (action.activate === true) requireCurrentProject(instance);
|
|
304
309
|
const result = await request(instance, 'codex.providers.upsert', {
|
|
305
310
|
profileId: context.profileId,
|
|
306
311
|
expectedRevision: context.expectedRevision,
|
|
@@ -495,6 +500,12 @@
|
|
|
495
500
|
}
|
|
496
501
|
|
|
497
502
|
function updateSettingsSummary(instance, override = {}) {
|
|
503
|
+
if (!instance.hasCurrentProject()) {
|
|
504
|
+
instance.getSettingsPanelInstance()?.setProviderSummary?.({ summary: instance.tx(
|
|
505
|
+
'Shared model API profiles · open a project to select a provider',
|
|
506
|
+
'共享模型 API 配置 · 进入项目后选择使用的服务'), tone: 'info' });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
498
509
|
const selectedProviderId = instance.getSelectedProviderId() || 'builtin';
|
|
499
510
|
const catalogProvider = instance.Profiles.getProviderById(
|
|
500
511
|
instance.catalog,
|
|
@@ -594,6 +605,11 @@
|
|
|
594
605
|
});
|
|
595
606
|
}
|
|
596
607
|
|
|
608
|
+
function requireCurrentProject(instance) {
|
|
609
|
+
if (!instance.hasCurrentProject()) throw createClientError(
|
|
610
|
+
'provider_project_required', instance.tx('Open an Overleaf project before selecting its provider.', '请先进入 Overleaf 项目,再选择该项目使用的模型服务。'));
|
|
611
|
+
}
|
|
612
|
+
|
|
597
613
|
function emptyCatalogChange() {
|
|
598
614
|
return { activeProviderChanged: false, activeModelCatalogChanged: false, changedProviderIds: [] };
|
|
599
615
|
}
|
|
@@ -47,12 +47,13 @@
|
|
|
47
47
|
if (instance.root) {
|
|
48
48
|
return instance.root;
|
|
49
49
|
}
|
|
50
|
-
const root = instance.document.createElement('
|
|
50
|
+
const root = instance.document.createElement('dialog');
|
|
51
51
|
root.className = 'codex-provider-dialog-root';
|
|
52
|
+
root.setAttribute('aria-labelledby', 'codex-provider-dialog-title');
|
|
52
53
|
root.hidden = true;
|
|
53
54
|
root.innerHTML = `
|
|
54
55
|
<div class="codex-provider-dialog-backdrop" data-provider-backdrop></div>
|
|
55
|
-
<section class="codex-provider-dialog"
|
|
56
|
+
<section class="codex-provider-dialog">
|
|
56
57
|
<header class="codex-provider-dialog-head">
|
|
57
58
|
<div class="codex-provider-dialog-heading">
|
|
58
59
|
<div class="codex-provider-dialog-titleline">
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
root.addEventListener('input', event => handleInput(instance, event));
|
|
80
81
|
root.addEventListener('change', event => handleInput(instance, event));
|
|
81
82
|
root.addEventListener('keydown', event => handleKeydown(instance, event));
|
|
83
|
+
root.addEventListener('cancel', event => { event.preventDefault(); requestClose(instance); });
|
|
82
84
|
root.addEventListener('mousedown', event => event.stopPropagation());
|
|
83
85
|
root.addEventListener('click', event => event.stopPropagation());
|
|
84
86
|
return root;
|
|
@@ -89,9 +91,12 @@
|
|
|
89
91
|
instance.root.hidden = false;
|
|
90
92
|
syncTheme(instance);
|
|
91
93
|
setCatalog(instance, catalog || instance.catalog);
|
|
94
|
+
if (!instance.root.open) instance.root.showModal?.();
|
|
92
95
|
queueMicrotask(() => {
|
|
93
|
-
instance.root.
|
|
94
|
-
|
|
96
|
+
if (!instance.root || instance.root.hidden) return;
|
|
97
|
+
const target = instance.root.querySelector('[data-provider-row][aria-current="true"]')
|
|
98
|
+
|| instance.root.querySelector('input, button, select, textarea');
|
|
99
|
+
target?.focus?.();
|
|
95
100
|
});
|
|
96
101
|
}
|
|
97
102
|
|
|
@@ -106,6 +111,7 @@
|
|
|
106
111
|
instance.callbacks.onCancelTest?.();
|
|
107
112
|
instance.busy = '';
|
|
108
113
|
}
|
|
114
|
+
instance.root.close?.();
|
|
109
115
|
instance.root.hidden = true;
|
|
110
116
|
instance.busy = '';
|
|
111
117
|
instance.returnFocus?.focus?.();
|
|
@@ -159,6 +165,9 @@
|
|
|
159
165
|
"Configure experimental third-party integrations used by this project's future Codex runs. Compatibility varies by provider and gateway.",
|
|
160
166
|
'配置当前项目后续 Codex 任务使用的实验性第三方集成;兼容性取决于具体模型服务与网关。'
|
|
161
167
|
);
|
|
168
|
+
if (instance.callbacks.hasCurrentProject?.() === false) instance.root.querySelector('[data-provider-dialog-subtitle]').textContent = tx(
|
|
169
|
+
'Manage shared model API profiles. Open a project to choose which provider its future turns use.',
|
|
170
|
+
'管理共享模型 API 配置。进入项目后,再选择该项目后续任务使用的服务。');
|
|
162
171
|
renderProviderList(instance);
|
|
163
172
|
renderDetail(instance);
|
|
164
173
|
renderFooter(instance);
|
|
@@ -408,8 +417,8 @@
|
|
|
408
417
|
<label class="codex-provider-disclosure">
|
|
409
418
|
<input type="checkbox" data-provider-disclosure ${disclosureSatisfied ? 'checked' : ''}>
|
|
410
419
|
<span data-provider-disclosure-text>${escapeHtml(tx(
|
|
411
|
-
`
|
|
412
|
-
|
|
420
|
+
`Projects using this shared provider may send task context to ${draft.baseUrl || 'this endpoint'}. Each project chooses its own provider.`,
|
|
421
|
+
`使用此共享服务配置的项目可能把任务上下文发送到 ${draft.baseUrl || '此端点'}。各项目独立选择使用的服务。`
|
|
413
422
|
))}</span>
|
|
414
423
|
</label>
|
|
415
424
|
<div class="codex-provider-test-row">
|
|
@@ -456,7 +465,7 @@
|
|
|
456
465
|
'Uses the authentication, model catalog, and provider configuration managed by the local Codex CLI.',
|
|
457
466
|
'使用本地 Codex CLI 管理的身份验证、模型目录和服务配置。'
|
|
458
467
|
))}</p>
|
|
459
|
-
${active
|
|
468
|
+
${instance.callbacks.hasCurrentProject?.() === false ? '' : active
|
|
460
469
|
? `<span class="codex-provider-active-badge">${escapeHtml(tx('Current project', '当前项目'))}</span>`
|
|
461
470
|
: `<button type="button" class="codex-provider-primary-button" data-provider-action="activate-builtin">${escapeHtml(tx('Use for this project', '用于当前项目'))}</button>`}
|
|
462
471
|
</div>
|
|
@@ -464,6 +473,7 @@
|
|
|
464
473
|
}
|
|
465
474
|
|
|
466
475
|
function getCurrentProjectProviderId(instance) {
|
|
476
|
+
if (instance.callbacks.hasCurrentProject?.() === false) return '';
|
|
467
477
|
const providerId = instance.callbacks.getCurrentProjectProviderId?.();
|
|
468
478
|
return typeof providerId === 'string' && providerId.trim()
|
|
469
479
|
? providerId.trim()
|
|
@@ -487,6 +497,7 @@
|
|
|
487
497
|
dirty: instance.dirty,
|
|
488
498
|
destinationChanged,
|
|
489
499
|
canSave: canSaveCurrentDraft(instance),
|
|
500
|
+
hasProject: instance.callbacks.hasCurrentProject?.() !== false,
|
|
490
501
|
canActivate: canActivateCurrentProvider(instance)
|
|
491
502
|
});
|
|
492
503
|
const saveDisabled = actionState.saveEnabled ? '' : ' disabled aria-disabled="true"';
|
|
@@ -508,8 +519,8 @@
|
|
|
508
519
|
const dirty = options.dirty === true;
|
|
509
520
|
return {
|
|
510
521
|
showSave: isNew || dirty,
|
|
511
|
-
showSaveAndUse: (isNew || dirty) && (!active || options.destinationChanged === true),
|
|
512
|
-
showUse: !isNew && !active && !dirty,
|
|
522
|
+
showSaveAndUse: options.hasProject !== false && (isNew || dirty) && (!active || options.destinationChanged === true),
|
|
523
|
+
showUse: options.hasProject !== false && !isNew && !active && !dirty,
|
|
513
524
|
saveEnabled: options.canSave === true,
|
|
514
525
|
useEnabled: options.canActivate === true
|
|
515
526
|
};
|
|
@@ -662,7 +673,7 @@
|
|
|
662
673
|
return;
|
|
663
674
|
}
|
|
664
675
|
const focusable = Array.from(instance.root.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'))
|
|
665
|
-
.filter(element => !element.closest('[hidden]'));
|
|
676
|
+
.filter(element => !element.closest('[hidden]') && (!element.getClientRects || element.getClientRects().length));
|
|
666
677
|
if (!focusable.length) {
|
|
667
678
|
return;
|
|
668
679
|
}
|
|
@@ -941,8 +952,8 @@
|
|
|
941
952
|
if (!text) return;
|
|
942
953
|
const baseUrl = getCurrentBaseUrl(instance) || instance.tx('this endpoint', '此端点');
|
|
943
954
|
text.textContent = instance.tx(
|
|
944
|
-
`
|
|
945
|
-
|
|
955
|
+
`Projects using this shared provider may send task context to ${baseUrl}. Each project chooses its own provider.`,
|
|
956
|
+
`使用此共享服务配置的项目可能把任务上下文发送到 ${baseUrl}。各项目独立选择使用的服务。`
|
|
946
957
|
);
|
|
947
958
|
}
|
|
948
959
|
|
|
@@ -984,10 +995,7 @@
|
|
|
984
995
|
if (!panel) {
|
|
985
996
|
return;
|
|
986
997
|
}
|
|
987
|
-
const style = instance.document.defaultView.getComputedStyle(panel);
|
|
988
998
|
instance.root.dataset.theme = panel.dataset.theme === 'light' ? 'light' : 'dark';
|
|
989
|
-
instance.root.style.setProperty('--provider-panel-bg', style.backgroundColor || '#151716');
|
|
990
|
-
instance.root.style.setProperty('--provider-panel-color', style.color || '#eceeea');
|
|
991
999
|
}
|
|
992
1000
|
|
|
993
1001
|
function escapeHtml(value) {
|
|
@@ -1011,6 +1019,7 @@
|
|
|
1011
1019
|
}
|
|
1012
1020
|
|
|
1013
1021
|
function destroy(instance) {
|
|
1022
|
+
instance.root?.close?.();
|
|
1014
1023
|
instance.root?.remove?.();
|
|
1015
1024
|
instance.root = null;
|
|
1016
1025
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
(function initRunFailureNotice(root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) {
|
|
3
|
+
module.exports = factory();
|
|
4
|
+
} else {
|
|
5
|
+
root.CodexOverleafModuleRegistry.define('RunFailureNotice', [], factory);
|
|
6
|
+
}
|
|
7
|
+
})(typeof window !== 'undefined' ? window : globalThis, function runFailureNoticeFactory() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const REVIEW_CODES = new Set(['write_timeout', 'partial_write_needs_review']);
|
|
11
|
+
const CANCEL_CODES = new Set(['cancelled', 'user_cancelled', 'codex_cancelled']);
|
|
12
|
+
|
|
13
|
+
function create({ tr, projectRunSettlement, sanitizeText = value => value,
|
|
14
|
+
document: documentRef = globalThis.document } = {}) {
|
|
15
|
+
const notices = new WeakMap();
|
|
16
|
+
|
|
17
|
+
function append(report, event = {}, run = {}) {
|
|
18
|
+
const projection = typeof projectRunSettlement === 'function' && run
|
|
19
|
+
? projectRunSettlement(run) : null;
|
|
20
|
+
const failure = projection?.primaryFailure || event.failure;
|
|
21
|
+
if (report.dataset.status === 'cancelled' || CANCEL_CODES.has(failure?.code)) return;
|
|
22
|
+
const facts = run?.settlement || run?.settlementFacts || {};
|
|
23
|
+
const unconfirmed = REVIEW_CODES.has(failure?.code)
|
|
24
|
+
|| facts.documentEffect === 'possibly-changed'
|
|
25
|
+
|| (facts.evidence?.applied === 'unknown' && facts.evidence?.settled === 'needs-review');
|
|
26
|
+
const partial = facts.evidence?.applied === 'partial' || run?.partialWriteback === true;
|
|
27
|
+
if (!failure && !unconfirmed && !partial && report.dataset.status !== 'failed') return;
|
|
28
|
+
|
|
29
|
+
const notice = documentRef.createElement('aside');
|
|
30
|
+
notice.className = 'run-feedback';
|
|
31
|
+
notice.dataset.tone = unconfirmed || partial || failure?.terminalState === 'degraded'
|
|
32
|
+
|| failure?.terminalState === 'needs_review' ? 'warning' : 'error';
|
|
33
|
+
notice.setAttribute('role', 'note');
|
|
34
|
+
const heading = documentRef.createElement('strong');
|
|
35
|
+
heading.className = 'run-feedback__title';
|
|
36
|
+
heading.textContent = tr(unconfirmed ? 'runFeedbackUnconfirmedTitle'
|
|
37
|
+
: partial ? 'runFeedbackPartialTitle' : 'runFeedbackFailedTitle');
|
|
38
|
+
const message = documentRef.createElement('p');
|
|
39
|
+
const detail = typeof failure?.userMessage === 'string' ? failure.userMessage : '';
|
|
40
|
+
const file = safeProjectPath(failure?.file);
|
|
41
|
+
message.textContent = sanitizeText([file, detail].filter(Boolean).join(': ').slice(0, 1000))
|
|
42
|
+
|| tr('runFeedbackGeneric');
|
|
43
|
+
notice.append(heading, message);
|
|
44
|
+
if (unconfirmed) {
|
|
45
|
+
const warning = documentRef.createElement('p');
|
|
46
|
+
warning.textContent = tr('runFeedbackUnconfirmed');
|
|
47
|
+
notice.append(warning);
|
|
48
|
+
}
|
|
49
|
+
notices.set(report, { notice, unconfirmed, failure, file });
|
|
50
|
+
report.append(notice);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatMetaValue(report, row) {
|
|
54
|
+
if (!notices.get(report)?.unconfirmed) return row.value;
|
|
55
|
+
const key = String(row.key || '').toLowerCase().replace(/[^a-z]/g, '');
|
|
56
|
+
const label = String(row.label || '').trim().toLowerCase();
|
|
57
|
+
if (key === 'writeresult' || key === 'next'
|
|
58
|
+
|| ['write result', 'next', '\u5199\u5165\u7ed3\u679c', '\u4e0b\u4e00\u6b65'].includes(label)) {
|
|
59
|
+
return tr('runFeedbackUnconfirmedResult');
|
|
60
|
+
}
|
|
61
|
+
return row.value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function prepareRecovery(report, { openFile } = {}) {
|
|
65
|
+
const info = notices.get(report);
|
|
66
|
+
const target = info?.notice || report;
|
|
67
|
+
if (!info?.unconfirmed) return { target, handled: false };
|
|
68
|
+
const canOpen = Boolean(info.file && typeof openFile === 'function');
|
|
69
|
+
const button = documentRef.createElement('button');
|
|
70
|
+
button.type = 'button';
|
|
71
|
+
button.className = 'run-final-answer__recovery-action';
|
|
72
|
+
button.dataset.recoveryFor = info.failure?.code || 'write_unconfirmed';
|
|
73
|
+
button.textContent = tr(canOpen ? 'runFeedbackReviewFile' : 'runFeedbackReviewDetails', { file: info.file });
|
|
74
|
+
button.addEventListener('click', event => {
|
|
75
|
+
event.stopPropagation();
|
|
76
|
+
if (canOpen) {
|
|
77
|
+
openFile(info.file);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const process = report.closest?.('[data-run-id]')?.querySelector?.('[data-run-process]');
|
|
81
|
+
if (process) {
|
|
82
|
+
process.open = true;
|
|
83
|
+
process.scrollIntoView?.({ block: 'nearest' });
|
|
84
|
+
} else {
|
|
85
|
+
button.disabled = true;
|
|
86
|
+
button.title = tr('runFeedbackDetailsUnavailable');
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
target.append(button);
|
|
90
|
+
return { target, handled: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Object.freeze({ append, formatMetaValue, prepareRecovery });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function safeProjectPath(value) {
|
|
97
|
+
if (typeof value !== 'string') return '';
|
|
98
|
+
const path = value.replace(/\\/g, '/');
|
|
99
|
+
return path && !path.startsWith('/') && !/^[A-Za-z]:/.test(path)
|
|
100
|
+
&& !path.split('/').includes('..') ? path.slice(0, 300) : '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Object.freeze({ create });
|
|
104
|
+
});
|